angular ui-routerの動的データとcssの切替え方法

# SPAの問題

SPAを本気で実装するとなるといろいろ苦しいところが出てきます。。例えば

 1. ページの初期化に動的なデータ（毎回サーバーにajaxを投げたい）が必要
 2. cssを各ページ専用のものに切り替えたい
 3. 動的にモジュールをロードしたい

今回はangularのui-routerを使用して１と２の解決方法を紹介したいと思います。

### ファイル構成

```
├── index.html

├── angular-ui-router.js
├── angular.js
├── app.js

├── app.css
├── state1.css
├── state2a.css
├── state2b.css

├── data.json
└── state1.json
```

`index.html`の中身（`ui-router`[公式サンプル](https://github.com/angular-ui/ui-router#get-started)を拡張）

```html
<!DOCTYPE html">
<html lang="ja" ng-app="myApp" ng-controller="AppCtrl">

<head>
    <meta charset="utf-8">
    <title>angular ui route tips</title>
    <link rel="stylesheet" href="app.css">
    <!-- dynamic css -->
    <link rel="stylesheet" ng-repeat="path in cssPaths track by $index" ng-href="{{path}}.css">
</head>

<body>
    <a ui-sref="root">Root</a>
    <a ui-sref="state1">State 1</a>
    <a ui-sref="state2">State 2</a>
    <a ui-sref="dummy">Dummy</a>
    <div ui-view></div>

    <script src="angular.js"></script>
    <script src="angular-ui-router.js"></script>
    <script src="app.js"></script>

    <script type="text/ng-template" id="partials/root.html">
        <div ui-view></div>
    </script>

    <script type="text/ng-template" id="partials/state1.html">
        <h1>State 1</h1>
        <hr/>
        <a ui-sref="state1.list">Show List</a>
        <a ng-href="#" ng-click="increaseCount($event)">Increase count</a>
        click count : {{clickCount}}
        <div ui-view></div>
    </script>

    <script type="text/ng-template" id="partials/state2.html">
        <h1>State 2</h1>
        <hr/>
        <a ui-sref="state2.list">Show List</a>
        <a ng-href="#" ng-click="increaseCount($event)">Increase count</a>
        click count : {{clickCount}}
        <div ui-view></div>
    </script>

    <script type="text/ng-template" id="partials/state1.list.html">
        <h3>List of State 1 Items</h3>
        <ul>
            <li ng-repeat="item in items">{{ item }}</li>
        </ul>
    </script>

    <script type="text/ng-template" id="partials/state2.list.html">
        <h3>List of State 2 Things</h3>
        <ul>
            <li ng-repeat="thing in things">{{ thing }}</li>
        </ul>
    </script>

    <script type="text/ng-template" id="partials/dummy.html">
        <h3>dummy</h3>
    </script>
</body>

</html>
```

### ステートの定義

```js
    var myApp = angular.module( 'myApp', ['ui.router']);

    myApp.config(function( $stateProvider, $urlRouterProvider ) {

        var session, dynamicData;

        session = {
            clickCount: 0
        };

        $urlRouterProvider.otherwise( '/state1' );

        $stateProvider
            .state( 'root', {
                url: '',
                abstract: true,
                template: '<ui-view/>',
                resolve: {
                    session: function() {

                        return session;
                    },
                    dynamicData: function( $q, $http ) {

                        var defer;

                        defer = $q.defer();

                        if ( ! dynamicData ) {

                            $http({
                                method: 'GET',
                                url: 'data.json'
                            }).then(function successCallback( response ) {

                                dynamicData = response.data;

                                defer.resolve( dynamicData );
                            });
                        } else {

                            defer.resolve( dynamicData );
                        }

                        return defer.promise;
                    }
                }
            })
            .state( 'state1', {
                parent: 'root',
                url: '/state1',
                templateUrl: 'partials/state1.html',
                css: 'state1',
                resolve: {
                    myData: function( $q, $http ) {

                        var defer;

                        defer = $q.defer();

                        $http({
                            method: 'GET',
                            url: 'state1.json'
                        }).then(function successCallback( response ) {

                            defer.resolve( response.data );
                        });

                        return defer.promise;
                    }
                },
                controller: function( $scope, $state, session, dynamicData, myData ) {

                    $scope.clickCount = session.clickCount;

                    $scope.increaseCount = function( $event ) {

                        $event.preventDefault();

                        $scope.clickCount = session.clickCount = session.clickCount + 1;
                    };
                }
            })
            .state( 'state1.list', {
                url: '/list',
                templateUrl: 'partials/state1.list.html',
                controller: function( $scope ) {

                    $scope.items = ['A', 'List', 'Of', 'Items'];
                }
            })
            .state( 'state2', {
                parent: 'root',
                url: '/state2',
                css: ['state2a', 'state2b'],
                templateUrl: 'partials/state2.html',
                controller: function( $scope, $state, session, dynamicData ) {

                    $scope.clickCount = session.clickCount;

                    $scope.increaseCount = function( $event ) {

                        $event.preventDefault();

                        $scope.clickCount = session.clickCount = session.clickCount + 1;
                    };
                }
            })
            .state( 'state2.list', {
                url: '/list',
                templateUrl: 'partials/state2.list.html',
                controller: function( $scope ) {

                    $scope.things = ['A', 'Set', 'Of', 'Things'];
                }
            })
            .state( 'dummy', {
                url: '/dummy',
                templateUrl: 'partials/dummy.html'
            });
    });
```

### ステート一覧

 1. `abstract`ステートの`root`
 2. `root`の子として`state1`
 3. `state1`の子として`state1.list`
 4. `root`の子として`state2`
 5. `state2`の子として`state2.list`
 6. 最後に独立した`dummy`ステート

### abstractステートとは

 - ステートとして存在するがアクティベートできない（遷移させられない、**Root**のリンクをクリックするとエラーになるのがわかる）
 - 子ステートへのプレフィックスとなる`url`を指定できる
 - `resolve`プロパティーで子ステートの依存を解決できる
 - `data`プロパティーで子ステートへデータを渡せる

**今回必要な一部のみ説明しますが、別の便利なプロパティーもまだあります。**

##  ページの初期化に動的なデータを取得の解決方法

### シナリオ

 1. 各ステートで共通のデータが必要でそれを`root`ステートで用意してあげます。
 2. 各ステートが独自に必要なデータはそれぞれが`resolve`で取得します。
 3. 子ステート間のセッションとして`session`プロパティーを提供します。


#### ルートのresolveプロパティーでの動作
`root`ステートの`resolve`の`dynamicData`プロパティーで**data.json**取得のプロミスを返しています。`resolve`はプロミスが返されるとそれが解決されるまで子ステートをアクティベートしません。

#### 子ステートから親のデータを参照
子ステートのコントローラーは親`resolve`のプロパティー名をそのまま**DI**できるようになっています。


#### resolveの実行タイミング
`resolve`が実行されるのは別のステートから遷移して来た場合のみです。すなわち同じ親を持つ兄弟間での遷移には実行されません。上記の例では**Dummy**から**State1**か**State2**へ遷移した場合のみ実行されます。

#### データのキャッシュ
今回は`ajax`の結果をキャッシュするために`dynamicData`のローカル変数に代入してから返しています。この場合`dynamicData`は共有されるので子ステートで更新する際に要注意です。

#### 個別resolve
各ステートで必要なデータは各自の`resolve`で取得します。今回は**state1.json**を`state1`がアクティベートされる度に取得してます。

#### オブジェクトの共有
セッションは各ステートで**Increase Count**リンクで更新され、別の兄弟ステートへ共有されているのがわかります。共有されたくないデータの場合は`data`プロパティーを使用データ実現可能です。（[詳細](https://github.com/angular-ui/ui-router/wiki#attach-custom-data-to-state-objects)）


## cssを各ページ専用のものに切り替えたい場合の解決方法

1. ステートの定義時に`css`プロパティーを追加する
2. `css`プロパティーが設定されている場合は自動的に`link`タグを作成する
3. `link`タグの作成タイミングはステートのイベント内で行う為各ステートでの処理は不要

### コントローラー

```js
    myApp.controller( 'AppCtrl', ['$rootScope', '$scope', function( $rootScope, $scope ) {

        $scope.cssPaths = [];

        $rootScope.$on( '$stateChangeStart',
            function( event, toState, toParams, fromState, fromParams ) {

                $scope.cssPaths = [];

                if ( toState.css ) {

                    $scope.cssPaths = angular.isArray( toState.css ) ? toState.css : [toState.css];
                }
            })
    }]);
```

### 動的css用linkタグの作成

```html
<link rel="stylesheet" ng-repeat="path in cssPaths track by $index" ng-href="{{path}}.css">
```

### cssの中身

```css
/* state1.css */
body {
    background-color: yellow;
}
body * {
    color: red;
}

/* state2a.css */
body {
    background-color: green;
}
/* state2b.css */
body * {
    color: white;
}
```

今回は複数`css`を対応するため、配列の指定も可能にしてあります。
`css`フォルダーを別に分けている場合は`link`タグのテンプレートの部分を編集することで対応可能です。