<!doctype html> <html ng-app="Demo"> <head> <meta charset="utf-8" /> <title> Using Anchor Tags And URL-Fragment Links In AngularJS </title> </head> <body ng-controller="AppController as vm"> <h1> Using Anchor Tags And URL-Fragment Links In AngularJS </h1> <h2> Linking The <Body> Element </h2> <p> <!-- Normal "route" links. --> <strong>Pages</strong>: <a href="#/section-a">Section A</a> | <a href="#/section-b">Section B</a> | <a href="#/section-c">Section C</a> | <a href="#/section-d">Section D</a> </p> <p> <strong>Current Url</strong>: {{ vm.currentUrl }} </p> <p> <!-- A "fragment" anchor link. --> <a href="#footer">Jump to footer</a>. </p> <p style="height: 2000px ;"> <!-- To force scrolling. -->. </p> <p id="footer"> This is a footer! </p> <!-- Load scripts. --> <script type="text/javascript" src="../../vendor/jquery/jquery-2.1.0.min.js"></script> <script type="text/javascript" src="../../vendor/angularjs/angular-1.4.2.min.js"></script> <script type="text/javascript" src="../../vendor/angularjs/angular-route-1.4.2.min.js"></script> <script type="text/javascript"> // Create an application module for our demo. angular.module( "Demo", [ "ngRoute" ] ); // --------------------------------------------------------------------------- // // --------------------------------------------------------------------------- // // I configure the application routes to make sure that the user cannot go out // side of the supported route definitions. angular.module( "Demo" ).config( function configureRoutes( $routeProvider ) { $routeProvider .when( "/section-a", {} ) .when( "/section-b", {} ) .when( "/section-c", {} ) .when( "/section-d", {} ) .otherwise({ redirectTo: "/section-a" }) ; } ); // --------------------------------------------------------------------------- // // --------------------------------------------------------------------------- // // I control the root of the application. angular.module( "Demo" ).controller( "AppController", function AppController( $scope, $location, $route ) { var vm = this; vm.currentUrl = ""; // When the location changes, capture the state of the full URL. $scope.$on( "$locationChangeSuccess", function locationChanged() { vm.currentUrl = $location.url(); } ); } ); // --------------------------------------------------------------------------- // // --------------------------------------------------------------------------- // // I setup an interceptor on <BODY> tag, allowing normal URL-fragment anchor // links to work "as intended" within the context of an AngularJS application // that uses the $location service, which alters the behavior of link-clicks. // -- // CAUTION: Since the event-delegation logic is a bit more complicated in this // approach, I am including the jQuery logic so I can set up event-delegation on // the BODY tag an easily search for the $rootElement. angular.module( "Demo" ).directive( "body", function urlFragmentDirective( $rootElement, $location, $anchorScroll, $log ) { // Return the directive configuration object. return({ link: link, restrict: "E" }); // I bind the JavaScript events to the view-model. function link( scope, element, attribute ) { // For this approach to work, we need to ensure that the BODY tag is // a descendant of the $rootElement. This is a critical point because // AngularJS uses event-delegation on the $rootElement to rewire <a> // link click behaviors. As such, we can only [be guaranteed to] // intercept the click-event if the BODY tag can see it first, before // it bubbles up to the $rootElement. if ( element.is( $rootElement ) || ! element.closest( $rootElement ).length ) { return( $log.warn( "URL-fragment interceptor cannot be configured on the Body as it is not a descendant of the $rootElement." ) ); } // At this point, we know that we are in a position to intercept // link-click events. As such, let's set up event-delegation to listen // for <a> clicks. element.on( "click", "a", function handleClickEvent( event ) { // If this was a "special" click, ignore it (the $location // service will also ignore it). if ( event.ctrlKey || event.metaKey || event.shiftKey || ( event.which == 2 ) || ( event.button == 2 ) ) { return; } // Since we are using jQuery's event-delegation, we know that // THIS refers to the <A> tag that triggered the event. var target = angular.element( this ); // If the anchor tag has a target attribute, ignore the event // since Angular will already allow the natural link behavior // (of a targeted link) to take place. if ( target.attr( "target" ) ) { return; } var href = ( target.attr( "href" ) || "" ); // If the relative HREF doesn't start with a URL-fragment // indicator, ignore this event. if ( href.charAt( 0 ) !== "#" ) { return; } // If the relative HREF appears to be a route, ignore this // event; let the natural routing behavior take place. if ( href.charAt( 1 ) === "/" ) { return; } // At this point, we know we want to intercept the link // behavior so that AngularJS doesn't try to manage it for us // (which is where the problem of URL-fragment links is coming // from). As such, cancel the default behavior. event.preventDefault(); var fragment = href.slice( 1 ); // If the fragment is already part of the URL, then we have // to explicitly tell Angular to perform the scroll to the // target anchor. Since this click won't actually change the // location state, the $anchorScroll won't execute. if ( fragment === $location.hash() ) { return( $anchorScroll() ); } // Now that we know we need to manage the state of the URL, // let's pipe the URL-fragment into the $location() where it // becomes the fragment ON THE FRAGMENT that represents the // current route. When doing this, the $anchorScroll() service // will automatically pick up the change and auto-scroll the // page to the appropriate hash. $location.hash( fragment ); // Since the $location is part of the view-model, we have to // tell AngularJS that the state has changed. scope.$apply(); } ); } } ); </script> </body> </html>