Created
July 18, 2015 17:39
-
-
Save bennadel/829519e28d33ff258ed8 to your computer and use it in GitHub Desktop.
Using Anchor Tags And URL-Fragment Links In AngularJS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> | |
Compiling The <A> 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/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 compile the <A> 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. | |
angular.module( "Demo" ).directive( | |
"a", | |
function urlFragmentDirective( $location, $anchorScroll ) { | |
// Return the directive configuration object. | |
return({ | |
compile: compile, | |
restrict: "E" | |
}); | |
// I compile the anchor tag, returning a linking function if necessary. | |
function compile( tElement, tAttributes ) { | |
// If the anchor tag has a target attribute, skip this instance of | |
// the tag since Angular will already allow the natural link behavior | |
// (of a targeted link) to take place. | |
if ( tAttributes.target ) { | |
return; | |
} | |
var href = ( tAttributes.href || tAttributes.ngHref || "" ); | |
// If the anchor tag doesn't have an HREF, skip this instance - we | |
// know that we won't have anything to link to upon click. | |
// -- | |
// NOTE: This assumes that the HREF for URl-fragment style anchor | |
// will be present in the original HTML and won't be injected during | |
// the compilation phase of another directive. | |
if ( ! href ) { | |
return; | |
} | |
// If the HREF value doesn't start with a URL-fragment hash, skip it. | |
// -- | |
// NOTE: This assumes that URL-fragment marker (ie, the "#") won't be | |
// injected based on attribute interpolation. Honestly, I think this | |
// is a pretty fair assumption based on the nature of URL-fragments | |
// style links. | |
if ( href.charAt( 0 ) !== "#" ) { | |
return; | |
} | |
// If the anchor tag appears to represent a "Route", ignore it - let | |
// the natural routing behavior take effect. | |
if ( href.charAt( 1 ) === "/" ) { | |
return; | |
} | |
return( link ); | |
} | |
// I bind the JavaScript events to the view-model. | |
function link( scope, element, attributes ) { | |
element.on( | |
"click", | |
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; | |
} | |
var href = element.attr( "href" ); | |
// If attribute interpolation caused this HREF to become a | |
// route, then ignore the click event and let the natural | |
// routing behavior take effect. | |
if ( href.indexOf( "#/" ) === 0 ) { | |
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> | |
Using An ngAnchor Directive | |
</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 using ngAnchor instead of HREF. --> | |
<a ng-anchor="#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/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 watch the ngAnchor attribute to automatically configure the HREF value to | |
// use natural URL-fragment behavior in AngularJS. | |
// -- | |
// NOTE: We don't actually need the $anchorScroll() service; but, we need to | |
// inject it here in order to guarantee that it is instantiated and starts | |
// watching the $location for changes. | |
angular.module( "Demo" ).directive( | |
"ngAnchor", | |
function anchorDirective( $location, $anchorScroll ) { | |
// Return the directive configuration object. | |
return({ | |
link: link, | |
restrict: "A" | |
}); | |
// I bind the JavaScript events to the view-model. | |
function link( scope, element, attributes ) { | |
// Whenever the attribute changes, we have to update our HREF state | |
// to incorporate the new ngAnchor value as the embedded fragment. | |
attributes.$observe( "ngAnchor", configureHref ); | |
// Whenever the the location changes, we want to update the HREF value | |
// of this link to incorporate the current URL plus the URL-fragment | |
// that we are watching in the ngAnchor attribute. | |
scope.$on( "$locationChangeSuccess", configureHref ); | |
// I update the HREF attribute to incorporate both the current top- | |
// level fragment plus our in-page URL-fragment intent. | |
function configureHref() { | |
var fragment = ( attributes.ngAnchor || "" ); | |
// Strip off the leading # to make the string concatenation | |
// handle variable-state inputs (ie, ones that may or may not | |
// include the leading pound sign). | |
if ( fragment.charAt( 0 ) === "#" ) { | |
fragment = fragment.slice( 1 ); | |
} | |
// Since the anchor is really the fragment INSIDE the fragment, | |
// we have to build two levels of fragment. | |
var routeValue = ( "#" + $location.url().split( "#" ).shift() ); | |
var fragmentValue = ( "#" + fragment ); | |
attributes.$set( "href", ( routeValue + fragmentValue ) ); | |
} | |
} | |
} | |
); | |
</script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<a href="#chapter-one" target="_self">Jump to Chapter One</a> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment