Skip to main content
Ben Nadel at FirstMark Tech Summit (New York, NY) with: Mike Boufford
Ben Nadel at FirstMark Tech Summit (New York, NY) with: Mike Boufford ( @mboufford )

Defining Post-Route-Change Scroll Behavior Using $location in AngularJS

By on

Over the weekend, I described my evolving thoughts on the use of $location in a route-driven AngularJS application. And, while I think that the post was accurate from a technical standpoint, I believe it failed to capture the actual workflow of $route / $location consumption in a complex user interface (UI). As such, I wanted to do a quick follow-up that might better articulate how I think the route and the overall $location can work together to render a View.

Run this demo in my JavaScript Demos project on GitHub.

In the previous post, I talked about how I use the route to determine which view is rendered (ie, the location of the user within the landscape of the application); and, how I use the $location to [help] define how the selected view should behave once it's rendered. To demonstrate this, I thought it would be useful to actually have a changing view. So, for this particular demo, I have two views that are mutually exclusive. Using links, the user can select one at a time - the route will determine which view is rendered and the location query string will determine the scroll-offset of the view once it is rendered.

<!doctype html>
<html ng-app="Demo">
<head>
	<meta charset="utf-8" />

	<title>
		Defining Post-Route-Change Scroll Behavior Using $location in AngularJS
	</title>

	<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body ng-controller="AppController">

	<h1>
		Defining Post-Route-Change Scroll Behavior Using $location in AngularJS
	</h1>

	<!--
		BEGIN: Nav.

		In these navigational links, the path is mapped on to the view that should
		be rendered (ie, "a" vs. "b") and the query string is then used to determine
		state transformation after the view is rendered.
	-->
	<div class="nav">

		<div
			class="section"
			ng-class="{ active: ( section == 'a' ) }">

			<strong>Section A:</strong>

			<a href="#/a">Normal</a>
			<a href="#/a?scroll=same">Same</a>
			<a href="#/a?scroll=500">500px</a>
			<a href="#/a?scroll=1000">1,000px</a>

		</div>

		<div
			class="section"
			ng-class="{ active: ( section == 'b' ) }">

			<strong>Section B:</strong>

			<a href="#/b">Normal</a>
			<a href="#/b?scroll=same">Same</a>
			<a href="#/b?scroll=500">500px</a>
			<a href="#/b?scroll=1000">1,000px</a>

		</div>

	</div>
	<!-- END: Nav. -->


	<!-- BEGIN: Views. -->
	<div
		class="views"
		ng-switch="section">

		<div
			ng-switch-when="a"
			bn-view
			class="view view-a">

			<h2>
				Section A
			</h2>

			Section a - 1.<br />Section a - 2.<br />Section a - 3.<br />Section a - 4.<br />
			Section a - 5.<br />Section a - 6.<br />Section a - 7.<br />Section a - 8.<br />
			Section a - 9.<br />Section a - 10.<br />Section a - 11.<br />Section a - 12.<br />
			Section a - 13.<br />Section a - 14.<br />Section a - 15.<br />Section a - 16.<br />
			Section a - 17.<br />Section a - 18.<br />Section a - 19.<br />Section a - 20.<br />

		</div>

		<div
			ng-switch-when="b"
			bn-view
			class="view view-b">

			<h2>
				Section B
			</h2>

			Section b - 1.<br />Section b - 2.<br />Section b - 3.<br />Section b - 4.<br />
			Section b - 5.<br />Section b - 6.<br />Section b - 7.<br />Section b - 8.<br />
			Section b - 9.<br />Section b - 10.<br />Section b - 11.<br />Section b - 12.<br />
			Section b - 13.<br />Section b - 14.<br />Section b - 15.<br />Section b - 16.<br />
			Section b - 17.<br />Section b - 18.<br />Section b - 19.<br />Section b - 20.<br />

		</div>

	</div>
	<!-- END: Views. -->


	<!-- Load scripts. -->
	<script type="text/javascript" src="../../vendor/angularjs/angular-1.3.8.min.js"></script>
	<script type="text/javascript" src="../../vendor/angularjs/angular-route-1.3.8.min.js"></script>
	<script type="text/javascript">

		// Create an application module for our demo.
		var app = angular.module( "Demo", [ "ngRoute" ] );


		// -------------------------------------------------- //
		// -------------------------------------------------- //


		// Configure the route provider.
		app.config(
			function( $routeProvider ) {

				// Set up the route so that we can easily map the URL component on to
				// the "section" route parameter.
				$routeProvider.when( "/:section", {} );

				// Default to section selection in case one is not being provided.
				$routeProvider.otherwise( "/a" );

			}
		);


		// -------------------------------------------------- //
		// -------------------------------------------------- //


		// I control the root of the application.
		// --
		// NOTE: While we are not referencing the $route service, we have to inject it or
		// the "$routeChangeSuccess" event will never be fired (as AngularJS will only
		// create the $route service on-demand).
		app.controller(
			"AppController",
			function( $scope, $route, $routeParams ) {

				// I determine which section is rendered.
				$scope.section = null;

				// I update the rendered section to reflect the route configuration.
				$scope.$on(
					"$routeChangeSuccess",
					function handleRouteChangeEvent( event ) {

						// The $routeChangeSuccess event will fire even if the query
						// string is the only thing that changes. As such, just ignore
						// any events that don't result in an actual route-change.
						if ( $scope.section === $routeParams.section ) {

							return;

						}

						$scope.section = $routeParams.section;

						console.log( "Route Changed:", $scope.section );

					}
				);

			}
		);


		// -------------------------------------------------- //
		// -------------------------------------------------- //


		// I help transition the state of the currently rendered view.
		app.directive(
			"bnView",
			function( $location ) {

				// Return the directive configuration.
				return({
					link: link,
					restrict: "A"
				});


				// I bind the JavaScript events to the local scope.
				function link( scope, element, attributes ) {

					// The current element is the "content" which is being scrolled
					// within the context of the parent. As such, we need to affect the
					// scrolling on the parent element.
					// --
					// NOTE: It would probably be cleaner to put this directive directly
					// on the parent element; but this approach keeps the demo simple.
					var parentNode = element.parent()[ 0 ];

					// Grab the "scroll" property out of the query string. This value
					// determines how we scroll the viewport. If no value is present,
					// we'll assume the viewport should scroll to the top.
					var scroll = ( $location.search().scroll || "none" );

					// Since the query string is only taken into account during the
					// linking phase, just remove it from the URL. This way, the user
					// won't be tempted to play with it.
					$location.search( "scroll", null );

					// If none, then scroll to the top of the viewport.
					if ( scroll === "none" ) {

						return( parentNode.scrollTop = 0 );

					}

					// If same, then keep the viewport scrolled to the same position.
					// Here, we can just return because this is the natural behavior of
					// the viewport (assuming some other directive didn't force a repaint
					// while the content was will loading).
					if ( scroll === "same" ) {

						return;

					}

					// If we've made it this far, assume the provided scroll value is an
					// integer - scroll the viewport appropriately.
					parentNode.scrollTop = parseInt( scroll, 10 );

				}

			}
		);

	</script>

