RESTEasyとJPAとAngularJSを使ってWebアプリを作ってみる８（画面）

やっと、クライアント側の作成に入ります。

# ライブラリ参照
とりあえず、CDNを参照するようにしていきます。

デザインはいまいちなので、見た目を多少まともに見せるために、bootstrap を使うことにします。
ルーティング機能とダイアログ表示を使うので angular-route と ui-bootstrap も使用します。

```html:src/main/webapp/index.html
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular-route.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.14.3/ui-bootstrap-tpls.min.js"></script>
```

# Angular モジュールの作成
[angular.module](http://js.studio-kingdom.com/angularjs/ng_global_apis/angular_module) でモジュールを作成します。

```js:src/main/webapp/script/app.js
var app = angular.module('HomeworkApp', ['ngRoute', 'ui.bootstrap']);
```
モジュール名は「HomeworkApp」としました。
ルーティングとダイアログを使うので、requires に ['ngRoute', 'ui.bootstrap'] を指定しています。

このモジュールを使用するため、html では [ng-app](http://js.studio-kingdom.com/angularjs/ng_directive/ng_app) を指定します。

```html:src/main/webapp/index.html
<html ng-app="HomeworkApp">
```

# ルートの設定
ログインページ（login）⇒トップページ（top）⇒問題ページ（question）⇒解答ページ（answer）という流れにするので、それぞれのページでどの画面を表示するかを [$routeProvider](http://js.studio-kingdom.com/angularjs/ngroute_service/$route_provider) で設定します。

```js:src/main/webapp/script/app.js
app.config(['$routeProvider',
	function($routeProvider) {
		$routeProvider
			.when('/login', {
				templateUrl: 'view/login.html',
				controller: 'InitController'
			})
			.when('/top', {
				templateUrl: 'view/top.html',
				controller: 'QuestionController'
			})
			.when('/question', {
				templateUrl: 'view/question.html',
				controller: 'AnswerController'
			})
			.when('/answer', {
				templateUrl: 'view/answer.html',
				controller: 'EndController'
			})
			.otherwise({
				redirectTo: '/login'
			});
	}
]);
```
angular.module で取得した app の config メソッドで設定します。

templateUrl を表示するエリアを [ng-view](http://js.studio-kingdom.com/angularjs/ngroute_directive/ng_view) で html に指定します。

```html:src/main/webapp/index.html
<div class="container-fluid">
	<div class="row">
		<div class="col-sm-3"></div>
		<div class="col-sm-6">
			<div ng-view></div>
		</div>
		<div class="col-sm-3"></div>
	</div>
</div>
```
とりあえず、センタリングして半分くらいのエリアに表示すればいいかなぁというレベルで指定しています。 

# ログイン画面
ユーザ（アカウント）IDとパスワードの入力エリアとログインボタンのみのシンプルな画面です。
/login の時、上記の ng-view の部分に、下記の login.html が表示されます。

```html:src/main/webapp/view/login.html
<div class="panel panel-default">
	<div class="panel-heading">ログイン</div>
	<div class="panel-body">
		<form name="loginForm" ng-submit="login()" novalidate>
			<div class="form-group" ng-class="{'has-error': loginForm.userId.$invalid}">
				<input type="text" name="userId" class="form-control" placeholder="ユーザＩＤ　※必須" autocomplete="off"
					ng-model="userId" ng-minlength="4" ng-maxlength="32" required />
				<span class="text-danger" ng-show="loginForm.userId.$error.minlength" ng-if="loginForm.userId.$invalid">ユーザーIDは4文字以上で入力してください。</span>
				<span class="text-danger" ng-show="loginForm.userId.$error.maxlength" ng-if="loginForm.userId.$invalid">ユーザーIDは32文字以内で入力してください。</span>
			</div>
			<br />
			<div class="form-group" ng-class="{'has-error': loginForm.passWd.$invalid}">
				<input type="password" name="passWd" class="form-control" placeholder="パスワード　※必須"
					ng-model="passWd" ng-minlength="4" ng-maxlength="16" required />
				<span class="text-danger" ng-show="loginForm.passWd.$error.minlength" ng-if="loginForm.passWd.$invalid">パスワードは4文字以上で入力してください。</span>
				<span class="text-danger" ng-show="loginForm.passWd.$error.maxlength" ng-if="loginForm.passWd.$invalid">パスワードは16文字以内で入力してください。</span>
			</div>
			<br />
			<input type="submit" value="ログイン" class="btn btn-primary btn-block" ng-disabled="loginForm.$invalid" />
		</form>
	</div>
</div>
```
- form タグの novalidate は html5 の検証を抑制するものらしいです。

## 入力チェック
- required は必須入力
- ng-minlength は最低の文字数
- ng-maxlength は最大の文字数

form-group にはそれぞれ ng-class="{'has-error': loginForm.userId.$invalid}" を指定しています。
[ng-class](http://js.studio-kingdom.com/angularjs/ng_directive/ng_class) は適用するかどうかを条件で指定できるらしいです。
loginForm.userId が $invalid（不正な値）の時に has-error クラスが適用されることになります。
必須入力項目なので初期表示時はクラスが適用されて赤枠となります。

input タグの下の span で指定しているのは、最小、最大文字数のエラーがあった場合は、エラーメッセージを表示するという内容です。
[ng-show](http://js.studio-kingdom.com/angularjs/ng_directive/ng_show) と [ng-if](http://js.studio-kingdom.com/angularjs/ng_directive/ng_if) で制御しています。

ログインボタンの [ng-disabled](http://js.studio-kingdom.com/angularjs/ng_directive/ng_disabled) はエラーがある場合はクリックできないようにしています。

内容については [入力フォームに検証機能を実装するには？（form／input）](http://www.buildinsider.net/web/angularjstips/0014) というページがすごく分かりやすかったので、こっちを参照したほうが早いと思います。

ちなみに僕は、最初 name 属性を指定していなくて、ちょっとハマリました。


# 画面間のデータ受け渡し
正直よくわからなかったので、AppData というオブジェクト経由でやり取りすることにします。

```js:src/main/webapp/script/utils.js
app.factory('AppData', function() {
	return {
		user: null,
		isLogin: function() {
			return (this.user != null);
		},
		values: {}
	};
});
```
user オブジェクトはログインしたかどうかを判定するために特別扱いして、通常のデータは values に突っ込むという方式にしました。

# メッセージダイアログ
認証エラーの場合はモーダルダイアログでメッセージを表示しようと思うので、[$uibModal](https://angular-ui.github.io/bootstrap/) を使用します。アイコンを表示したいので MessageBox というオブジェクトを用意することにしました。

```js:src/main/webapp/script/utils.js
app.factory('MessageBox', function($uibModal) {
	return {
		info: function($scope, message) {
			var dialogInfo = {
				type: "info",
				title: "情報メッセージ",
				message: message,
				icon: "image/info.png"
			};
			$scope.dialogInfo = dialogInfo;
			$uibModal.open({templateUrl: "view/message.html", scope: $scope});
		},
		warn: function($scope, message) {
			var dialogInfo = {
				type: "warn",
				title: "警告メッセージ",
				message: message,
				icon: "image/alert.png"
			};
			$scope.dialogInfo = dialogInfo;
			$uibModal.open({templateUrl: "view/message.html", scope: $scope});
		},
		error: function($scope, message) {
			var dialogInfo = {
				type: "error",
				title: "エラーメッセージ",
				message: message,
				icon: "image/error.png"
			};
			$scope.dialogInfo = dialogInfo;
			$uibModal.open({templateUrl: "view/message.html", scope: $scope});
		}
	};
});
```

# 未ログインの対応
ログインしていなくても login 以外のページが表示されると、ユーザIDを知らないため、必ずエラーが発生してしまうので、[$rootScope.$on](http://js.studio-kingdom.com/angularjs/ng_types/scope) で [$routeChangeStart](http://js.studio-kingdom.com/angularjs/ngroute_service/$route#6) を設定します。

```js:src/main/webapp/script/app.js
app.run(['$rootScope', '$location', 'AppData',
	function($rootScope, $location, AppData) {
		$rootScope.$on('$routeChangeStart', function(event, next, current) {
			if (next.controller != 'InitController' && !AppData.isLogin()) {
				event.preventDefault();
				$location.path('/login');
			}
		});
	}
]);
```
ログイン処理を担当する InitController 以外で未ログインの場合は、イベントをキャンセルして /login へ遷移させます。


[RESTEasyとJPAとAngularJSを使ってWebアプリを作ってみる９（リクエスト）](http://qiita.com/1roh/items/10a00656327e01384bbb)へ