Tuesday, February 5, 2013

Angular JS - Scrolling To An Element By Id

So a question on StackOverflow recently was asking about how to scroll to an element by an anchor id, and it garnered some really hacky answers. They worked, but it seemed weird that Angular would force you to jump through hoops to manage scrolling. I thought the question was intriguing because it dealt with a potentially common scenario with a client-side routed application: How do I scroll to an element on the page?



Angular Already Has It

(I'm hoping for $kitchenSinkProvider in 2.x!)

Turns out that Angular already has a mechanism for this called $anchorScroll(). Unfortunately, it's poorly documented, and I had to actually view the source code on GitHub to figure out how to use it. So here's my attempt to remedy this: A quick run down of how to use $anchorScroll to scroll to the proper element after a routed request:

$anchorScroll works off of the hash value set in $location.hash(), so to use it dynamically, you'll need to first set $location.hash to something, then you'll need to call $anchorScroll().


Scrolling To An Element By ID With Routing


So presuming you've set up routing for your application module already in your .config function, I'm going to set up the scrolling mechanism in the application module's run function like so:

app.run(function($rootScope, $location, $anchorScroll, $routeParams) {
  $rootScope.$on('$routeChangeSuccess', function(newRoute, oldRoute) {
    $location.hash($routeParams.scrollTo);
    $anchorScroll();  
  });
});

Pretty simple, right? What I did here is I subscribed to the $routeChangeSuccess event broadcast by the $routeProvider. Inside of that, I set the $location.hash to the value passed into $routeParams at the key scrollTo.

But where does that scrollTo value come from?

<a href="#/test/123/?scrollTo=foo">Test id=123, scroll to #foo</a>

The above link, assuming I have a route like "/test/:id" will route and then scroll to foo. It seems to be a little known thing about Angular that you can pass as many parameters as you like into $routeParams via a faux-querystring.

Anyhow, here is a demonstration on Plunker of the above technique.



Scrolling To An Element By ID Without Routing


Now something to note about $anchorScroll is that you don't have to use it just with routing, if that weren't obvious. You can inject it into any controller or directive and call it as you see fit. You just need to make sure you're setting the hash on Angular's $location.

For example, you could add an item to a list, and then scroll to it after it were added:


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 demo of that on Plunker

Now, truth be told, it's a bit of fuzzy ground as to whether or not you should be using $anchorScroll in a controller like that. Since it's sort of "DOM manipulation" maybe it belongs in a directive... Then again, it never directly references the DOM does it? So it's most likely okay. It's down to a judgement call, and I always differ to whatever is easiest to maintain in those cases.


I Hope This Helps Someone


It's certainly not a well-documented feature. And in the sake of full (probably obvious) disclosure: I did of course contribute my answer on StackOverflow, I don't know or even care whether or not it's accepted, it's likely a better place than my blog for the answer to be so more people find it. I just want to make sure I got what I learned down somewhere I might think to look later, or that might help someone else.

If you found this and it helped you, I'd encourage you to head over the StackOverflow and answer just one question and help someone else. Pick a question you might not even know the answer to if you want to learn something new. The important thing is that we keep technology pushing forward by contributing, gaining knowledge, and innovating.

29 comments:

  1. I referenced your blog post to answer a mailing list question. Thanks for the info!

    https://groups.google.com/forum/?fromgroups=#!topic/angular/K5VCpDc5JvM

    ReplyDelete
  2. Awesome! I'm glad someone found this useful!

    ReplyDelete
  3. Thank you so much.
    I wish I had found your post earlier, it would have saved 2 hours of my time!
    By the way, it's a shame that angular doesn't provide a better documentation for such a widely used feature (anchor linking).

    It's worth mentioning that $anchorScroll behave weirdly when using ng-include in your view as well. That was my case an I just added autoscroll as in <{div} ng-include="'template.html'" autoscroll> to make it work as expected. Another poorly documented feature of angular !

    Thx again !

    ReplyDelete
  4. I referenced this blog post from the AngularJS documentation here: http://docs.angularjs.org/api/ng.$anchorScroll

    ReplyDelete
  5. Thank you, this helped me and saved me some precious time! Before reading this post I didn't understand the documentation but now it (kind of) makes sense...

    ReplyDelete
  6. Hi Ben,
    Thanks for this blog post. Perhaps you could put together a Pull Request to add it to the documentation? Here is the relevant page:https://github.com/angular/angular.js/edit/master/src/ng/anchorScroll.js

    ReplyDelete
    Replies
    1. I've added a basic example to the documentation. I was a little unsure if you meant I should link my blog in your documentation, so I felt uncomfortable adding a link. If you look at the pull request and would like me to add anything else, let me know.

      https://github.com/angular/angular.js/pull/2932

      Delete
  7. Thanks for the post, any extra docs are greatly welcome for angular now :)

    Is there an easy way to scroll to a particular position using pixels?

    ReplyDelete
    Replies
    1. You should be able to just inject $window and then call $window.scrollTo(x, y);

      Delete
  8. Thanks this works but it's clearing my form values. Should that be expected and is there a work around? I am not using HTML 5 mode.

    ReplyDelete
    Replies
    1. It depends on where your form is in your app. I'd really need to see what your'e trying to do. If you need to simply scroll to an element by id, then routing would be unnecessary and may be the cause of your form clearing out (when it loads the new route)... but that's speculatory on my part, because I don't know what you've got or what you're trying to do with it.

      Delete
  9. Trying to use this in a controller in much the same way and running into problems. Any ideas?

    http://stackoverflow.com/questions/17711232/scroll-to-in-angular

    ReplyDelete
    Replies
    1. Looks like an issue with the scroll adding another '#' to my url. I.E. my admin page url changes from /#/admin to /#/admin/#admin-form when I attempt the scroll. For whatever reason that change in url is causing my route provider to reload the route (therefore page). Once its reloaded and the url is /#/admin#admin-form the scroll to works as the route is not changing and reloadeding my page.

      Delete
    2. I've created a plunk to show off this problem. Notice how you have to click the button twice for the message to appear? Let me know if you figure anything out.

      http://plnkr.co/edit/oU3REYC2upvlPMc5ipBp?p=preview

      Delete
  10. Thanks by this post...was useful to me! :)

    ReplyDelete
  11. Great post, thanks a lot. Maybe you could explain a bit why angularjs needs to provide a mechanism like anchorScroll. Isn't the browser supposed to be able to take of such things when provided with a proper url?

    ReplyDelete
    Replies
    1. I guess I probably should have covered this. But angular has this service so it can schedule "scrollTos" after a digest is completed. Maybe you have multiple components affecting $location.hash, but you're triggering the scrollTo at a certain point. You'll have to wait for your digest to complete to make sure you've accounted for all of your $watches and whatever else. ... if that makes sense.

      Delete
  12. Excellent, thanks very much for the post that basically explains everything I was looking for. Both scroll-to scenarios are required now and then, and both are covered here.

    ReplyDelete
  13. Hi Ben

    This doesn't seem to work as expected if you have Angular-generated content on your page (in which case anchorScroll doesn't scroll far enough). Your first Plunker example uses static content.

    Any ideas?

    Andrew

    ReplyDelete
    Replies
    1. This shouldn't at all be the case. I'd have to see your code to see what's happening. Here is a very primitive example of $anchorScroll working with "Angular generated content"

      So my best guess is that perhaps you're kicking off the scroll too early? Or maybe forgetting to first set the hash?

      You should really post a question on StackOverflow.

      Delete
  14. Hey Benjamin,

    I've been searching for a pure angular version of a "smooth scroll" animation. All of the solutions that I come across (like this stack overflow answer and this google groups post) use jquery.

    I would like to avoid jquery if possible. Any suggestions?

    ReplyDelete
    Replies
    1. Well, if you're going to avoid JQuery in this case I'm afraid you're left with the option of rolling your own animation loop to update scrollTop (or pageYOffset) and notifying you of completion. This is because there aren't a lot of good ways of doing what you're asking with scroll, since it's not really controlled by CSS3 and therefor transitions and keyframes are out of the question. JQuery just so happens to be really good at animating "physical properties" of the DOM that CSS doesn't cover (like scrollTop). Particularly with easing.

      There are probably simple libraries out there that are targeted at doing just this without JQuery, but I'm afraid I don't know what those libraries are off the top of my head.

      Delete
  15. Readers of this post might be interested in this currently unresolved Angular bug:
    https://github.com/angular/angular.js/issues/4608

    ReplyDelete
    Replies
    1. I responded to that issue #4608. $location makes an assumption that you're using ng's default routing (not HTML5) and basically "owns" everything after "#" at that point.

      Delete
    2. I go into more detail on Github.

      Delete
  16. Hi Ben,

    I really enjoyed your post, I had to work with something similar, and I took you example to incorporate the anchor links into the url, something like this:

    http://run.plnkr.co/HVHoBybt0AQ8OUH3/#/faq

    /faq
    /faq/why-x
    /faq/who-z
    /faq/when-y

    They all point to the same route, by declaring the route with the "q" param as optional.

    Then I use that param to fetch the element id and scroll to it

    ReplyDelete

This form allows some basic HTML. It will only create links if you wrap the URL in an anchor tag (Sorry, it's the Blogger default)