I have a list of hidden items.
I need to show the list and then scroll to one of them with a single click.
I reproduced the code here: http://plnkr.co/edit/kp5dJZFYU3tZS6DiQUKz?p=preview
As I see in the console, scrollTop() is called before the items are visible, so I think that ng-show is not instant and this approach is wrong.
It works deferring scrollTop() with a timeout, but I don't want to do that.
Are there other solutions?
I don't see any other solution than deferring the invocation of scrollTop() when using ng-show. You have to wait until the changes in your model are reflected in the DOM, so the elements become visible. The reason why they do not appear instantly is the scope life cycle. ng-show internally uses a watch listener that is only fired when the $digest() function of the scope is called after the execution of the complete code in your click handler.
See http://docs.angularjs.org/api/ng.$rootScope.Scope for a more detailed explanation of the scope life cycle.
Usually it should not be a problem to use a timeout that executes after this event with no delay like this:
setTimeout(function() {
$(window).scrollTop(50);
}, 0);
Alternative solution without timeout:
However, if you want to avoid the timeout event (the execution of which may be preceded by other events in the event queue) and make sure that scrolling happens within the click event handler. You can do the following in your controller:
$scope.$watch('itemsVisible', function(newValue, oldValue) {
if (newValue === true && oldValue === false) {
$scope.$evalAsync(function() {
$(window).scrollTop(50);
});
}
});
The watch listener fires within the same invocation of $digest() as the watch listener registered by the ng-show directive. The function passed to $evalAsync() is executed by angular right after all watch listeners are processed, so the elements have been already made visible by the ng-show directive.
You can use the $anchorScroll.
Here is the documentation:
https://docs.angularjs.org/api/ng/service/$anchorScroll
Example:
$scope.showDiv = function()
{
$scope.showDivWithObjects = true;
$location.hash('div-id-here');
$anchorScroll();
}
Related
There is 1 angular app, with 1 parent controller, and a child controller.
In the child, there is 1 $watch WATCH-CHILD for OBJ-CHILD, which triggers an $emit.
In the parent, there is a listener for the $emit, we'll call it ON-LISTENER, and a $watch WATCH-PARENT for OBJ-PARENT (which uses true as the 3rd argument).
When the child's OBJ-CHILD is changed, it triggers WATCH-CHILD, which triggers the $emit.
The parent listener ON-LISTENER is fired, and changes OBJ-PARENT. It also sets some $location properties.
The $watch WATCH-PARENT for OBJ-PARENT is never fired (even though the value has changed), as well as the properties set on $location not changed in the browser URL (I know they are indeed changed inside the JavaScript, cause I print them).
In order to make sure that ON-LISTENER is called within a $digest, I tried to call $digest at the end of ON-LISTENER, and got the expected exception.
Any idea if I'm doing something wrong? I expect the changes that occur in ON-LISTENER to trigger WATCH-PARENT and browser URL change.
I will try to reproduce on jsfiddle and edit this post if successful.
The code looks like:
CHILD:
$scope.$watch('vars.model', function(newValue, oldValue) {
console.log('model changed');
$scope.$emit('highlightChange', newValue);
}, true);
PARENT:
$scope.$watch('vars.model.highlight', function(newValue, oldValue) {
console.log('highlight changed');
}, true);
$scope.$on('highlightChange', function(event, value) {
console.log('listener', $scope.vars.model.highlight.categoryId, value.categoryId);
$location.search('category-id', value.categoryId);
$scope.vars.model.highlight.categoryId = value.categoryId;
}
Next time please provide more code which works, that way you can get better answers.
Here is a Demo plunker which I created to test the code which you provided. It works just fine. If you could provide more code then we could find the real reason why it did not work.
I created two controllers parentCtrl and childCtrl which uses your code and object of provided structure.
$scope.vars = {
model:{
highlight:{
categoryId : 5 //This value is set for testing purposes
}
}
};
Also, I changed watch target (vars.model -> vars.model.highlight) to be the same as in parent controller
$scope.$watch('vars.model.highlight', function(newValue, oldValue) {
console.log('child model changed (new/old)', newValue, oldValue);
$scope.$emit('highlightChange', newValue);
console.log('Emited change event');
}, true);
Thanks for the help. I found out that my event originated from a manual call to $scope.$digest() due to the originating event being triggered from a daterangepicker 'apply.daterangepicker' event.
When I changed that to $scope.$apply, the problem seemed to go away.
I can assume from that, that $apply() is the one in charge of keep calling $watch as long as there are changes, and that $digest() doesn't do so.
For future reference, I placed the problem here:
https://plnkr.co/edit/ItkALhw16Aqukk3EFrRz?p=preview
$scope.digest();
should become
$scope.apply();
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.
I have this code inside an angular directive, and I'm finding the $watch behavior a bit confusing. The updateSelect is called in an "ng-click":
scope.updateSelect = function (type) {
scope.selectionCtrl.activeList = scope.seedLists[type];
scope.selectionCtrl.activeListKey = type;
scope.selectionCtrl.activeSelection = scope.selection[type];
scope.selectionCtrl.staged = [];
scope.selectionCtrl.stageRemove = [];
if (type !== scope.activeTab) {
scope.activeTab = type;
}
console.log("update");
};
scope.$watch('selectionCtrl.activeList', function(newValue, oldValue) {
console.log("watch");
}, true);
When I click on the button (triggering updateSelect), and watch the console, I see "update" and then "watch". The first thing that happens inside the function is selectionCtrl.activeList is set, so I would expect to see "watch" and then "update".
Shouldn't watch trigger as soon as the array has changed?
The function has to finish first as javascript is single threaded.
Because the function was called via the ng-click directive, angular will run a digest cycle. Part of that digest cycle is to run through the watch list and resolve all the changes that may have occurred since the cycle last ran.
In the example you give, selectionCtrl.activeList is changed in updateSelect which subsequently results in the watch callback being called.
When does Angular execute watch callback?
It's related to $digest and $apply, and certainly it does not execute within your raw javascript code.
To make watch execute forcefully, you can run $scope.apply() manually, but may cause more problem and not necessary if it is within a angularjs function, i.e. $timeout, $interval, etc, because it will be called automatically after the function.
For more info., lookup;
How do I use $scope.$watch and $scope.$apply in AngularJS?
https://groups.google.com/forum/#!topic/angular/HnJZTEXRztk
https://docs.angularjs.org/api/ng/type/$rootScope.Scope :
The watchExpression is called on every call to $digest() and should return the value that will be watched. (Since $digest() reruns when it detects changes the watchExpression can execute multiple times per $digest() and should be idempotent.)
If you try i.e.:
scope.selectionCtrl.activeList = scope.seedLists[type];
scope.$digest();
you'll get Error: [$rootScope:inprog] $apply already in progress.
I have some code that is not behaving as expected... I have an event listener within a AngularJS controller like this:
$scope.$on("newClipSelected", function(e) {
$scope.$apply(function() {
$scope.isReady = true;
});
});
The controller's markup has a ng-show='isReady'. When this event handler runs, the ng-show region shows as expected. However, this event handler does not work:
$scope.$on("newClipSelected", function(e) {
$scope.isReady = true;
});
With this event handler, if I use the debugger to expect the AngularJS scope I do see that $scope.isReady=true, however the ng-show element is not showing.
Its my understanding that $scope.$on will in fact call $watch/$apply as appropriate. So why am I needing to call $apply in this case?
The event is being triggered by a call to $rootScope.$broadcast() within a non-angularJS asynchronous completion event.
No, angular doesn't fire $apply on the events so if $broadcast is called outside angular's context, you are going to need to $apply by hand.
Check the source here
$on and $broadcast doesn't call $apply for you, you need to call it yourself. I'm not sure why that is, but my guess is that it's because a digest is a bit expensive, and it would be a cost to run a digest cycle for every event.
I have a UI component which has a $watch callback on its width (the reason is not relevant for this post).
The problem is that in some cases:
The width is changed from a non angular context ->
There is no $digest cycle ->
My $watch callback is not called.
Eventhough my application is a full angular application there are still cases in which code is executed in non angular context. For example:
JQuery calles window.setTimeout - so even if my code called JQuery from within angular context the timeout callback is called in non angular context and my $watch callback will not be executed afterwards.
By the way, even angular themselves call window.setTimeout in their AnimatorService...
So my question is:
How can I make sure a $digest cycle is always performed after any code is executed? (even when the code is a 3rd party code...)
I thought about overriding the original window.setTimeout method but:
It feels a bit ugly and dangerous.
I'm afraid it won't cover all use cases.
Adding a plunker.
The plunker sample contains:
An element which can be hidden using JQuery fadeOut method.
A button which executes the fadeOut call for hiding the element.
A text showing the element display status (Shown!!! or Hidden!!!). This text is updated by $watching on the element display property.
A button which does nothing but to initiate some angular code so that a $digest cycle is called.
Flow:
Click the Fade Out button -> the element will be hidden but the status text will remain Shown!!!.
You can wait forever now - or:
Click the Do Nothing button -> suddenly the text will change.
Why?
When clicking the Fade Out button JQuery.fadeOut calls the window.setTimeout method. After that my $watch callback is called but the element is still not hidden.
The element is only hidden after the timeout callback is called - but then there is no $digest cycle (and i have no way that i know of to trigger one).
Only on the next time an angular code will run my $watch function will be called again and the status will be updated.
AngularJS provides a special $apply method on the scope object to allow you to execute code from outside the AngularJS framework.
The $apply function will execute your code in the correct context and apply a $digest cycle afterwards so you don't have to deal with that yourself.
To implement it in your code, you can:
// Get element
var $element = $('#yourElement');
// Get scope
var scope = angular.element($element).scope();
// Execute your code
scope.$apply(function(scope){
// Your logic here
// All watchers in the scope will be triggered
});
(The scenario above can change depending on your actual application).
You can read more about the $apply method of the scope object right here: http://docs.angularjs.org/api/ng.$rootScope.Scope
Hope that helps!
Looking at your plunker, you could add a callback on the call to animate to manually trigger an update to the Angular scope once the animation is complete:
Before:
$scope.fadeOut = function() {
animatedElement.fadeOut('slow');
};
var getDisplay = function() {
return animatedElement.css('display');
};
$scope.$watch(getDisplay, function(display) {
console.log('$watch called with display = `' + display + '`');
$scope.display = display === 'none' ? 'Hidden!!!' : 'Shown!!!';
});
After:
$scope.fadeOut = function() {
animatedElement.fadeOut('slow', function() { $scope.$digest(); });
};
This would cause your watch on getDisplay to be called when the animation is complete, by which time it will return the correct value.