入門 angular-ui-router


# angular-ui-router
[angular-ui-router](http://angular-ui.github.io/ui-router/site/#/api/ui.router.state.$stateProvider) は、AngularJS のルーティング、およびビュー分割プラグインです。

## セットアップ
[CDNJS](https://cdnjs.com/)を利用して、必要なファイル `angular.js`、`angular-ui-router.js`を読み込みます。

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.1/angular.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.15/angular-ui-router.js"></script>

  <title>myApp</title>
</head>
<body>

</body>
</html>
```

JavaScript コンソール上で、ui-router が読み込まれていることを確認できます。

```js:⌘+⌥+J
angular.module('ui.router')// Object
```

module名が `ui.router` であることに注意してください。

## myAppの初期化

```bash:tree
myApp
├── index.html
├── index.js
└── top
    ├── index.html
    └── index.js
```

アプリケーションを初期化します。<br>[DEMO](http://run.plnkr.co/plunks/P7bBnqRRQ3SkwS19LtP4/)

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">  
  <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.1/angular.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.15/angular-ui-router.js"></script>

  <title>myApp</title>
  <script src="index.js"></script>
  <script src="top/index.js"></script>
</head>
<body ng-app="myApp">

  <div ui-view></div>

</body>
</html>
```

```js:index.js
// Dependencies
angular.module('myApp',[
  'ui.router',
])

// Routes
.config(function($stateProvider){
  $stateProvider.state('top',{
    url: '/',
    templateUrl: 'top/index.html',
    controller: 'topCtrl as top',
  })
})
```

```html:top/index.html
<a ui-sref="top">{{top.title}}</a>
```

```js:top/index.js
// Dependencies
angular.module('myApp')

// Publish
.controller('topCtrl',function(){
  this.title= 'hello world'
})
```

`index.html` を開く[^1]と、myApp を初期化して top が読み込まれます。

[$stateProvider.state(name, stateConfig)](http://angular-ui.github.io/ui-router/site/#/api/ui.router.state.$stateProvider) で、ステートと呼ばれる画面単位を追加します。

[ui-sref ディレクティブ](http://angular-ui.github.io/ui-router/site/#/api/ui.router.state.directive:ui-sref)は、設定したステートの`URI`を自動発行して`href`属性を追加します。この例では、自分自身のステートを指しているため、クリックしても画面が変わらないのが正常です。

> [ui-view ディレクティブ](http://angular-ui.github.io/ui-router/site/#/api/ui.router.state.directive:ui-view)は、直接`<ui-view></ui-view>`と書くこともできますが、inline要素で表示されることに注意してください。これは`display:block`スタイルで修正できます。

---

> 9月25日追記： coffee-scriptの場合、`.controller`の第二引数がコンストラクタ関数であること、つまり`this`以外をreturnしないように注意して下さい。
>
>
>```coffeescript
># 以下のコンパイル結果は`return this.title= 'foo';`
># `this`ではなく'foo'を返す
>app= angular.module('myApp',[])
>
>app.controller 'myCtrl',->
>  this.title= 'foo'
>
>```

### リロードすると白紙になる

DEMO の `hello world` リンクをクリックして、画面をリロードすると分かりますが、白紙になります。

ui-router は起動時に、 URI の `#` 以降をurlとして解釈します。つまり

* `index.html` -> url:`''` -> state:`top`
* クリック -> state:`top` -> `index.html#/`
* リロード -> `index.html#/` -> url:`'/'` -> state:`undefined`

この問題[^2]は、下記のように修正します。

```js:index.js
// Dependencies
angular.module('myApp',[
  'ui.router'
])

// Routes
.config(function($stateProvider){
  $stateProvider.state('top',{
    url: '/',
    template: 'top/index.html',
    controller: 'topCtrl as top',
  })
})

// Default
.config(function($urlRouterProvider){
  $urlRouterProvider.when('', '/')
})
```

これで、`index.html`、`index.html#/` 、両方で state が top と解釈されます。

## 子ステート

```bash:tree
myApp
├── index.html
├── index.js
└── top
    ├── index.html
    ├── index.js
    ├── search.html
    └── search.js
```

ステート名`$stateProvider.state(name)`をドットで区切ると、ステートに所属するステートを作成します。<br>[DEMO](http://plnkr.co/edit/TEkTYA5HBq00sB3PnNiz?p=preview)

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Getting Started</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.1/angular.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.15/angular-ui-router.js"></script>

  <script src="index.js"></script>
  <script src="top/index.js"></script>
  <script src="top/search.js"></script>
</head>
<body ng-app="myApp">

  <div ui-view></div>

</body>
</html>
```

```js:index.js
// Dependencies
angular.module('myApp',[
  'ui.router',
])

// Routes
.config(function($stateProvider){
  $stateProvider.state('top',{
    url: '/',
    templateUrl: 'top/index.html',
    controller: 'topCtrl as top',
  })
})
.config(function($stateProvider){
  $stateProvider.state('top.search',{
    url: 'search',
    templateUrl: 'top/search.html',
    controller: 'topSearchCtrl as topSearch',
  })
})

// Default
.config(function($urlRouterProvider){
  $urlRouterProvider.when('', '/')
})
```

```html:top/index.html
<a ui-sref="top">{{top.title}}</a>
<a ui-sref=".search">search</a>

<div ui-view></div>
```

```js:top/index.js
// Dependencies
angular.module('myApp')

// Publish
.controller('topCtrl',function(){
  this.title= 'hello world'
})
```

```html:top/search.html
{{topSearch.title}}
```

```js:top/search.js
// Dependencies
angular.module('myApp')

// Publish
.controller('topSearchCtrl',function(){
  this.title= 'would like?'
})
```

top/index.html の `ui-sref=".search"` ディレクティブ をクリックすることで、自身がもつ ui-view ディレクティブに `top.search`の子ビューを読み込みます。

> 子ステートの url は、親ステートの url を継承することに注意してください：例 state:`top.seach` -> `'/'`+`'search'` -> url:`'/search'`

## ビューの分割

```bash:tree
myApp
├── index.html
├── index.js
├── root
│   ├── index.js
│   ├── header.html
│   ├── header.js
│   ├── footer.html
│   └── footer.js
└── top
    ├── index.html
    ├── index.js
    ├── search.html
    └── search.js
```

ヘッダ、コンテナ、フッタといったお決まりの要素を、それぞれ別のビュー・コントローラーで管理できます。<br>[DEMO](http://plnkr.co/edit/1daAOWIShZk66FxGzKXq?p=preview)

```html:index.html
<!DOCTYPE html>
<html lang="ja" ng-app="myApp" ng-controller="rootCtrl as root">
<head>
  <meta charset="UTF-8">
  <title ng-bind="root.title"></title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.1/angular.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.15/angular-ui-router.js"></script>

  <script src="index.js"></script>
  <script src="root/index.js"></script>
  <script src="root/header.js"></script>
  <script src="root/footer.js"></script>
  <script src="top/index.js"></script>
  <script src="top/search.js"></script>
</head>
<body>

  <header ui-view="header"></header>
  <div ui-view="container"></div>
  <footer ui-view="footer"></footer>

</body>
</html>
```

```js:index.js
// Dependencies
angular.module('myApp',[
  'ui.router',
])

// Routes
.config(function($stateProvider){
  $stateProvider.state('root',{
    views: {
      header: {
        templateUrl: 'root/header.html',
        controller: 'headerCtrl as header',
      },
      footer: {
        templateUrl: 'root/footer.html',
        controller: 'footerCtrl as footer',
      },
    },
  })
})
.config(function($stateProvider){
  $stateProvider.state('root.top',{
    url: '/',
    views: {
      'container@': {
        templateUrl: 'top/index.html',
        controller: 'topCtrl as top',
      }
    }
  })
})
.config(function($stateProvider){
  $stateProvider.state('root.top.search',{
    url: 'search',
    templateUrl: 'top/search.html',
    controller: 'topSearchCtrl as topSearch',
  })
})

// Default
.config(function($urlRouterProvider){
  $urlRouterProvider.when('', '/')
})
```

* ステートに `views` プロパティを設定した場合、`controller*`, `template*`プロパティは無視されます。
* `views` プロパティは key-value ペアで、 keyには `ui-view` ディレクティブの属性値を、 `value` には、ステートに設定していた `controller*`,`template*` を設定します。
* 子ステートから親ステートのビューを変更する場合、 `views` の key の末尾に `@` を足し、そのビューを持つステート名を指定します（対象がルート ui-view の場合`@`の後に何も書きません）。
* 前述の理由により、root ステートにコントローラーを設定できません。`ng-controller`ディレクティブを利用して、`ui-view` よりも親の要素 `html` に設定しました。これにより`$rootScope`を使わずにステート共通のプロパティやメソッドを、このファイルに隔離できます[^3]。

> 参考：[（英語）angularjs ui-router - how to build master state which is global across app](http://stackoverflow.com/questions/16473952/angularjs-ui-router-how-to-build-master-state-which-is-global-across-app)

## 外部リソースを待つ

```bash:tree
myApp
├── index.html
└── index.js
```

データベースや外部からデータを受け取るまで、ビューを表示させたくない場合は、ステートに `resolve` プロパティを設定します。
以下の例では、[$http](http://js.studio-kingdom.com/angularjs/ng_service/$http)サービスを使用して、[iTunesAPI](http://syncer.jp/itunes-api-matome) から jsonp を待ち、その後でビューを表示します。<br>[DEMO](http://plnkr.co/edit/j6oR15MJyksZ5itMZsKN?p=preview)

```html:index.html
<!DOCTYPE html>
<html lang="ja" ng-app="myApp">
<head>
  <meta charset="UTF-8">
  <title></title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.1/angular.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.15/angular-ui-router.js"></script>

  <script src="index.js"></script>
</head>
<body>

  <div ui-view></div>

</body>
</html>
```

```js:index.js
// Dependencies
angular.module('myApp',[
  'ui.router',
])

// Routes
.config(function($urlRouterProvider){
  $urlRouterProvider.when('', '/')
})
.config(function($stateProvider){
  $stateProvider.state('top',{
    url: '/',
    resolve: {
      itunes: function($http){
        var query= '漁港 鮪'

        var api= 'https://itunes.apple.com/search'
        var params= [
          'term='+query,
          'country=jp',
          'media=music',
          'entity=album',
          'lang=ja_jp',
          'limit=10',
          'callback=JSON_CALLBACK'
        ]
        var uri= api+'?'+params.join('&')

        // available promise at resolve
        return $http.jsonp(uri)
      },
    },
    template: [
      '<section ng-repeat="result in top.results">',
        '<a ng-href="{{result.collectionViewUrl}}">',
          '<img ng-src="{{result.artworkUrl100}}" alt="{{result.collectionName}}" />',
        '</a>',
        '<pre>{{result|json}}</pre>',
      '</section>',
    ].join(''),
    controller: 'topCtrl as top',
  })
})

// Publish
.controller('topCtrl',function(itunes){
  this.results= itunes.data.results
})
```

* resolve は key-value ペアで、key にDI名を、value にプロミスを返す関数[^5]を定義します。
* [controller に注入する](http://qiita.com/kenjiSpecial@github/items/6b7db5591bb2684058d5)ことで、注入したコントローラが起動する前に value の関数を実行します。
* 実行した関数がプロミスを返した場合、そのプロミスが解決するまで`template*`, `controller*`は実行されません。

> プロミスの詳しい書き方については、[azu 氏のJavaScript Promiseの本](http://azu.github.io/promises-book/)や、[たいが氏の $q サービスで覚える Promise](http://dev.classmethod.jp/client-side/javascript/angularjs-q-promise/)を参照ください。

## `url`のパース

```bash:tree
myApp
├── index.html
└── index.js
```

`#/search/澤野弘之` のようなURIへアクセスした時、`search`ステートに`澤野弘之`を変数として渡したい場合があります。その場合、ステートを`url:'search/:query'`と書くと、`resolve`や`controller*`、`template*`に`$stateParams`を注入することで、`$stateParams.query`から`澤野弘之`を取得することができます。<br>[DEMO](http://plnkr.co/edit/nKrMBMp4HWnXDTIv2ET4?p=preview)

```html:index.html
<!DOCTYPE html>
<html lang="ja" ng-app="myApp">
<head>
  <meta charset="UTF-8">
  <title>iTunes Finder</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.1/angular.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.15/angular-ui-router.js"></script>

  <script src="index.js"></script>
</head>
<body>

  <div ui-view></div>

</body>
</html>
```

```js:index.js
// Dependencies
angular.module('myApp',[
  'ui.router',
])

// Routes
.config(function($urlRouterProvider){
  $urlRouterProvider.when('', '/search/澤野弘之')
})
.config(function($stateProvider){
  $stateProvider.state('top',{
    url: '/',
    template: [
      '<form ng-submit="top.submit(query)">',
        '<input ng-model="query" autofocus required/>',
        '<button>search</button>',
      '</form>',
      '<div ui-view/>',
    ].join(''),
    controller: 'topCtrl as top',
  })
})
.config(function($stateProvider){
  $stateProvider.state('top.search',{
    url: 'search/:query',
    resolve: {
      itunes: function($stateParams,$http){
        var query= $stateParams.query

        var api= 'https://itunes.apple.com/search'
        var params= [
          'term='+query,
          'country=jp',
          'media=music',
          'entity=album',
          'lang=ja_jp',
          'limit=10',
          'callback=JSON_CALLBACK'
        ]
        var uri= api+'?'+params.join('&')

        console.log('Request to',uri)

        return $http.jsonp(uri)
      }
    },
    template: [
      '<section ng-repeat="result in search.results">',
        '<a ng-href="{{result.collectionViewUrl}}">',
          '<img ng-src="{{result.artworkUrl100}}" alt="{{result.collectionName}}" />',
        '</a>',
        '<pre>{{result|json}}</pre>',
      '</section>',
    ].join(''),
    controller: 'searchCtrl as search',
  })
})

// Controllers
.controller('topCtrl',function($state){
  this.submit= function(query){
    $state.go('top.search',{query:query})
  }
})
.controller('searchCtrl',function($scope,$stateParams,itunes){
  $scope.$parent.query= $stateParams.query// remember :query via URI
  this.results= itunes.data.results
})
```

[外部リソースを待つ](#外部リソースを待つ)のサンプルコードに、検索機能を加えています。

[$state.go](http://angular-ui.github.io/ui-router/site/#/api/ui.router.state.$state) は、`ui-sref`ディレクティブをコントローラーから使用するための API です。

> `url`は`UrlMatcher`を介して$stateParamsに変換されます。 参考：[（英語）UI Router: UrlMatcher](http://angular-ui.github.io/ui-router/site/#/api/ui.router.util.type:UrlMatcher)

## 例外処理

```bash:tree
myApp
├── index.html
└── index.js
```

`$stateProvider`、はアクセス時のURIを、`.state`で追加した順番に`url`を評価し、はじめに一致したステートを実行します。一致しなかったとき、表示するビューが無いため白紙になります。
`url:'/:path'`を設定したステートを一番最後に追加する[^4]ことで、一致しなかった全ての url を捕まえることができます。<br>[DEMO](http://plnkr.co/edit/PMGIdhVe50xjt7TLk0T8?p=preview)

```html:index.html
<!DOCTYPE html>
<html lang="ja" ng-app="myApp">
<head>
  <meta charset="UTF-8">
  <title></title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.1/angular.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.15/angular-ui-router.js"></script>

  <script src="index.js"></script>
</head>
<body>

  <div ui-view></div>

</body>
</html>
```

```js:index.js
// Dependencies
angular.module('myApp',[
  'ui.router',
])

// Routes
.config(function($urlRouterProvider){
  $urlRouterProvider.when('', '/')
})
.config(function($stateProvider){
  $stateProvider.state('top',{
    url: '/',
    template: [
      '<h1>top</h1>',
      '<a href="#/foo">foo</a>',
      '<a href="#/bar">bar</a>',
      '<a href="#/baz">baz</a>',
      '<a ui-sref="invalid-state">error</a>',
    ].join('')
  })

  $stateProvider.state('error',{
    url: '/:path',
    template: '<strong>404</strong><a ui-sref="top">back</a>'
  })
})
```

`url:'/:path'`は`ui-sref`ディレクティブに適用されません。
これは、`$rootScope.$on('$stateNotFound',callback)`で補足します。
同様に、ステートの resolve プロパティ が解決されなかった場合（`$http.get` で 404が返る、など）は、`$rootScope.$on('$stateChangeError',callback)`で補足します。<br>[DEMO](http://plnkr.co/edit/JUPXefLCuWu8BvconXni?p=preview)

> callback の第一引数 event の event.preventDefault() を実行することで、ui-router の処理（エラー表示など）を中断できます

# 参考にした記事
* [（英語）公式リファレンス…UI Router: API Reference](http://angular-ui.github.io/ui-router/site/#/api/ui.router.state.$state)
* [（英語）よくある質問…Frequently Asked Questions · angular-ui/ui-router Wiki](https://github.com/angular-ui/ui-router/wiki/Frequently-Asked-Questions)
* [（英語）UI-Router: Why many developers don’t use AngularJS’s built-in router](http://www.funnyant.com/angularjs-ui-router/)
* [AngularUI Router Wiki - Home.md の日本語訳](http://qiita.com/damele0n/items/3ee2e8c9adc2520cc0b3)
* [初心者向けAngularJS - その２ - albatrosary's blog](http://albatrosary.hateblo.jp/entry/2014/11/07/075848)
* [AngularUI Routerのつかいかた](http://b.denkizakana.com/2014/02/angularui-router.html)
* [Angular ui-router tips - まぐねっとのブログ](http://magnet88jp.hateblo.jp/entry/2015/02/25/154614)
* [ui-router を使ってログイン必須のstateを作る](http://qiita.com/ooharabucyou/items/65f52646d721da5b2032)
* [angular-ui-router が便利すぎてやばいという話](http://qiita.com/mashiro/items/17d836ada8e96a928968)

## ui-router以外の参考にした記事
* [AngularJS styleguide 日本語訳](http://angularjsninja.com/blog/2014/07/29/angularjs-styleguide-in-Japanese/)
* [AngularJSのパフォーマンス改善入門](http://qiita.com/zoetro/items/5156aef51222e81dd022)
* [JavaScript Promiseの本](http://azu.github.io/promises-book/)
* [[AngularJS] $q サービスで覚える Promise](http://dev.classmethod.jp/client-side/javascript/angularjs-q-promise/)
* [ウェブデザイナーがはじめるAngularJS：Promise（Deferred）をつかった非同期処理 | Webデザイン、フロントエンド系の技術に関する備忘録 - whiskers](http://whiskers.nukos.kitchen/2015/04/27/angularjs-promise.html)
* [AngularJS 地味だけど知っておきたい filter 機能 - Can I do web?](http://pote.hatenadiary.jp/entry/2012/11/09/140457)

## この記事では触れなかった関連する内容の記事
* [プロバイダーに関して (factory, service, provider) | AngularJS - angular.jsメモ](http://qiita.com/akkun_choi/items/618d2d9107029ab0422e#%E3%83%97%E3%83%AD%E3%83%90%E3%82%A4%E3%83%80%E3%83%BC%E3%81%AB%E9%96%A2%E3%81%97%E3%81%A6-factory-service-provider)
* [AngularJS の $locationProvider.html5Mode について](http://qiita.com/shogogg/items/542bd6d18f777bcc24bc)
* [angularでng-annotateを利用してminify対策を自動化する
](http://soundtricker.blogspot.jp/2014/08/angularng-annotateminify.html)
* [（英語）angular-animate + animate.cssを使ったアニメーションの実装例](https://divshot.com/blog/tips-and-tricks/angular-1-2-and-animate-css/)

[^1]: [XMLHttpRequestを使用する関係上](http://stackoverflow.com/questions/4819060/allow-google-chrome-to-use-xmlhttprequest-to-load-a-url-from-a-local-file)、`file://`上では開発が難しいです。Windows であれば、[XAMPP](https://www.apachefriends.org/jp/index.html) などを使用して、myAppディレクトリを localhost で開けるように設定すれば良いでしょう。Macはターミナルで [SimpleHTTPServer](http://maepon.skpn.com/Program/entry-38.html) を使えば、すぐに動作を確認できます。
[^2]: https://github.com/angular-ui/ui-router/issues/739#issuecomment-31702635
[^3]: 「[プレゼンテーションロジックのみ (MVVM): controller 内ではプレゼンテーションロジックのみとし、ビジネスロジックは service に委譲する](https://github.com/tama3bb/angularjs-styleguide#controllers)」も参照ください
[^4]: [$urlRouterProvider.otherwise](https://github.com/angular-ui/ui-router/wiki/URL-Routing#otherwise-for-invalid-routes) を使用してリダイレクト先を設定することもできます
[^5]: [関数ではなく静的な値も注入できます](https://github.com/angular-ui/ui-router/wiki#resolve)が、resolve 以外の手段(プロバイダなど)を使用するケースが殆どでしょう