8

I am not building a single-page application, but rather a "traditional" site that uses AngularJS in places. I've hit the following problem (using 1.3.0-beta.6):

Standard, working anchor links:

<a href="#foo">Link text</a>
... [page content]
<a id="foo"></a>
<h1>Headline</h1>
[more content]

That works fine. Now I introduce a template partial somewhere:

<script type="text/ng-template" id="test-include.html">
  <p>This text is in a separate partial and inlcuded via ng-include.</p>
</script>

which is invoked via:

<div ng-include="'test-include.html'"></div>

The partial is included properly, but the anchor link no longer works. Clicking on "Link text" now changes the displayed URL to /#/foo rather than /#foo and the page position does not change.

My understanding is that using ng-include implicitly tells Angular that I want to use the routes system and overrides the browser's native anchor link behavior. I've seen recommendations to work around this by changing my html anchor links to #/#foo, but I can't do that for other reasons.

I don't intend to use the routes system - I just want to use ng-include without it messing with browser behavior. Is this possible?

shacker
  • 14,712
  • 8
  • 89
  • 89

5 Answers5

6

The reason is that angular overrides the behavior of standard HTML tags which include <a> also. I'm not sure when this change happened because angular v1.0.1 works fine with this.

You should replace the href attribute with ngClick as:

<a ng-click="scroll()">Link text</a>

And in a controller so:

function MyCtrl($scope, $location, $anchorScroll) {
  $scope.scroll = function() {
    $location.hash('foo');
    $anchorScroll();
  };
};

Demo: http://jsfiddle.net/HB7LU/3261/show/

Or simply use double hash as:

<a href='##foo'>Link text</a>

Demo: http://jsfiddle.net/HB7LU/3262/show/

Update: I did not know that you want no modification in HREF. But you can still achieve the desired result by overriding the existing a directive as:

myApp.directive('a', function() {
  return {
    restrict: 'E', 
    link: function(scope, element) {
        element.attr('href', '#' + element.attr('href'));
    }
  };
});

Demo: http://jsfiddle.net/HB7LU/3263/

codef0rmer
  • 10,284
  • 9
  • 53
  • 76
  • From OP follows that scrolling works if no `ng-include` has been used, so your first hypothesis seems to be incorrect. Moreover OP does want to/cannot make any tricks with `href`, so probably the second option would not work neither. – artur grzesiak Apr 25 '14 at 19:10
  • artur, correct. I do have a working demo using exactly the technique suggested by codef0rmer, but unfortunately refactoring all internal links is not an option (not b/c it's too much work, but because we'll be inlining apps provided by trusted outside parties and we can't ask them to write all their anchors in a special way, which they wouldn't be able to test in advance, etc.). We really are looking for a non-intrusive way to accomplish this. Thanks though, codef0rmer. – shacker Apr 25 '14 at 19:15
  • arturgrzesiak, Thanks for correcting me as I was wrong in my assumption but @shacker you can update it with the help of custom directives(see the update above). – codef0rmer Apr 25 '14 at 19:24
  • This answer *almost* works - the navigation happens correctly, but the displayed URL then changes to /##foo - how would I make it show a normal /#foo ? Thanks. – shacker Apr 25 '14 at 21:59
  • @shacker: Angular converts `#foo` to #/foo` - If this is fine with you then just watch for $route change event and update the hash. – codef0rmer Apr 26 '14 at 07:50
  • Thanks for the directive apprache codef0rmer. Ended up going with the function approach rather than directive, but it was a tough call. Thanks much. – shacker Apr 29 '14 at 07:09
  • 1
    The directive solution was perfect for my IE8 client. Thanks! – azium Jun 08 '15 at 23:07
2

My understanding is that using ng-include implicitly tells Angular that I want to use the routes system and overrides the browser's native anchor link behavior. I've seen recommendations to work around this by changing my html anchor links to #/#foo, but I can't do that for other reasons.

Routing system is defined in a separate module ngRoute, so if you did not injected it on your own - and I am pretty sure you did not - it is not accessible at all.

The issue is somehow different here.

ng-include depends on: $http, $templateCache, $anchorScroll, $animate, $sce. So make use of ng-include initiate all these services.

