ScrollTo function in AngularJS

152,867

Solution 1

Here is a simple directive that will scroll to an element on click:

myApp.directive('scrollOnClick', function() {
  return {
    restrict: 'A',
    link: function(scope, $elm) {
      $elm.on('click', function() {
        $("body").animate({scrollTop: $elm.offset().top}, "slow");
      });
    }
  }
});

Demo: http://plnkr.co/edit/yz1EHB8ad3C59N6PzdCD?p=preview

For help creating directives, check out the videos at http://egghead.io, starting at #10 "first directive".

edit: To make it scroll to a specific element specified by a href, just check attrs.href.

myApp.directive('scrollOnClick', function() {
  return {
    restrict: 'A',
    link: function(scope, $elm, attrs) {
      var idToScroll = attrs.href;
      $elm.on('click', function() {
        var $target;
        if (idToScroll) {
          $target = $(idToScroll);
        } else {
          $target = $elm;
        }
        $("body").animate({scrollTop: $target.offset().top}, "slow");
      });
    }
  }
});

Then you could use it like this: <div scroll-on-click></div> to scroll to the element clicked. Or <a scroll-on-click href="#element-id"></div> to scroll to element with the id.

Solution 2

This is a better directive in case you would like to use it:

you can scroll to any element in the page:

.directive('scrollToItem', function() {                                                      
    return {                                                                                 
        restrict: 'A',                                                                       
        scope: {                                                                             
            scrollTo: "@"                                                                    
        },                                                                                   
        link: function(scope, $elm,attr) {                                                   

            $elm.on('click', function() {                                                    
                $('html,body').animate({scrollTop: $(scope.scrollTo).offset().top }, "slow");
            });                                                                              
        }                                                                                    
    }})     

Usage (for example click on div 'back-to-top' will scroll to id scroll-top):

<a id="top-scroll" name="top"></a>
<div class="back-to-top" scroll-to-item scroll-to="#top-scroll"> 

It's also supported by chrome,firefox,safari and IE cause of the html,body element .

Solution 3

In order to animate to a specific element inside a scroll container (fixed DIV)

/*
    @param Container(DIV) that needs to be scrolled, ID or Div of the anchor element that should be scrolled to
    Scrolls to a specific element in the div container
*/
this.scrollTo = function(container, anchor) {
    var element = angular.element(anchor);
    angular.element(container).animate({scrollTop: element.offset().top}, "slow");
}

Solution 4

An angular solution using $anchorScroll taken from a now archived blog post by Ben Lesh, which is also reproduced in some detail at this SO answer he contributed (including a rewrite of how to do this within a routing):

app.controller('MainCtrl', function($scope, $location, $anchorScroll) {
  var i = 1;
  
  $scope.items = [{ id: 1, name: 'Item 1' }];
  
  $scope.addItem = function (){
    i++;
    //add the item.
    $scope.items.push({ id: i, name: 'Item ' + i});
    //now scroll to it.
    $location.hash('item' + i);
    $anchorScroll();
  };
});

And here is the plunker, from the blog that provided this solution: http://plnkr.co/edit/xi2r8wP6ZhQpmJrBj1jM?p=preview

Important to note that the template at that plunker includes this, which sets up the id that you're using $anchorScroll to scroll to:

<li ng-repeat="item in items" 
    id="item{{item.id}}"
>{{item.name}</li>

And if you care for a pure javascript solution, here is one:

Invoke runScroll in your code with parent container id and target scroll id:

function runScroll(parentDivId,targetID) {
    var longdiv;
    longdiv = document.querySelector("#" + parentDivId);
    var div3pos = document.getElementById(targetID).offsetTop;
    scrollTo(longdiv, div3pos, 600);
}


function scrollTo(element, to, duration) {
    if (duration < 0) return;
    var difference = to - element.scrollTop;
    var perTick = difference / duration * 10;

    setTimeout(function () {
        element.scrollTop = element.scrollTop + perTick;
        if (element.scrollTop == to) return;
        scrollTo(element, to, duration - 10);
    }, 10);
}

Reference: Cross browser JavaScript (not jQuery...) scroll to top animation

Solution 5

Thanks Andy for the example, this was very helpful. I ended implementing a slightly different strategy since I am developing a single-page scroll and did not want Angular to refresh when using the hashbang URL. I also want to preserve the back/forward action of the browser.

Instead of using the directive and the hash, I am using a $scope.$watch on the $location.search, and obtaining the target from there. This gives a nice clean anchor tag

<a ng-href="#/?scroll=myElement">My element</a>

I chained the watch code to the my module declaration in app.js like so:

.run(function($location, $rootScope) {
   $rootScope.$watch(function() { return $location.search() }, function(search) { 
     var scrollPos = 0;
     if (search.hasOwnProperty('scroll')) {
       var $target = $('#' + search.scroll);
       scrollPos = $target.offset().top;
     }   
     $("body,html").animate({scrollTop: scrollPos}, "slow");
   });
})

The caveat with the code above is that if you access by URL directly from a different route, the DOM may not be loaded in time for jQuery's $target.offset() call. The solution is to nest this code within a $viewContentLoaded watcher. The final code looks something like this:

.run(function($location, $rootScope) {
  $rootScope.$on('$viewContentLoaded', function() {
     $rootScope.$watch(function() { return $location.search() }, function(search) {
       var scrollPos = 0 
       if (search.hasOwnProperty('scroll')) {
         var $target = $('#' + search.scroll);
         var scrollPos = $target.offset().top;
       }
       $("body,html").animate({scrollTop: scrollPos}, "slow");                                                                                                                                                                    
     });  
   });    
 })

Tested with Chrome and FF

Share:
152,867
EnigmaRM
Author by

EnigmaRM

I've focused my learning to front-end technologies. AngularJS, D3.js, jQuery, and AJAX.

Updated on July 05, 2022

Comments

  • EnigmaRM
    EnigmaRM almost 2 years

    I'm trying to get a quick nav to work correctly. It's floating on the side. When they click on a link, it takes them to that ID on the page. I'm following this guide from Treehouse. This is what I have for the scrolling:

    $("#quickNav a").click(function(){
        var quickNavId = $(this).attr("href");
        $("html, body").animate({scrollTop: $(location).offset().top}, "slow");
        return false;
    });
    

    I initially placed it before the </body>. But I seem to be running into a race condition where that was firing before the quickNav compiled (it has a ng-hide placed on it, not sure if that's causing it - but it is within the DOM).

    If I run that block of code in the console, then the scrolling works as expected.

    I figured it'd be more effective to move this into the controller - or more likely within a directive. But I'm not having luck accomplishing that. How can I get this block of code to work with AngularJS?

  • EnigmaRM
    EnigmaRM almost 11 years
    Thanks for the help with a basic directive. I've made a couple very basic ones already. I'm not exactly sure how I would access the href within the quicknav (using a directive) to have it do the anchor linking.
  • EnigmaRM
    EnigmaRM almost 11 years
    I ended up removing several lines of code from your edit (mostly just the if block.) That would be used to scroll to an element clicked on (like you demonstrated in your plunker) correct? Just so it would be more modular?
  • Simon H
    Simon H over 9 years
    Anyone managed to use this and get around the iOS 'feature' that results in having to double-tap to trigger a 'click'
  • nidal
    nidal over 9 years
    @rnrneverdies it does work on firefox if you change $("body") to $("body, html")
  • Cory
    Cory about 9 years
    For best cross-browser support, you should use $("html, body").animate()
  • Hasteq
    Hasteq almost 9 years
    Thanks Ionic. Updated my answer
  • Rafael code
    Rafael code over 8 years
    How come I get "Uncaught TypeError: $ is not a function" when trying your solution?
  • aw04
    aw04 over 8 years
    I'm not sure using href to pass the id is the best solution... I found it to be a bit jumpy as the browser tries to process it in a 'normal' way on click as well. The better solution in my opinion is to bind something to the scope, ie. { scope: elementId: '@' }, then <a href scroll-on-click element-id="#element-id"></a> and grab that in the link function instead
  • nmante
    nmante about 8 years
    @Cory have you included/loaded JQuery?
  • Wang'l Pakhrin
    Wang'l Pakhrin over 7 years
    says, $elm.offest is undefined. how can it be an undefined value.
  • Undefitied
    Undefitied over 7 years
    Why do I need two directives instead one scroll-to-item=".selector"?
  • Dylanthepiguy
    Dylanthepiguy about 6 years
    @AndrewJoslin Your plunker example does not work at all for me on chrome on mac