8

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?

macene
  • 85
  • 1
  • 1
  • 3
  • This seems to be a special case of this related question: http://stackoverflow.com/q/12304291/302793 – lex82 Nov 10 '13 at 12:57

2 Answers2

16

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.

lex82
  • 11,173
  • 2
  • 44
  • 69
  • I thought there was a more elegant way to do that, but I guess it's ok to use the timeout if that doesn't have problems depending on the browser. Thanks! – macene Nov 10 '13 at 14:50
  • 1
    Maybe the behaviour of `$evalAsync` has changed in newer angular versions. – lex82 Jun 22 '16 at 16:24
  • The problem is that setTimeout doesn't work for all devices, because an mobile phone, for example, has less memory, thus executes the DOM operations slowly. So, it works on a desktop, but on a mobile - it doesn't. If I increase the timeout, it works on both devices, but on the desktop looks awful, because it scrolls down the page (because of showing and hiding different elements) and then goes up to the proper position. Any ideas how to tackle this? – Yulian Jul 05 '16 at 13:09
  • You can use the short timeout and in the timeout handler check whether the elements are visible (access the DOM directly). If not, you set a new timeout with a longer delay. However, if you start to access the DOM directly, you can as well do it right away and not use angular in the first place to toggle visibility of the affected elements. – lex82 Jul 06 '16 at 08:34
  • better to inject the $timeout service and use this.$timeout(function() { ... so you're able to unit test it properly – Nick B Jul 15 '16 at 12:39
0

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();
}
zx485
  • 28,498
  • 28
  • 50
  • 59