</body>
</html>

As you can see, while the $route and $routeParams determine which view will be included, the scroll behavior is determined by the $location search (query string) and is consumed by the linking function on the view directive. Since the scroll behavior is only observed during the linking phase, I'm removing the "scroll" query string value after it is consumed (by setting it to null).

Hopefully this demo does a bit more to clarify my thoughts on a more robust consumption of the $route and $location services in a complex AngularJS application; rather than using one or the other, both the $route and the $location services can be used to drive different aspects of the application.

Want to use code from this post? Check out the license.

Reader Comments

2 Comments

I was wondering - shouldn't you have implemented this cleanly with ui-views and states and routing in Angular? Why write your own views?

On top of that your example does not work. Try hitting A500, then B1000 and it wont work (in FF 41.0.2)

2 Comments

@Oskar,

I was unclear as to the bugs in your demo, but if you click around a bit, go from a to b, use normal, then hit a500 and then try a1000, nothing happens etc.

15,674 Comments

@Oskar,

I don't know too much about the ui-router. The core AngularJS router has always been sufficient for my needs. I am not sure how much cleaner it would have made this demo. That said, the part that's actually involved in the routing is the $routeChangeSuccess handler, so I am not sure how much of a win it would have been.

As far a bug, the response to the $location scroll request is in the link() function of the view. So, if you won't change the view and cause a re-linking of the directive, you won't get a change in the vertical offset. But, if you go from A to B and back, the scroll should always work (at least in my Firefox and Chrome).

15,674 Comments

@Oskar,

If you wanted to be able to jump from one offset to another, in the same view, you could always add an event listener to the $locationChangeSuccess in the link() function. Then, it could change the offset any time the relevant URL search param changed.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel