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

  • 6
    Like
  • 0
    Comment
More than 1 year has passed since last update.

SPAの問題

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

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

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

ファイル構成

├── 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公式サンプルを拡張)

<!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>

ステートの定義

    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ステートのresolvedynamicDataプロパティーでdata.json取得のプロミスを返しています。resolveはプロミスが返されるとそれが解決されるまで子ステートをアクティベートしません。

子ステートから親のデータを参照

子ステートのコントローラーは親resolveのプロパティー名をそのままDIできるようになっています。

resolveの実行タイミング

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

データのキャッシュ

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

個別resolve

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

オブジェクトの共有

セッションは各ステートでIncrease Countリンクで更新され、別の兄弟ステートへ共有されているのがわかります。共有されたくないデータの場合はdataプロパティーを使用データ実現可能です。(詳細

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

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

コントローラー

    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タグの作成

<link rel="stylesheet" ng-repeat="path in cssPaths track by $index" ng-href="{{path}}.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タグのテンプレートの部分を編集することで対応可能です。