AngularJSでページ内リンクを使うにはどうするか

  • 投稿日:
  • by

最近、ページ内リンクというのはあまり使われないと思いますが、使いたいケースがあったので、調べました。備忘メモです。

ページ内リンクというのは、リンク先に

<a name="id001">

とか

<div id="id001">

とか書いておいて、リンク元を

<a href="#id001">

とすることで、このリンクをクリックするとリンク先にスクロールしてくれるものです。

ところが、AngularJSではこれはルートとみなされ、ご丁寧に#/id001に変換して、$routeProviderの処理がされてしまいます。当然、そのようなルートは用意していないので、予期せぬ動作になります。例えば、otherwiseにredirectToを設定していたりすると、トップページに戻ります。

これは困ったということで、いろいろ調べました。はじめに見つけたのがこれ。

Angular JS - Scrolling To An Element By Id

AngularJSには$anchorScroll()というのが用意されていてその使い方が説明されています。具体的には、$rootScope$routeChangeSuccessイベントをフックするのですが、詳しくは、上記サイトをご覧ください。

さて、これを試しましたが、うまくありませんでした。 新しいページを表示してスクロールする分にはいいのですが、同一ページでもリフレッシュされてしまいます。つまり、$routeChangeSccessイベントはルートが新しくなってから呼ばれるものなのです。AngularJSのドキュメントを読むと、$routeChangeStartというイベントもあるのですが、結局、そのイベント処理のあとに、ルートを書き換えてしまいました。

さらに調べると、次のIssueを見つけました。

https://github.com/angular/angular.js/issues/1699

$locationのhashやpathを変更しても、ルートを書き換えない方法を用意してほしいという要望です。この中に書いてある方法では解決しませんでしたが、最後にPRを見つけました。

https://github.com/angular/angular.js/pull/2555

蛇足ですが、このPRは分かりやすく書かれてますね、Basic Usageとか。 でも、結局このPRもリジェクトされてました。 これを許すと、URLの状態とルートの状態が整合しないことができてしまうので避けたいそうです。

悩んだあげく、次の方法で解決しました。

まず、$routeProviderを次のようにします。

$routeProvider.when('/home', {
  templateUrl: 'partials/home.html',
  controller: HomeCtrl,
  reloadOnSearch: false
}).
otherwise({
  redirectTo: '/home'
});

ポイントは、reloadOnSearchです。OnSearchとありますが、hashの変更にも有効のようでした。

その上で、

<a href="#id001">

の代わりに、

<a href="#/home#id001">

とします。まあなんとも普通の方法ですが、これで目的は達成できました。 /homeがハードコードされているのが気になる方は、$locationが使える状況なら、次のようにしましょう。

'<a href="#' + $location.path() + '#id001">'

いかがでしょうか。AngularJSを使いつつ、レガシーなことをやろうとすると大変だ、という例でした。

ちなみに、hrefを使わなくて良いのなら、

Angular JS - Scrolling To An Element By Id

の2つ目の方法がよさそうです。試していませんが。