ばかおもちゃ本店:Youtube twitter:@sashimizakana Amazon.co.jpアソシエイト

2014年2月9日日曜日

AngularUI Routerのつかいかた

まえおき

AngularUI RouterはAngularJSのルーティングモジュール。
AngularJSの標準のルーティング機能より高機能で、ページ内に複数のviewを持ったり、
階層化されたviewを利用することなどができる。
最終的なプロジェクトの目標はngRouteに取って代わって標準搭載されることだそうで、AngularJS本体も興味を示しているらしい(GitHubのwikiに書いてある)。
で、ちょこっと触ってみて、実際の使い方に触れた解説記事は少ないようなのでざっくり使い方を書く。

https://github.com/angular-ui/ui-router/wiki

ちなみに上記の公式のwikiを読めば分かることしか書いてない。

あとQiitaに抜粋版を書いたので、細かい説明が要らない人はたぶんそっちのがわかりやすい。

AngularJS - AngularUI Routerの使い方 - Qiita

サンプル


以下でやっていることをだいたい盛り込んだサンプルがこれ。
右側のメニューのCodeを押せば各ファイルが、Editを押せばPlunkerで編集できる。Plunkerでいろいろコードを変えてみたりして試してみるとわかりやすいかもしれない(知ってると思うけど、Plunkerで編集したりしても別にこっちのサンプルが壊れたりしないので安心していい)
いちいち対応させて説明することはしない。

つかいかた

モジュールなので、ダウンロードしたら普通のモジュールと同じように読み込む。
ちなみにAngularJS Ver1.2から標準のルーティング機能もモジュール化されてる。
なのでそっからの移行だと、ngRouteを読み込む部分をui.routerに書き換えればいい。

ngRouteとの対比で書いていくと、

$route -> $state
$routeParams -> $stateParams
$routeProvider -> $stateProvider
ng-view -> ui-view

のような感じで置き換える。
メソッド名は同じにしてあるところもあり、使いまわせるところもある。
たとえば$routeParamsなんかは、$stateParamsに置き換えるだけで使える。
ちなみに$locationProvider(html5Mode設定するやつ)はngRouteと関係ないので
そのまま。

$stateProvider

$stateProviderのstateメソッドでルーティングの設定をする。
ngRouteでの$routeProvider.whenを$stateProvider.stateに置き換える。
で、設定はぱっと見似たようなもんだけど、考え方はちょっと違う。

ngRouteはシンプルで、特定のURLに来たらアプリ全体で唯一のng-viewに対して、指定のテンプレートとコントローラを埋め込むというだけのものだ。設定するのはURLとテンプレートおよびコントローラーの対応関係になる。

それに対してui-routerはstate(状態)を設定する。stateがまず先にあり、そのstateに対応するURLを設定し、また、そのstateのとき、どのview(複数もあり得る)にテンプレートとコントローラーを当てるかという設定をする。
stateは階層を持ち、ドットで区切って指定をする。
つまりitemsというstateを設定して、items.detailというstateを設定すると、items.detailはitemsの子として作成される。子として作成されると、viewで設定するテンプレートを表示する位置が親の内側になったり、URLが親のURLに続く感じになる。もっともこれは何も指定しなければそうなるというだけで、設定次第で変更できる(後述)。

とにかく、ngRouteはURLを基準にして動作するが、ui-routerはstateが基準になる。
なので設定も以下のような感じになる。

$routeProvider(url,{設定}) → $stateProvider(state名,{設定})

たとえば/emp/12345みたいなURLがあり、empだけなら社員一覧、12345が渡されたら詳細というような設計なら、以下のようなコードになる。

ngRouteの場合
$routeProvider
    .when("/emp",{
        templateUrl:"list.html"
        ,controller:"ListCtrl"
    })
    .when("/emp/:empID"){
        templateUrl:"emp.html"
        ,controller:"EmpCtrl"
    };
ui-routeの場合
$stateProvider
    .state("emp",{
        url:"/emp"
        ,templateUrl:"list.html"
        ,controller:"ListCtrl"
    })
    .state("emp.detail",{
        url:"/:empID"
        ,templateUrl:"emp.html"
        ,controller:"EmpCtrl"
    });
という感じになる。
上でも書いてるがemp.detailはempの子なので、:empIDというUrlは、empのUrl(/emp)に続く。
つまり/emp/:empIDとなるので、ngRouteと同じURLになる。

もっとも、ngRouteの場合、すべてのテンプレートは常に単一のng-viewに表示されるが、ui-routerの場合この設定だとemp.htmlはlist.htmlの中のui-viewに表示される。
つまり一覧のリストのページの一部に詳細が表示されるような感じになる。
で、先に書いたとおり、ngRouteと同じように単一のviewに表示することもできる。
そのためには表示先のviewを指定すれば良い。そのためにはviewの名前の指定と、参照先のviewを絶対指定することが必要になるので、順に説明する。

