SPAの問題
SPAを本気で実装するとなるといろいろ苦しいところが出てきます。。例えば
- ページの初期化に動的なデータ(毎回サーバーにajaxを投げたい)が必要
- cssを各ページ専用のものに切り替えたい
- 動的にモジュールをロードしたい
今回は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'
});
});
ステート一覧
-
abstract
ステートのroot
-
root
の子としてstate1
-
state1
の子としてstate1.list
-
root
の子としてstate2
-
state2
の子としてstate2.list
- 最後に独立した
dummy
ステート
abstractステートとは
- ステートとして存在するがアクティベートできない(遷移させられない、Rootのリンクをクリックするとエラーになるのがわかる)
- 子ステートへのプレフィックスとなる
url
を指定できる -
resolve
プロパティーで子ステートの依存を解決できる -
data
プロパティーで子ステートへデータを渡せる
今回必要な一部のみ説明しますが、別の便利なプロパティーもまだあります。
ページの初期化に動的なデータを取得の解決方法
シナリオ
- 各ステートで共通のデータが必要でそれを
root
ステートで用意してあげます。 - 各ステートが独自に必要なデータはそれぞれが
resolve
で取得します。 - 子ステート間のセッションとして
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
プロパティーを使用データ実現可能です。(詳細)
cssを各ページ専用のものに切り替えたい場合の解決方法
- ステートの定義時に
css
プロパティーを追加する -
css
プロパティーが設定されている場合は自動的にlink
タグを作成する -
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
タグのテンプレートの部分を編集することで対応可能です。