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?

10 Answers 10

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.

Sign up to request clarification or add additional context in comments.

10 Comments

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.
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?
Anyone managed to use this and get around the iOS 'feature' that results in having to double-tap to trigger a 'click'
@rnrneverdies it does work on firefox if you change $("body") to $("body, html")
For best cross-browser support, you should use $("html, body").animate()
|
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 .

1 Comment

Why do I need two directives instead one scroll-to-item=".selector"?
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");
}

Comments

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

Comments

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

Comments

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;
      });
    }
  }
});

Comments

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

Comments

2

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

Comments

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']);
      });
    }
  };
}]);

Comments

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>

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.