[monaca][ui-router]Back!Back!Back!?

ui-routerには、デフォルトでは戻る機能はないので、onsenuiと組み合わせて、それっぽく実装してみました。

# 特徴

Monaca + ui-routerだけですと、AndroidのBackボタンでアプリが閉じてしまいます。
そのため、onsenuiの```ons-navigator```タグを使って、ページ情報をスタックしていきます。
実際の画面スタック＆ポップ処理は、ui-routerの遷移情報を蓄積して、それを元に行き来します。

# ベース

[[monaca][onsenui][AngularJS][ui-router]onsenui/ui-router最小限プロジェクト](https://qiita.com/miz21358/items/af9e6416caa9754c43f5)

# 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">
                <!-- 何も記載しない場合は、空文字を入れる。（入れないとセンタリングレイアウトがずれる） -->
                &nbsp;
            </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;
}
```

# 処理の流れ

1. アプリを起動する
2. ```$urlRouterProvider.otherwise("/")```で、初期表示URL```"/"```に遷移する
    3. ```state=main```を表示する
    4. ```state=main```の表示成功時(```$transitions.onSuccess({to: 'main'} (略)```);)に、画面スタック(```$rootScope.pages```)を初期化する
    5. ```state=main```の表示が完了する
6. ```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ボタンでも閉じなくなる
9. ```pageA```を表示する
    10. ```ons-back-button```タグで戻るボタン表示判定
        11. ```$rootScope.pages```に1件以上遷移情報が入っている状態であるため、戻るボタン表示
    12. ```ons-navigator```は、cssで```display: none;```としているため、HTMLが設定されていても非表示のまま。
11. ツールバーの戻るボタンを押下する
    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```を表示する
18. ハードの戻るボタンを押下する(Android限定)
    19. ```document.addEventListener```で、```backbutton```（戻るボタン）の機能をフックする
    20. 以降は、5と同様、```stateBack```を実行する
21. 画面間の値の受け渡し方法
    22. ```$stateProvider```の遷移元と遷移先に、受け渡すパラメータを```params```として登録する
    23. 画面遷移時に、パラメータ付で遷移させる。(```$state.go(to, {"sample": $scope.sample})```)
    24. 遷移先画面のコントローラで、```$stateParams```から値を取得する
    25. 遷移元にも戻った場合に、値を画面上に表示する場合、```$stateParams```から値を取得する

# ソースコード

https://github.com/miz21358/ui-router-back-sample

# いいわけ…

このソースはとてもウツクシクないです。
でも、今の自分ではこれが精いっぱいでした。
もっと良い方法をご存じの方、どうぞご指摘・ご教授ください m(__;)m

Monaca UG TOKYO #3 へのお土産として、やっと投稿できましたｗ