Skip to main content
Ben Nadel at cf.Objective() 2010 (Minneapolis, MN) with: Anne Porosoff
Ben Nadel at cf.Objective() 2010 (Minneapolis, MN) with: Anne Porosoff ( @AnnePorosoff )

Conditionally Canceling Href Navigation Using Directives In AngularJS

By on

Lately, I've been thinking a lot about the separation of responsibilities in AngularJS and about ways to make the View more expressive and explanatory. As a thought experiment, I wanted to take this mindset and revisit my previous post on managing conditional links and route changes. In that previous post, I let the parent component-directive manage links using event delegation. But, what if I could replace that with a different directive that made the view behavior more transparent?

Run this demo in my JavaScript Demos project on GitHub.

I don't think that there is anything wrong with my previous post. In fact, it's an approach that I have used with much success. However, in order to see how and when Href-based navigation is going to be canceled you have 1) know that there is even logic for conditionally routing and 2) find that logic in the linking function of the component directive. So, instead of using event delegation to capture the click-event, I want to try achieving the same thing with an anchor-based directive that can conditionally cancel the click event.

In this demo, I have created a cancelHref directive that is intended to be part of an href or ngHref based anchor tag. The cancelHref directive takes a view-model expression that will be evaluated on the click event. And, if the expression strictly evaluates to True, the default behavior of the click event will be canceled (which, for an anchor tag means canceling the navigation).

I think this makes the view a bit more obvious, especially to someone who is jumping in and maintaining the code for the first time:

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

	<title>
		Conditionally Canceling Href Navigation Using Directives In AngularJS
	</title>

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

	<h1>
		Conditionally Canceling Href Navigation Using Directives In AngularJS
	</h1>

	<p>
		<strong>Current Route</strong>: <span class="route">{{ vm.currentRoute }}</span>
	</p>

	<ul>
		<li ng-repeat="friend in vm.friends track by friend.id">

			<!--
				Notice that, in addition to the HREF attribute interpolation, I also have
				a cancelHref directive. This directive takes a scope expression and will
				cancel the defaultBehavior of the click event object (which will, in turn
				cancel the navigation) if the cancelHref expression evaluates to TRUE.
				This allows the view to be very explicit as to how and why a navigation
				might be canceled while, at the same time, still allowing for native
				browser behavior like CMD+Click to open the link in another browser tab.
			-->
			<a href="#/friends/{{ friend.id }}" cancel-href="! vm.canViewFriend( friend )">
				( {{ friend.id }} ) {{ friend.name }}
			</a>

			<!-- Indicating the currently-selected friend. -->
			<span ng-if="( vm.currentRoute == ( '/friends/' + friend.id ) )">
				*
			</span>

		</li>
	</ul>


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

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


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


		// I control the root of the application.
		angular.module( "Demo" ).controller(
			"AppController",
			function AppController( $scope, $location ) {

				var vm = this;

				// I hold the collection of friends to render.
				vm.friends = buildFriends( "Sarah", "Kim", "Nina", "Joanna" );

				// I keep track of the current route.
				vm.currentRoute = "";

				// I synchronize the view-model to the route as the route changes.
				$scope.$on( "$locationChangeSuccess", handleLocationChange );

				// Expose public methods.
				vm.canViewFriend = canViewFriend;


				// ---
				// PUBLIC METHODS.
				// ---


				// I determine if the given friend can be viewed. This allows the
				// navigation to the friend route to be conditionally canceled by the
				// calling context using the cancelHref directive.
				function canViewFriend( friend ) {

					console.log(
						"%s is %s",
						friend.name,
						( friend.isFrienemy ? "frienemy, oh noes!!" : "cool." )
					);

					return( ! friend.isFrienemy );

				}


				// ---
				// PRIVATE METHODS.
				// ---


				// I build up the collection for friends based on the given arguments.
				function buildFriends() {

					var collection = [];

					for ( var i = 0, length = arguments.length ; i < length ; i++ ) {

						collection.push({
							id: ( i + 1 ),
							name: arguments[ i ],
							isFrienemy: ( i === 2 )
						});

					}

					return( collection );

				}


				// I update the current route mapping when the location changes.
				function handleLocationChange() {

					vm.currentRoute = $location.path();

				}

			}
		);


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


		// I provide a way to conditionally cancel an HREF-based navigation action.
		angular.module( "Demo" ).directive(
			"cancelHref",
			function cancelHrefDirective() {

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


				// I compile the directive. Since this directive is intended to interplay
				// with the HREF-based navigation, it doesn't make sense to link this
				// directive unless there is a sibling HREF on the template element.
				function compile( tElement, tAttributes ) {

					if ( ! ( tAttributes.href || tAttributes.ngHref ) ) {

						throw( new Error( "cancelHref requires href or ngHref sibling." ) );

					}

					return( this.link );

				}


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

					element.on(
						"click",
						function handleClick( event ) {

							// If the expression strictly evaluates to True, we want to
							// prevent the default behavior. On a click-event, initiated
							// by an HREF attribute, this will prevent the navigation
							// from taking place.
							if ( scope.$eval( attributes.cancelHref ) === true ) {

								event.preventDefault();

							}

						}
					);

				}

			}
		);

	</script>

</body>
</html>

As you can see, the View markup clearly indicates both the navigation intent and the potential to cancel the navigation intent. And, when we run this code and try to click on the friend in the list, you will see that the "frienemy" cannot be viewed:

Conditionally canceling href navigation using directives in AngularJS.

Now, if you wanted to, you could just move all of this into an ngClick directive that explicitly and programmtically changes the $location value. However, I feel very strongly that as much navigation as possible should be kept in href attributes. This way, you maintain as much of the native behavior of the browser as possible and lower the chances of creating a negative interaction for the user (such as preventing them from opening a link in a new browser tab).

AngularJS is super powerful and super flexible. And, there's generally a good number of ways to approach any problem. In this demo, my goal was to try and make the View more expressive so that the behavior becomes more obvious.

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

Reader Comments

15,688 Comments

@Fergus,

Unfortunately, I haven't yet played with AngularJS 2.0 yet. As such, I am not sure why the router would behave any differently. If you cancel the default behavior on the click event for the Anchor, then theoretically, the Location should never change. And, if the location never changes, then theoretically, the route would never change either.

When A2 goes full release, I'll be doing a lot more digging!

20 Comments

@Ben,

I got it to work in ng2 - satisfyingly concise, clear and elegant in the end. MUCH more so than ng1, IMHO. (I won't clutter here with my solution, so bump me if anyone's interested. Cynical at first, ng2+typescript is growing in me.)

15,688 Comments

@Fergus,

I'm glad to hear it's growing on you. I'm still feeling a little hesitant. Not on the NG2 part - on the total shift in the JavaScript itself. I guess I just feel that ES5 has been really good. I hope all the ES6 class stuff and TypeScript stuff actually makes things easier.

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