Vue.jsを触ってみた


# はじめに

Vue.jsを触ってみました．
新領域への挑戦なので間違った部分もあるかもしれません（ご了承ください）

本記事の構成

* はじめに
* Vue.js
* サンプルアプリ
* コード
    * ユーザの検索
        * Vueインスタンスの生成
        * v-bind, v-model
        * v-on
        * コンポーネント
    * todoの状態更新
        * created
        * emit
* おわりに


# Vue.js

公式ページによると

> Vueはユーザーインターフェイスを構築するためのプログレッシブフレームワーク

らしいです．

調べると知らない単語が増える一方なので，
とりあえず触ってみて動いたものをお見せします．


# サンプルアプリ

todoアプリのようなものを作りました．
フレームワークはAmon2を使用しています．
このアプリ（？）では「ユーザの検索」と「todoの状態更新」を行っています．
ちなみに裏ではajax通信によりデータベースの値を更新しています．

**ユーザの検索**

* エンターキーの押下を検出して検索
* 検索結果を受け取ってレンダリング

**todoの状態更新**

* クリックを検出して状態更新
* レコードの削除

<img width="800" alt="todo.gif" src="https://qiita-image-store.s3.amazonaws.com/0/82527/472e2ae0-4d95-3aa6-5642-d06b192ec057.gif">

# コード

`.html`にしていますが，厳密にはText::Xslateというテンプレートエンジンを使用した`.tx`です．
`: cascade`や`:override content`は無視しても構いません．

## ユーザの検索

```search.html
: cascade "include/layout.tx"

: override content -> {

<h3>Search</h3>
<div id="users">
	<div class="container">
		<div class="form-group">
			<label>user name</label>
			<input type="text" class="form-control" v-model="query_key" v-on:keyup.enter="search">
		</div>
		<div>
			<table class="table">
				<thead>
					<tr>
						<th class="col-md-2">id</th>
						<th class="col-md-2">nickname</th>
						<th class="col-md-2">password</th>
					</tr>
				</thead>
				<tbody>
					<tr is="user-row" v-for="user in users" :u="user"></tr>
				</tbody>
			</table>
		</div>
	</div>
</div>

<script type="text/x-template" id="user-row">
	<tr>
		<td>{{ u.id }}</td>
		<td><a v-bind:href="'/user/' + u.id">{{ u.nickname }}</a></td>
		<td>{{ u.password }}</td>
	</tr>
</script>

<script src="<: uri_for('/static/js/search.js') :>"></script>

: }
```

```search.js
Vue.component("user-row", {
	template: "#user-row",
	props: ["u"]
});
new Vue({
	el: "#users",
	data: {
		query_key: "",
		users: []
	},
	methods: {
		search: function() {
			this.users = [];
			this.$http.get("http://localhost:8080/search/users/" + this.query_key).then(response => {
				var _users = response.body;
				for(var key in _users) {
					this.users.push({
						id: _users[key].id,
						nickname: _users[key].nickname,
						password: _users[key].password
					});
				}
			}, response => {
			});
		}
	}
});
```

いくつか重要な点を挙げます．

* Vueインスタンスの生成
* `v-bind`, `v-model` によるデータのバインド
* `v-on` によるイベントに対する処理の設定
* コンポーネントを使用したテンプレートの再利用

### Vueインスタンスの生成

```js
new Vue({
	el: "#users",
	data: {
		query_key: "",
		users: []
	},
	methods: {
		search: function() {
			this.users = [];
			this.$http.get("http://localhost:8080/search/users/" + this.query_key).then(response => {
				var _users = response.body;
				for(var key in _users) {
					this.users.push({
						id: _users[key].id,
						nickname: _users[key].nickname,
						password: _users[key].password
					});
				}
			}, response => {
			});
		}
	}
});
```

**el**
`el` で指定したhtml要素と紐づけることができます．
上記コードでは `<div id="users">` がVueインスタンスと紐づきます．

**data**
`data` でhtmlにバインドする値を保持します．
`data` に保持した値のhtmlへの表示するだけでなく，
フォームから受け取った値を `data` に格納することもできます．
バインド方法は後ほど説明します．

**methods**
エンターキーの押下やクリックをトリガとして発火する挙動を定義します．
上記コードではレスポンスとして得られるユーザデータを `users` に詰め込んでいます．

### v-bind, v-model

```html
<td><a v-bind:href="'/user/' + u.id">{{ u.nickname }}</a></td>
```

`v-bind` は値をバインドします．
`<a href="user/xxx">` の `xxx` の部分に `u.id` を展開するイメージです．

```html
<input type="text" class="form-control" v-model="query_key" v-on:keyup.enter="search">
```

`v-model` は値の双方向バインディングを可能にします．
双方向とは「htmlへの値の表示」と「htmlからの値の取得」を表します．
`v-model="query_key"` によりVueインスタンスの `query_key` と値やりとりできます．
上記コードではフォームに入力した値が `query_key` に格納されます．

### v-on

```html
<input type="text" class="form-control" v-model="query_key" v-on:keyup.enter="search">
```

`v-on` はトリガの種類とトリガを検出した時の処理を設定します．
`v-on:keyup.enter="search"` は `keyup.enter` をトリガとして，
Vueインスタンスの `search` メソッドを実行します．
上記コードではエンターキーが押下されるとユーザ検索のリクエストを投げ，
レスポンスを `users` に詰め込みます．

### コンポーネント

コンポーネントは再利用可能なテンプレートを定義します．
コードを使いながら説明してみます．
（ここは特に理解が浅い部分なのでご注意ください）

```js
Vue.component("user-row", {
    template: "#user-row",
    props: ["u"]
});
```

```html
...
<tbody>
	<tr is="user-row" v-for="user in users" :u="user"></tr>
</tbody>

<script type="text/x-template" id="user-row">
	<tr>
		<td>{{ u.id }}</td>
		<td><a v-bind:href="'/user/' + u.id">{{ u.nickname }}</a></td>
		<td>{{ u.password }}</td>
	</tr>
</script>
```

**テンプレート定義**
`<script type="text/x-template">` でテンプレートを定義します．
上記テンプレートは各ユーザのid, nickname, passwordをカラムに持つ1行を定義しています．

**コンポーネント**
コンポーネントでは `props: ["u"]` により `"u"` という名前で値を受け取れるようになります．

**htmlでの値の受け渡し**

```html
<tr is="user-row" v-for="user in users" :u="user"></tr>
```

ここは順を追って説明します．

まず，`v-for="user in users"` によりVueインスタンスの `users` から値を一つずつ取り出します．
取り出した値を `user` という変数に格納しています．

次に，`:u="user"` により取り出した `user` をコンポーネントの `props: ["u"]` にバインドします．
コンポーネントは `u` で値を保持しているので，先ほど定義したテンプレート内では `u` で値を参照できます．

## todoの状態更新

```user.html
<script>
	var user_id = <: $user.id :>;
</script>

   <h3>User</h3>
<div>
	<table class="table">
		<thead>
			<tr>
				<th class="col-md-2">nickname</th>
				<th class="col-md-2">password</th>
			</tr>
		</thead>
		<tbody>
			<tr>
				<td><: $user.nickname :></td>
				<td><: $user.password :></td>
			</tr>
		</tbody>
	</table>
</div>

   <h3>Todos</h3>
<div id="todos">
	<table class="table">
		<thead>
			<tr>
				<th class="col-md-2">id</th>
				<th class="col-md-2">title</th>
				<th class="col-md-2">progress</th>
				<th class="col-md-2">button</th>
			</tr>
		</thead>
		<tbody>
			<tr is="todo-row" v-for="todo in todos" v-bind:t="todo" v-bind:p="progress_strs" @changed="delete_row"></tr>
		</tbody>
	</table>
</div>

<script type="text/x-template" id="todo-row">
	<tr>
		<td>{{ t.id }}</td>
		<td v-bind:class="t.progress_str">{{ t.title }}</td>
		<td>{{ t.progress_str }}</td>
		<td>
			<button class="btn btn-default btn-sm" v-on:click="update_progress(0)">NotYet</button>
			<button class="btn btn-default btn-sm" v-on:click="update_progress(1)">Doing</button>
			<button class="btn btn-default btn-sm" v-on:click="update_progress(2)">Done</button>
			: if $session_user_id == $user.id {
				<button class="btn btn-default btn-sm" v-on:click="delete_record">Delete</button>
			: }
		</td>
	</tr>
</script>
```

```todo.js
Vue.component("todo-row", {
	template: "#todo-row",
	props: {
		t: Object,
		p: Array
	},
	methods: {
		update_progress: function(progress) {
			this.$http.post("http://localhost:8080/todo/" + this.t.id + "/update?progress=" + progress).then(response => {
				this.t.progress_str = this.p[response.body.progress];
			}, response => {
			});
		},
		delete_record: function() {
			this.$http.post("http://localhost:8080/todo/" + this.t.id + "/delete").then(response => {
				this.$emit("changed", this.t);
			}, response => {
			});
		}
	}
});
new Vue({
	el: "#todos",
	data: {
		todos: [],
		progress_strs: ["notyet", "doing", "done"]
	},
	created: function() {
		this.$http.post("http://localhost:8080/user/" + user_id + "/todos").then(response => {
			var _todos = response.body;
			for(var key in _todos) {
				this.todos.push({
					id: _todos[key].id,
					title: _todos[key].title,
					progress: _todos[key].progress,
					progress_str: this.progress_strs[_todos[key].progress]
				});
			}
		}, response => {
		});
	},
	methods: {
		delete_row: function(todo) {
			var index = this.todos.indexOf(todo);
			this.todos.splice(index, 1);
		}
	}
});
```

コードの分量は多いですが，ユーザ検索と重複部分が多いので説明する箇所は以下の点です．

* `created` によるVueインスタンス生成後の処理
* `emit` によるコンポーネントからのイベント伝播

### created

`methods` ではイベント発生時の処理を定義しました．
`created` はVueインスタンス生成後の処理を定義します．
上記コードではレスポンスとして得られるtodoリストを `todos` に詰め込んでいます．

### emit

このアプリではDeleteボタンを押した時にその行が消えるようになっています．
コンポーネントでボタン押下のイベントを検出していますが，
`todos` 自体はVueインスタンスで管理しているためイベントを伝播させる必要があります．

このイベント伝播を説明するため，関係するコード片を集めて以下にまとめます．

```html
<tbody>
	<tr is="todo-row" v-for="todo in todos" v-bind:t="todo" v-bind:p="progress_strs" @changed="delete_row"></tr>
</tbody>

<script type="text/x-template" id="todo-row">
	<tr>
		...
		<td>
			...
			<button class="btn btn-default btn-sm" v-on:click="delete_record">Delete</button>
		</td>
	</tr>
</script>
```

```js
Vue.component("todo-row", {
	...
	methods: {
		...
		delete_record: function() {
			this.$http.post("http://localhost:8080/todo/" + this.t.id + "/delete").then(response => {
				this.$emit("changed", this.t);
			}, response => {
			});
		}
	}
});
new Vue({
	...
	methods: {
		delete_row: function(todo) {
			var index = this.todos.indexOf(todo);
			this.todos.splice(index, 1);
		}
	}
});
```

**コンポーネントへ処理の登録**

```html
<tr is="todo-row" ... @changed="delete_row"></tr>
```

コンポーネントに `changed` という名前で `delete_row` メソッドを登録しておきます．
`delete_row` は引数として受け取ったtodoを `todos` から削除する処理です．

**登録した処理の実行**

```js
delete_record: function() {
	this.$http.post("http://localhost:8080/todo/" + this.t.id + "/delete").then(response => {
		this.$emit("changed", this.t);
	}, response => {
	});
}
```

`v-on:click="delete_record"` でクリックをトリガとして `delete_record` が動作します．
`this.$emit` により `changed` が発火します．第二引数の `this.t` は `changed` に渡されます．


# おわりに

イベントに応じた処理の実装や再利用可能なテンプレートの定義など
面倒そうなものを簡単に書くことができて便利でした．
また，[公式ページ](https://vuejs.org) のドキュメントも充実しているため，
jsをほとんど触ったことのない私でもそれなりにすぐ動かせました．
Vue.jsを使って何か面白いアプリ作ってみたいです．