Do any of you know how to nicely handle anchor hash linking in AngularJS?
I have the following markup for a simple FAQ-page
<a href="#faq-1">Question 1</a>
<a href="#faq-2">Question 2</a>
<a href="#faq-3">Question 3</a>
<h3 id="faq-1">Question 1</h3>
<h3 id="faq-2">Question 2</h3>
<h3 id="fa1-3">Question 3</h3>
When clicking on any of the above links AngularJS intercepts and routes me to a completely different page (in my case, a 404-page as there are no routes matching the links.)
My first thought was to create a route matching "/faq/:chapter" and in the corresponding controller check $routeParams.chapter
after a matching element and then use jQuery to scroll down to it.
But then AngularJS shits on me again and just scrolls to the top of the page anyway.
So, anyone here done anything similar in the past and knows a good solution to it?
Edit: Switching to html5Mode should solve my problems but we kinda have to support IE8+ anyway so I fear it's not an accepted solution :/
This question is related to
javascript
angularjs
anchor
hashtag
You could try to use anchorScroll.
So the controller would be:
app.controller('MainCtrl', function($scope, $location, $anchorScroll, $routeParams) {
$scope.scrollTo = function(id) {
$location.hash(id);
$anchorScroll();
}
});
And the view:
<a href="" ng-click="scrollTo('foo')">Scroll to #foo</a>
...and no secret for the anchor id:
<div id="foo">
This is #foo
</div>
See https://code.angularjs.org/1.4.10/docs/api/ngRoute/provider/$routeProvider
[reloadOnSearch=true] - {boolean=} - reload route when only $location.search() or $location.hash() changes.
Setting this to false did the trick without all of the above for me.
Try to set a hash prefix for angular routes $locationProvider.hashPrefix('!')
Full example:
angular.module('app', [])
.config(['$routeProvider', '$locationProvider',
function($routeProvider, $locationProvider){
$routeProvider.when( ... );
$locationProvider.hashPrefix('!');
}
])
On Route change it will scroll to the top of the page.
$scope.$on('$routeChangeSuccess', function () {
window.scrollTo(0, 0);
});
put this code on your controller.
Here is kind of dirty workaround by creating custom directive that will scrolls to specified element (with hardcoded "faq")
app.directive('h3', function($routeParams) {
return {
restrict: 'E',
link: function(scope, element, attrs){
if ('faq'+$routeParams.v == attrs.id) {
setTimeout(function() {
window.scrollTo(0, element[0].offsetTop);
},1);
}
}
};
});
Sometime in angularjs application hash navigation not work and bootstrap jquery javascript libraries make extensive use of this type of navigation, to make it work add target="_self"
to anchor tag.
e.g. <a data-toggle="tab" href="#id_of_div_to_navigate" target="_self">
<a href="##faq-1">Question 1</a>
<a href="##faq-2">Question 2</a>
<a href="##faq-3">Question 3</a>
<h3 id="faq-1">Question 1</h3>
<h3 id="faq-2">Question 2</h3>
<h3 id="faq-3">Question 3</h3>
This is an old post, but I spent a long time researching various solutions so I wanted to share one more simple one. Just adding target="_self"
to the <a>
tag fixed it for me. The link works and takes me to the proper location on the page.
However, Angular still injects some weirdness with the # in the URL so you may run into trouble using the back button for navigation and such after using this method.
If you don't like to use ng-click
here's an alternate solution. It uses a filter
to generate the correct url based on the current state. My example uses ui.router.
The benefit is that the user will see where the link goes on hover.
<a href="{{ 'my-element-id' | anchor }}">My element</a>
The filter:
.filter('anchor', ['$state', function($state) {
return function(id) {
return '/#' + $state.current.url + '#' + id;
};
}])
There is no need to change any routing or anything else just need to use target="_self"
when creating the links
Example:
<a href="#faq-1" target="_self">Question 1</a>
<a href="#faq-2" target="_self">Question 2</a>
<a href="#faq-3" target="_self">Question 3</a>
And use the id
attribute in your html elements like this:
<h3 id="faq-1">Question 1</h3>
<h3 id="faq-2">Question 2</h3>
<h3 id="faq-3">Question 3</h3>
There is no need to use ## as pointed/mentioned in comments ;-)
If you always know the route, you can simply append the anchor like this:
href="#/route#anchorID
where route
is the current angular route and anchorID
matches an <a id="anchorID">
somewhere on the page
Get your scrolling feature easily. It also supports Animated/Smooth scrolling as an additional feature. Details for Angular Scroll library:
Github - https://github.com/oblador/angular-scroll
Bower: bower install --save angular-scroll
npm : npm install --save angular-scroll
Minfied version - only 9kb
Smooth Scrolling (animated scrolling) - yes
Scroll Spy - yes
Documentation - excellent
Demo - http://oblador.github.io/angular-scroll/
Hope this helps.
I am not 100% sure if this works all the time, but in my application this gives me the expected behavior.
Lets say you are on ABOUT page and you have the following route:
yourApp.config(['$routeProvider',
function($routeProvider) {
$routeProvider.
when('/about', {
templateUrl: 'about.html',
controller: 'AboutCtrl'
}).
otherwise({
redirectTo: '/'
});
}
]);
Now, in you HTML
<ul>
<li><a href="#/about#tab1">First Part</a></li>
<li><a href="#/about#tab2">Second Part</a></li>
<li><a href="#/about#tab3">Third Part</a></li>
</ul>
<div id="tab1">1</div>
<div id="tab2">2</div>
<div id="tab3">3</div>
In conclusion
Including the page name before the anchor did the trick for me. Let me know about your thoughts.
Downside
This will re-render the page and then scroll to the anchor.
UPDATE
A better way is to add the following:
<a href="#tab1" onclick="return false;">First Part</a>
I could do this like so:
<li>
<a href="#/#about">About</a>
</li>
This may be a new attribute for ngView, but I've been able to get it anchor hash links to work with angular-route
using the ngView autoscroll
attribute and 'double-hashes'.
(The following code was used with angular-strap)
<!-- use the autoscroll attribute to scroll to hash on $viewContentLoaded -->
<div ng-view="" autoscroll></div>
<!-- A.href link for bs-scrollspy from angular-strap -->
<!-- A.ngHref for autoscroll on current route without a location change -->
<ul class="nav bs-sidenav">
<li data-target="#main-html5"><a href="#main-html5" ng-href="##main-html5">HTML5</a></li>
<li data-target="#main-angular"><a href="#main-angular" ng-href="##main-angular" >Angular</a></li>
<li data-target="#main-karma"><a href="#main-karma" ng-href="##main-karma">Karma</a></li>
</ul>
In my mind @slugslog had it, but I would change one thing. I would use replace instead so you don't have to set it back.
$scope.scrollTo = function(id) {
var old = $location.hash();
$location.hash(id).replace();
$anchorScroll();
};
Docs Search for "Replace method"
<a href="/#/#faq-1">Question 1</a>
<a href="/#/#faq-2">Question 2</a>
<a href="/#/#faq-3">Question 3</a>
None of the solution above works for me, but I just tried this, and it worked,
<a href="#/#faq-1">Question 1</a>
So I realized I need to notify the page to start with the index page and then use the traditional anchor.
I'm using AngularJS 1.3.15 and looks like I don't have to do anything special.
https://code.angularjs.org/1.3.15/docs/api/ng/provider/$anchorScrollProvider
So, the following works for me in my html:
<ul>
<li ng-repeat="page in pages"><a ng-href="#{{'id-'+id}}">{{id}}</a>
</li>
</ul>
<div ng-attr-id="{{'id-'+id}}" </div>
I didn't have to make any changes to my controller or JavaScript at all.
I was trying to make my Angular app scroll to an anchor opon loading and ran into the URL rewriting rules of $routeProvider.
After long experimentation I settled on this:
angular.module("bla",[]).}])_x000D_
.run(function($location, $anchorScroll){_x000D_
$(document).ready(function() {_x000D_
if(location.hash && location.hash.length>=1) {_x000D_
var path = location.hash;_x000D_
var potentialAnchor = path.substring(path.lastIndexOf("/")+1);_x000D_
if ($("#" + potentialAnchor).length > 0) { // make sure this hashtag exists in the doc. _x000D_
location.hash = potentialAnchor;_x000D_
$anchorScroll();_x000D_
}_x000D_
} _x000D_
});
_x000D_
Based on @Stoyan I came up with the following solution:
app.run(function($location, $anchorScroll){
var uri = window.location.href;
if(uri.length >= 4){
var parts = uri.split('#!#');
if(parts.length > 1){
var anchor = parts[parts.length -1];
$location.hash(anchor);
$anchorScroll();
}
}
});
$anchorScroll
works for this, but there's a much better way to use it in more recent versions of Angular.
Now, $anchorScroll
accepts the hash as an optional argument, so you don't have to change $location.hash
at all. (documentation)
This is the best solution because it doesn't affect the route at all. I couldn't get any of the other solutions to work because I'm using ngRoute and the route would reload as soon as I set $location.hash(id)
, before $anchorScroll
could do its magic.
Here is how to use it... first, in the directive or controller:
$scope.scrollTo = function (id) {
$anchorScroll(id);
}
and then in the view:
<a href="" ng-click="scrollTo(id)">Text</a>
Also, if you need to account for a fixed navbar (or other UI), you can set the offset for $anchorScroll like this (in the main module's run function):
.run(function ($anchorScroll) {
//this will make anchorScroll scroll to the div minus 50px
$anchorScroll.yOffset = 50;
});
My solution with ng-route was this simple directive:
app.directive('scrollto',
function ($anchorScroll,$location) {
return {
link: function (scope, element, attrs) {
element.click(function (e) {
e.preventDefault();
$location.hash(attrs["scrollto"]);
$anchorScroll();
});
}
};
})
The html is looking like:
<a href="" scrollTo="yourid">link</a>
This was my solution using a directive which seems more Angular-y because we're dealing with the DOM:
angular.module('app', [])
.directive('scrollTo', function ($location, $anchorScroll) {
return function(scope, element, attrs) {
element.bind('click', function(event) {
event.stopPropagation();
var off = scope.$on('$locationChangeStart', function(ev) {
off();
ev.preventDefault();
});
var location = attrs.scrollTo;
$location.hash(location);
$anchorScroll();
});
};
});
<ul>
<li><a href="" scroll-to="section1">Section 1</a></li>
<li><a href="" scroll-to="section2">Section 2</a></li>
</ul>
<h1 id="section1">Hi, I'm section 1</h1>
<p>
Zombie ipsum reversus ab viral inferno, nam rick grimes malum cerebro. De carne lumbering animata corpora quaeritis.
Summus brains sit??, morbo vel maleficia? De apocalypsi gorger omero undead survivor dictum mauris.
Hi mindless mortuis soulless creaturas, imo evil stalking monstra adventus resi dentevil vultus comedat cerebella viventium.
Nescio brains an Undead zombies. Sicut malus putrid voodoo horror. Nigh tofth eliv ingdead.
</p>
<h1 id="section2">I'm totally section 2</h1>
<p>
Zombie ipsum reversus ab viral inferno, nam rick grimes malum cerebro. De carne lumbering animata corpora quaeritis.
Summus brains sit??, morbo vel maleficia? De apocalypsi gorger omero undead survivor dictum mauris.
Hi mindless mortuis soulless creaturas, imo evil stalking monstra adventus resi dentevil vultus comedat cerebella viventium.
Nescio brains an Undead zombies. Sicut malus putrid voodoo horror. Nigh tofth eliv ingdead.
</p>
I used the $anchorScroll service. To counteract the page-refresh that goes along with the hash changing I went ahead and cancelled the locationChangeStart event. This worked for me because I had a help page hooked up to an ng-switch and the refreshes would esentially break the app.
In my case, I noticed that the routing logic was kicking in if I modified the $location.hash()
. The following trick worked..
$scope.scrollTo = function(id) {
var old = $location.hash();
$location.hash(id);
$anchorScroll();
//reset to old to keep any additional routing logic from kicking in
$location.hash(old);
};
I got around this in the route logic for my app.
function config($routeProvider) {
$routeProvider
.when('/', {
templateUrl: '/partials/search.html',
controller: 'ctrlMain'
})
.otherwise({
// Angular interferes with anchor links, so this function preserves the
// requested hash while still invoking the default route.
redirectTo: function() {
// Strips the leading '#/' from the current hash value.
var hash = '#' + window.location.hash.replace(/^#\//g, '');
window.location.hash = hash;
return '/' + hash;
}
});
}
Source: Stackoverflow.com