74

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
  • 7,523
  • 11
  • 46
  • 72

10 Answers10

122

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.

Andrew Joslin
  • 43,033
  • 21
  • 100
  • 75
  • 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 Jun 25 '13 at 16:34
  • 1
    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? – EnigmaRM Jun 25 '13 at 18:43
  • Anyone managed to use this and get around the iOS 'feature' that results in having to double-tap to trigger a 'click' – Simon H Sep 12 '14 at 06:59
  • 5
    @rnrneverdies it does work on firefox if you change $("body") to $("body, html") – nidal Oct 11 '14 at 14:21
  • 3
    For best cross-browser support, you should use $("html, body").animate() – Cory Apr 28 '15 at 03:50
  • How come I get "Uncaught TypeError: $ is not a function" when trying your solution? – Rafael code Dec 10 '15 at 09:47
  • 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 and grab that in the link function instead – aw04 Dec 28 '15 at 18:56
  • @Cory have you included/loaded JQuery? – nmante Apr 14 '16 at 09:03
  • says, $elm.offest is undefined. how can it be an undefined value. – Wang'l Pakhrin Aug 10 '16 at 23:48
  • @AndrewJoslin Your plunker example does not work at all for me on chrome on mac – Dylanthepiguy Feb 06 '18 at 07:30
33

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 .

Liad Livnat
  • 7,435
  • 16
  • 60
  • 98
23

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");
}
teter
  • 1,468
  • 1
  • 14
  • 19
7

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

ruffin
  • 16,507
  • 9
  • 88
  • 138
Hasteq
  • 926
  • 13
  • 21
4

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

Marc Gibbons
  • 1,190
  • 10
  • 5
4

I used andrew joslin's answer, which works great but triggered an angular route change, which created a jumpy looking scroll for me. If you want to avoid triggering a route change,

myApp.directive('scrollOnClick', function() {
  return {
    restrict: 'A',
    link: function(scope, $elm, attrs) {
      var idToScroll = attrs.href;
      $elm.on('click', function(event) {
        event.preventDefault();
        var $target;
        if (idToScroll) {
          $target = $(idToScroll);
        } else {
          $target = $elm;
        }
        $("body").animate({scrollTop: $target.offset().top}, "slow");
        return false;
      });
    }
  }
});
danyamachine
  • 1,848
  • 1
  • 18
  • 21
3

Another suggestion. One directive with selector.

HTML:

<button type="button" scroll-to="#catalogSection">Scroll To</button>

Angular:

app.directive('scrollTo', function () {
    return {
        restrict: 'A',
        link: function (scope, element, attrs) {
            element.on('click', function () {

                var target = $(attrs.scrollTo);
                if (target.length > 0) {
                    $('html, body').animate({
                        scrollTop: target.offset().top
                    });
                }
            });
        }
    }
});

Also notice $anchorScroll

Undefitied
  • 747
  • 5
  • 14
2

What about angular-scroll, it's actively maintained and there is no dependency to jQuery..

Marwen Trabelsi
  • 4,167
  • 8
  • 39
  • 80
0

very clear answer, using just ANGULARJS, no any JQUERY depends

in your html somewhere on the bottom <back-top>some text</back-top>

in your html somewhere on the top <div id="top"></div>

in your js:

/**
 * @ngdoc directive
 * @name APP.directive:backTop
 <pre>
<back-top></back-top>
 </pre>
 */


angular
.module('APP')
.directive('backTop', ['$location', '$anchorScroll' ,function($location, $anchorScroll) {
  return {
    restrict: 'E',
    replace: true,
    transclude: true,
    template: '<span class=\'btn btn-mute pull-right\'><i class=\'glyphicon glyphicon-chevron-up\'></i><ng-transclude></ng-transclude></span>',
    scope: {
    },
    link: function(scope, element) {
      element.on('click', function(event) {
        $anchorScroll(['top']);
      });
    }
  };
}]);
Anja Ishmukhametova
  • 1,535
  • 16
  • 14
0

Scroll to target div by using ID of the element

Directive(Angular 1)

angular.module("App") // Module Name
    .directive('scrollOnClick', function () {
        return {
            restrict: 'A',
            scope: {
                scrollTo: "@"
            },
            link: function (scope, $elm, attrs) {
                //var idToScroll = attrs.href;
                $elm.on('click', function () {
                    $('html,body').animate({ scrollTop: $(scope.scrollTo).offset().top }, "slow");
                });
            }
        }
    });

HTML Code

<!-- Click to scroll -->
<a scroll-on-click scroll-to="#scheduleDiv">Click here to Scroll to Div With Id ""</a>


<!-- scrollable / target div -->
<div id="scheduleDiv">Test scrolling ... You are able to view me on click of above anchor tag.</div>
Ravindra Vairagi
  • 1,055
  • 15
  • 22