ui-routerには、デフォルトでは戻る機能はないので、onsenuiと組み合わせて、それっぽく実装してみました。
特徴
Monaca + ui-routerだけですと、AndroidのBackボタンでアプリが閉じてしまいます。
そのため、onsenuiのons-navigator
タグを使って、ページ情報をスタックしていきます。
実際の画面スタック&ポップ処理は、ui-routerの遷移情報を蓄積して、それを元に行き来します。
ベース
[monaca][onsenui][AngularJS][ui-router]onsenui/ui-router最小限プロジェクト
html
index.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com; style-src * 'unsafe-inline'; script-src * 'unsafe-inline' 'unsafe-eval'">
<script src="components/loader.js"></script>
<script src="script.js"></script>
<link rel="stylesheet" href="components/loader.css">
<link rel="stylesheet" href="css/style.css">
</head>
<body ng-app="MyApp">
<ons-page>
<!-- ツールバー -->
<ons-toolbar>
<!-- ツールバー左 -->
<div class="left" ng-controller="BackController">
<!-- 戻るボタン -->
<!-- 画面スタックが1件以上ある場合のみ、アイコンを表示する -->
<ons-back-button ng-show="$root.pages.length > 0" ng-click="backNavi();">
Back
</ons-back-button>
</div>
<!-- ツールバー真ん中 -->
<div class="center" style="text-align: center;">
[Back]
</div>
<!-- ツールバー右 -->
<div class="right">
<!-- 何も記載しない場合は、空文字を入れる。(入れないとセンタリングレイアウトがずれる) -->
</div>
</ons-toolbar>
<!-- コンテンツ部分 -->
<div ui-view></div>
<!-- ナビゲーター(実際には表示しない) -->
<ons-navigator var="navi">
</ons-navigator>
</ons-page>
<!-- 初期表示画面 -->
<ons-template id="main.html">
<ons-page ng-class="contents">
<ons-button ui-sref="page-A">pageA</ons-button>
</ons-page>
</ons-template>
<ons-template id="pageA.html">
<!-- navigatorに詰めるHTMLはons-pageタグで囲む -->
<ons-page ng-class="contents">
<div>
pageAだよ
<ul>
<li><a ui-sref="page-A-1">pageA-①</a></li>
<li><a ui-sref="page-A-2">pageA-②</a></li>
</ul>
pageAの入れ子だよ
<ul>
<li><a ui-sref=".page-A-sub1">pageA-sub①</a></li>
<li><a ui-sref=".page-A-sub2">pageA-sub②</a></li>
</ul>
<div ui-view></div>
</div>
</ons-page>
</ons-template>
<ons-template id="page-A-sub1.html">
pageAの入れ子①だよ
</ons-template>
<ons-template id="page-A-sub2.html">
pageAの入れ子②だよ
</ons-template>
<ons-template id="pageA-1.html">
<ons-page ng-class="contents">
<div>
pageA-①だよ
<ul>
<li><a ui-sref="page-A-1-1">pageA-①-①</a></li>
<li><a ui-sref="page-A-1-2">pageA-①-②</a></li>
</ul>
</div>
</ons-page>
</ons-template>
<ons-template id="pageA-2.html">
<ons-page ng-controller="PageA2Controller" ng-class="contents">
値受け渡し
<p>
<ons-input modifier="underbar" ng-model="sample" placeholder="テストデータ" float></ons-input>
</p>
<hr>
pageA-②だよ
<ul>
<li><ons-button ng-click="jump('page-A-2-1');">pageA-②-①</ons-button> そのまま表示</li>
<li><ons-button ng-click="jump('page-A-2-2');">pageA-②-①</ons-button> 計算表示</li>
</ul>
</ons-page>
</ons-template>
<ons-template id="pageA-1-1.html">
<ons-page ng-class="contents">
pageA-①-①だよ
</ons-page>
</ons-template>
<ons-template id="pageA-1-2.html">
<ons-page ng-class="contents">
pageA-①-②だよ
</ons-page>
</ons-template>
<ons-template id="pageA-2-1.html">
<ons-page ng-controller="PageA21Controller" ng-class="contents">
pageA-②-①だよ
<p>{{sample}}</p>
</ons-page>
</ons-template>
<ons-template id="pageA-2-2.html">
<ons-page ng-controller="PageA22Controller" ng-class="contents">
pageA-②-②だよ
<p>{{result}}</p>
</ons-page>
</ons-template>
</body>
</html>
javascript
script.js
var myApp = angular.module('MyApp', ['onsen', 'ui.router']);
myApp.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) {
// 初期表示設定
$urlRouterProvider.otherwise("/");
// メイン画面(初期表示)
$stateProvider.state('main', {
url: '/',
templateUrl: 'main.html'
});
// 各画面
$stateProvider
.state('page-A', {
url: '/pageA',
templateUrl: 'pageA.html'
})
.state('page-A-1', {
url: '/pageA-1',
templateUrl: 'pageA-1.html'
})
.state('page-A-1-1', {
url: '/pageA-1-1',
templateUrl: 'pageA-1-1.html'
})
.state('page-A-1-2', {
url: '/pageA-1-2',
templateUrl: 'pageA-1-2.html'
})
.state('page-A-2', {
url: '/pageA-2',
templateUrl: 'pageA-2.html',
// 戻ってきた値を受け取る可能性があるため、params指定
params: {
sample: null
}
})
.state('page-A-2-1', {
url: '/pageA-2-1',
templateUrl: 'pageA-2-1.html',
// 引数を受け取る
params: {
sample: null
}
})
.state('page-A-2-2', {
url: '/pageA-2-2',
templateUrl: 'pageA-2-2.html',
// 引数を受け取る
params: {
sample: null
}
})
// 入れ子
.state('page-A.page-A-sub1', {
url: '/pageA/page-A-sub1',
templateUrl: 'page-A-sub1.html'
})
.state('page-A.page-A-sub2', {
url: '/pageA/page-A-sub2',
templateUrl: 'page-A-sub2.html'
})
;
}]);
// 戻る機能のコントローラの割り当て
myApp.controller('BackController', ['$rootScope', '$scope', '$state', function($rootScope, $scope, $state) {
// ツールバーの戻るアイコン押下時の処理
$scope.backNavi = function() {
// 戻る処理実行
console.log("backNavi");
stateBack($rootScope, $state, null);
};
}]);
// Page2の値受け渡し処理
myApp.controller('PageA2Controller', ['$rootScope', '$scope', '$state', '$stateParams', function($rootScope, $scope, $state, $stateParams) {
// 値が設定されている場合、スコープに再設定する
if ($stateParams.sample) {
$scope.sample = $stateParams.sample;
}
// 画面遷移の処理
$scope.jump = function(to) {
console.log("jump sample="+ $scope.sample);
// パラメータ付で次に渡す
$state.go(to, {"sample": $scope.sample});
};
}]);
// Page2の値受け取り処理①
myApp.controller('PageA21Controller', ['$rootScope', '$scope', '$state', '$stateParams', function($rootScope, $scope, $state, $stateParams) {
// 値が設定されている場合、スコープに再設定する
if ($stateParams.sample) {
$scope.sample = $stateParams.sample;
}
}]);
// Page2の値受け取り処理②
myApp.controller('PageA22Controller', ['$rootScope', '$scope', '$state', '$stateParams', function($rootScope, $scope, $state, $stateParams) {
console.log("sample: "+ $stateParams.sample);
// 値が設定されている場合、スコープに再設定する
if ($stateParams.sample) {
if (isNumber($stateParams.sample)) {
// 数値の場合、計算して表示する
var num = eval($stateParams.sample);
$scope.result = num + " * 2 = "+ (num * 2);
}
else {
// 数値以外の場合、エラーメッセージ
$scope.result = '数値以外が受け渡されました: '+ $stateParams.sample;
}
}
}]);
// 数値/数字文字列チェック
function isNumber(x){
if( typeof(x) != 'number' && typeof(x) != 'string' )
return false;
else
return (x == parseFloat(x) && isFinite(x));
}
// 画面遷移時の処理
myApp.run(['$rootScope', '$transitions', '$state', function($rootScope, $transitions, $state){
// main 画面への遷移(初期表示)
$transitions.onSuccess({to: 'main'}, function(trans){
// main画面読み込み成功
// 画面スタック初期化
$rootScope.pages = [];
});
// main 画面以外への遷移(初期表示以外)
$transitions.onBefore({to: function(state){ return state != 'main' }}, function(trans){
// main以外の場合
console.log("$transitions.onBefore: "+ trans.$from() +" -> "+ trans.$to() +" / "+ trans.params());
if (trans.$from() != trans.$to()) {
// 遷移情報(from→toの両方+パラメータを保持)を画面スタック
$rootScope.pages.push(trans);
// 遷移HTMLをnavigatorに追加する
$rootScope.navi.pushPage(trans.$to().self.templateUrl);
}
});
// Android の戻るキー押下時処理
document.addEventListener("backbutton", function(e){
// 戻る処理実行(イベントを引き継ぐ)
console.log("backbutton");
stateBack($rootScope, $state, e);
}, false);
}]);
// 戻る処理
var stateBack = function($rootScope, $state, e) {
console.log("--------------------------");
console.log("stateBack: $root="+ $rootScope +", $state="+ $state + ", e="+ e);
if (e != null) {
// イベントがある場合は一旦止める
e.preventDefault();
}
if ($rootScope.pages.length < 1) {
// キューがない場合、アプリを終了するか確認する
ons.notification.confirm({
title: "終了確認",
message: "アプリを終了させてもよろしいですか?",
callback: function(answer) {
if (answer) {
navigator.app.exitApp();
}
}
});
return;
}
// 現在のステート
console.log("current: "+ $state.current);
console.log("current.name: "+ $state.current.name);
console.log("current.url: "+ $state.current.url);
console.log("$rootScope.pages.length: "+ $rootScope.pages.length);
console.log("* * * *");
// 最新の遷移情報(from→toの両方を保持)を取得する
var nowPage = $rootScope.pages[$rootScope.pages.length - 1];
// 画面スタックをポップする
$rootScope.pages.pop();
console.log("* back: "+ $state.current.name + " => "+ nowPage);
console.log(nowPage.$from().name);
console.log(nowPage.params());
// 最新の遷移情報のfromを元に、戻る方向(前画面)の遷移を実行する
// パラメータもそのまま再設定する
$state.go(nowPage.$from().name, nowPage.params());
// 遷移して追加されたので除去しておく
$rootScope.pages.pop();
// 戻る処理実行後のステート
console.log("current: "+ $state.current);
console.log("current.name: "+ $state.current.name);
console.log("current.url: "+ $state.current.url);
console.log("$rootScope.pages.length: "+ $rootScope.pages.length);
}
style.css
/** 実際には使わないので、navigatorを非表示にしておく。 */
ons-navigator {
display: none;
}
/** コンテンツのスタイル */
div.page__content {
padding: 20px 10px;
}
処理の流れ
- アプリを起動する
-
$urlRouterProvider.otherwise("/")
で、初期表示URL"/"
に遷移する
3.state=main
を表示する
4.state=main
の表示成功時($transitions.onSuccess({to: 'main'} (略)
);)に、画面スタック($rootScope.pages
)を初期化する
5.state=main
の表示が完了する -
pageA
ボタンを押下する
7.state=page-A
への遷移を開始する
8.state=page-A
への遷移を開始する前($transitions.onBefore (略)
);)に、遷移元と遷移先が異なる(trans.$from() != trans.$to()
)場合、遷移情報を画面スタックにpush($rootScope.pages.push(trans)
)する
9. 同様に、ons-navigator
に遷移先HTMLをpushPage
する ※これでAndroidのBackボタンでも閉じなくなる -
pageA
を表示する
10.ons-back-button
タグで戻るボタン表示判定
11.$rootScope.pages
に1件以上遷移情報が入っている状態であるため、戻るボタン表示
12.ons-navigator
は、cssでdisplay: none;
としているため、HTMLが設定されていても非表示のまま。 - ツールバーの戻るボタンを押下する
12.ons-back-button
タグより、$scope.backNavi
を実行し、stateBack
を実行する
13.$rootScope.pages[$rootScope.pages.length - 1]
で、直近の遷移情報を取得する
14.$rootScope.pages
をpop
する
15. 遷移情報から$from
より遷移元名称と、params
から受け渡しパラメータを元に、元の画面に通常遷移させる
16. 遷移させると、遷移情報が$rootScope.pages
にpush
されるため、それをpop
して戻る遷移を保持させない
17.state=main
を表示する - ハードの戻るボタンを押下する(Android限定)
19.document.addEventListener
で、backbutton
(戻るボタン)の機能をフックする
20. 以降は、5と同様、stateBack
を実行する - 画面間の値の受け渡し方法
22.$stateProvider
の遷移元と遷移先に、受け渡すパラメータをparams
として登録する
23. 画面遷移時に、パラメータ付で遷移させる。($state.go(to, {"sample": $scope.sample})
)
24. 遷移先画面のコントローラで、$stateParams
から値を取得する
25. 遷移元にも戻った場合に、値を画面上に表示する場合、$stateParams
から値を取得する
ソースコード
いいわけ…
このソースはとてもウツクシクないです。
でも、今の自分ではこれが精いっぱいでした。
もっと良い方法をご存じの方、どうぞご指摘・ご教授ください m(__;)m
Monaca UG TOKYO #3 へのお土産として、やっと投稿できましたw