The most natural candidate to investigate would be $anchorScroll. The code of $anchorScroll does not seem to do any harm, but the service depends on $window, $location, $rootScope. The line 616 of $location says:

 baseHref = $browser.baseHref(), // if base[href] is undefined, it defaults to ''

So basically the base href is set to '', if it was no set before.

Now look HERE - from BalusC answer :

As to using named anchors like , with the tag you're basically declaring all relative links relative to it, including named anchors. None of the relative links are relative to the current request URI anymore (as would happen without the tag).

How to mitigate the issue?

I do not have much time today, so cannot test it myself, but what I would try to check as the first option is to hook up to '$locationChangeStart' event and if the new url is of #xxxxxx type just prevent the default behaviour and scroll with $anchorScroll native methods instead.

Update

I think this code should do the work:

$scope.$on("$locationChangeStart", function (event, next, current) {

    var el, elId;

    if (next.indexOf("#")>-1) {
        elId = next.split("#")[1];
        el = document.getElementById(elId);
        if(el){
           // el.scrollIntoView(); do not think we need it
           window.location.hash = "#" + elId;
           event.preventDefault();
        }    
    }
});
Community
  • 1
  • 1
artur grzesiak
  • 20,230
  • 5
  • 46
  • 56
  • artur, I've got this *almost* working as shown in this gist, but now the URL in the browser doesn't reflect the change. https://gist.github.com/shacker/11304251 I've tried setting $location.path() and $location.url() to a new location after navigating, but no dice. Any ideas? – shacker Apr 25 '14 at 21:42
  • @shacker you can access next and current route directly from callback arguments. Look at the updated answer. (I did not test it though) – artur grzesiak Apr 25 '14 at 22:03
  • Ooh, dynamite - this is working nicely, with one small exception: the displayed URL goes to /#/foo rather than /#foo. Is there any way to correct that? Much appreciated! – shacker Apr 25 '14 at 22:10
  • e.g. even if I change $location.hash(elId) to $location.hash("/#bar"), the displayed URL is unaffected. IOTW $location.hash() seems to have no effect, which is the same thing I was grappling with. – shacker Apr 25 '14 at 22:19
  • Hmm, no change in that behavior after your update. Any other ideas? Thanks. – shacker Apr 25 '14 at 22:43
  • @shacker why not to use native methods? – artur grzesiak Apr 26 '14 at 06:55
  • Thanks a bunch for the solution and for your iterations, artur. Tough call between this and the directive approach, but went with this in the end. Ironically, angular seems to have made the problem go away in the latest betas of 1.3, but this is a great one for the back pocket. – shacker Apr 29 '14 at 07:08
  • The `$scope.$on()` approach worked for me, but it also causes an infinite event loop. The browser (at least Chrome) recognizes the loop and kills it after some iterations, but it's still not ideal. Am I doing something wrong? I found that I had to apply `.substring(1)` to `elId` because `next.split()[1]` returned "/foo", so maybe the issue lies there? – dx_over_dt Mar 25 '15 at 16:38
2

This is the best solution, and works in recent versions of Angular:

Turn off URL manipulation in AngularJS

Community
  • 1
  • 1
Kohjah Breese
  • 4,008
  • 6
  • 32
  • 48
2

A lot late to the party but I found that adding a simple target="_self" fixes it.

<a href="#anchor" target="_self">Link</a>
Jessica
  • 141
  • 3
  • 6
1

Rather than applying the angular application to the entire page, you can isolate the application to just the places you want to perform an ng-include. This will allow links outside the scope of the application to retain their normal functionality, while allowing links within the application to be handled as desired.

See this plunkr:

http://plnkr.co/edit/hOB7ixRM39YZEhaz0tfr?p=preview

The plunkr shows a link outside the app that functions as normal, and a link within the app that is handled using an overriding a directive to restore normal functionality. HTML5 mode is enabled to retain 'standard' URLs (rather than 'hashbang' [without the bang!] URLs).

You could equally run the whole of the page within the app, but I thought it would be worth demonstrating how to isolate angular to certain parts of the page in any case.

mjtko
  • 1,040
  • 9
  • 12
  • Good point mjtko. Would definitely not want to have to go back and forth between in-app-mode and out-of-app-mode (php-style) every time an anchor link comes up, but yes, this technically could work. – shacker Apr 28 '14 at 17:54