viewの名前付け

表示先のviewは名前をつけることができ、これにより一度に複数指定したりできる。
<div ui-view="view1"></div>

<div ui-view="view2"></div>
上記のタグのviewを設定する場合、
.state("emp.detail"),{
    url:"/:empID"
    ,views:{
        view1:{
            templateUrl:"view1.html"
            ,controller:"View1Ctrl"
        }
        ,view2:{
            templateUrl:"view1.html"
            ,controller:"View1Ctrl"
        }
    }
}
のように設定する。viewsは保持するプロパティ名がviewの名前に対応する。
ちなみに最初のコードでは名前を指定していないが、それは以下の設定と同じだ。
.state("emp.detail"),{
    url:"/:empID"
    ,views:{
        "":{
            templateUrl:"emp.html"
            ,controller:"EmpCtrl"
        }
    }
}
これでも問題なく動作する。
ちなみにAngularJSの話からはやや脱線するがJavaScriptでプロパティ名に上記のような空文字やハイフン、アットマークなんかを使ったりするときは二重引用符(")で囲まないとエラーになる。
で、何でそんなことを書くかと言うと、viewsのプロパティでアットマークを使えば、表示対象のviewの位置を親と関係なく絶対指定することができるためだ。

viewの絶対・相対指定

画面遷移とURLの階層は常に対応するわけではない。
ファイラーとか常に階層化された画面構造ならともかく、一番上のサンプルと同じように社員の一覧から、社員の詳細を選び、その社員の投稿した画像一覧をほぼ画面全域に展開したい、なんてことがありうる。その場合、URL上は親子関係があっても、画像一覧の画面は親の表示位置を上書きすることになる。
(画像一覧ページが階層的に詳細の下に入る必要はないとかいうことはさておく)
(さておくけど、うまい設計なら良い感じに階層化できるのかもしれない)

ともかく、viewsのプロパティ名は単純に名前だけ指定すると、親の持つビュー名になる。
だがビュー名@ステート名のように書けば、直接表示対象のビューを指定できる。つまり絶対指定だ。
ちなみにビュー名@で終わる場合は、一番上位の階層のui-viewになる。
一番上位階層のビューが名前無しの場合、ただ"@"で指定することができる。
同じく、各ステートの名前無しビューは、"@ステート名"という形で指定する。
.state("emp.detail.picture"),{
    url:"/all"
    ,views:{
        "@emp":{
            templateUrl:"picture.html"
            ,controller:"PictureCtrl"
        }
    }
}
このコードだと、empの持つ名前無しのビューに表示される。
つまり現在emp.detailが表示されている位置が差し替えられる。

その他設定

設定側で書きたいことはだいたい書けたのであとは適当に列挙。
詳しくは公式のWikiを見てね。

  • 抽象(abstract)指定で親になるが指定はできないステートとか作れる。
  • urlは{id:[0-9]{5}}みたいに正規表現指定できる。
  • urlは^から始めると絶対指定で最上位から始められる。
    • これ移動時にstate指定するときの^(親のステート)と同文字で意味が違ってわかりにくい気がする。

ルートの利用

今まで設定したルートを利用する場合、つまりアンカータグ(a)なんか移動するときは、ui-srefディレクティブを使う。
もちろん#/hogeみたいにhrefとかng-hrefに書いても正しく動作するんだけど、
それだとhtml5Modeを切り替えたりURLの構造を変えた時に全部書きなおすことになる。
ui-srefはステートとパラメータを渡しておけば、あとのURL構造の変更とかhtml5Modeへの対応なんかは勝手にやってくれる。

使い方はこんな感じ。
<a href="#/emp/hoge/1234" .. → <a ui-sref=".hoge({id:empID})"...>
これも相対パスと絶対パスがあって、この例だと現在のstate直下のhogeというstateに移動する。ドットが頭に付くと相対パスで、ドットが無ければ絶対パスで、^から始まると親のステートとか、そんな感じ。詳しくはWikiを参照のこと。
引数で渡すオブジェクトのプロパティ名は当然設定したときにurlが受け取るパラメータ名と一致していないとならない。

ui-sref-activeにそのステートがアクティブなときに付けたいクラスを指定すると、上のサンプルみたいにbootstrapのactiveなんかを簡単に利用できるので何かと楽。ng-classと違って、普通のclass指定とも共存するので便利。

ちなみにコード上から移動するときは$state.go()を利用する。

以上、そんな感じ。