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.

5 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