【旧版】ExcelからElixirマスター⑦：Vue.js＋内部API (更新編)

【本コラムは、5分で読めて、15分くらいでお試しいただけます】
[piacere](https://twitter.com/piacere_ex)です、ご覧いただいてありがとございます :bow:

[前回](https://qiita.com/piacerex/items/50d847170291c41fef64)は、Vue.jsからPhoenix内部APIを呼び出し、Web表示しました

今回は、Web入力をできるようにし、APIによるデータ更新を行う … いわゆるSPA（Single Page Application）を実装します

**■「ExcelからElixirマスター」シリーズの目次**
[**①データ並替え／絞り込み**](https://qiita.com/piacere_ex/items/6714e1440e3f25fb46a1)
|> [**②データ列抽出、Web表示**](https://qiita.com/piacere_ex/items/b7787580fce5f148242f)
|> [<b>③WebにDBデータ表示</b>](https://qiita.com/piacere_ex/items/a7558adc6856e3577dc6)
|> [<b>④Webに外部APIデータ表示</b>](https://qiita.com/piacere_ex/items/4c212615a4eb699dd109)
|> [<b>⑤Webにグラフ表示</b>](https://qiita.com/piacere_ex/items/290b76b76d5ff8e019bf)
|> [<b>⑥Vue.js＋内部API（表示編）</b>](https://qiita.com/piacere_ex/items/50d847170291c41fef64)
|> [<b>⑦Vue.js＋内部API（更新編）</b>](https://qiita.com/piacerex/items/7cd1162ce6d66a334a07)
|> [<b>⑧Gigalixirに本番リリース</b>](https://qiita.com/piacerex/items/1a9cbcc740ca3707eaec)
|> [**⑨Elixir／PhoenixのCRUD Webアプリをリリース**](https://qiita.com/piacerex/items/511f24888c7350639d0b)
|> [**⑩「LiveView」ElixirサーバサイドのみでReact的SPA／リアルタイムUIが作れる**](https://qiita.com/piacerex/items/3f8ee18c9443d63955bf)
|> [**⑪LiveView製Qiita検索SPAをフォームsubmitスタイルに換装**](https://qiita.com/piacerex/items/21b0e308a36e486d8b25)
|> [**⑫LiveViewのコード内HTMLをテンプレートファイルに分離**](https://qiita.com/piacerex/items/5b1aabb0c45bb9934bb9)
|> [**⑬ElixirサーバサイドSPAをスマホで見るためにGigalixirリリース**](https://qiita.com/piacerex/items/583174d72e6fc0cd289e)
|> [**⑭Gigalixir上のLiveViewアプリに独自ドメイン名を付与して正式なアプリ公開**](https://qiita.com/piacerex/items/b61348c282395167fd8c)

# データ入力可能にし、全件更新できるようにする

まずは、現在のデータ表示から作りやすい、データ更新に対応してみましょう

これまでのデータ表示部分を、入力フィールドに変更し、「全件更新」ボタンを追加した上で、データ更新APIを全データ件数分、繰り返すonUpdateメソッドを追加します

```html:lib/vue_sample_web/templates/page/index.html.eex
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>

<div id="app">

<h1>Posts</h1>
<table border="1">
<tr v-for="result in results">
	<td style="padding: 10px;"><input type="text" v-model="result.title"></td>
	<td style="padding: 10px;"><input type="text" v-model="result.body"></td>
</tr>
</table>
<button v-on:click="onUpdate">全件更新</button>

</div>

<script>
	var app = new Vue
	( {
		el: '#app',
		data: 
		{
			results: [], 
		}, 
		mounted()
		{
			axios.get( '/posts' )
			.then( response => { this.results = response.data.data } )
		}, 
		methods: 
		{
			onUpdate: async function( evt )
			{
				this.results.forEach( ( result, i ) => 
				{
					axios.put( '/posts/' + result.id, 
						{ 
							'post':
							{
								'title': result.title, 
								'body':  result.body
							} 
						} )
				} )
			}, 
		}, 
	} )
</script>
```

## 「v-model」を使い、入力データをVue.js側に反映

コードの解説ですが、入力フィールドで変更されたデータをVue.js側に反映するには、「v-model」という属性を使います

v-modelの中に書くのは、{{～}}で表示をしたのと同様、Vue.jsの「data:」部で定義したモデルとなります（実際には、v-forでバラされたモデル断片）

なお、v-modelは、HTMLからVue.jsへの一方通行の反映では無く、Vue.jsからHTMLへの反映も兼ねているので、Vue.js側で「data:」部のモデルを書き換えると、画面表示に自動反映されます（双方向バインディングと呼ばれます）

mountedでこれが使われ、初期表示では、APIで取得したデータを画面に反映します

```html:lib/vue_sample_web/templates/page/index.html.eex
…
<tr v-for="result in results">
	<td style="padding: 10px;"><input type="text" v-model="result.title"></td>
	<td style="padding: 10px;"><input type="text" v-model="result.body"></td>
</tr>
…
```

## 「v-on:click」でクリック時のハンドラ呼出

「全件更新」ボタンは、クリックされたときに、onUpdateを呼び出します

「v-on:click」という属性を使うことで、クリック時のハンドラメソッドを指定できます

他にも、「v-on:change」や「v-on:focus」、「v-on:blur」等、JavaScriptで利用可能なハンドラと同様のものをVue.jsでも利用できます

なお、引数を指定することもでき、引数指定無の場合は、クリックイベントが発生したパーツ（今回だとbutton）のDOMが暗黙の引数として渡されます

```html:lib/vue_sample_web/templates/page/index.html.eex
…
<button v-on:click="onUpdate">全件更新</button>
…
```

# 「methods:」部にハンドラを定義

onUpdateの中身は、v-modelで更新されたresultsを全件、PUTメソッドで更新APIを呼び出し続ける処理です

results全件を更新に回す部分は、forEachを使うと、JavaScriptでも、関数型っぽく書けます

第1引数に配列の要素（resultの部分）、第2引数に配列の要素の添字（iの部分）が返りますので、それらを使って全件処理を書けます（今回は、要素のみ利用）

PUTメソッドで送信する内容は、axios.putの第1引数であるURLには、更新対象となるデータのidを指定します

第2引数は、更新内容をJavaScriptオブジェクト形式で書きます

```html:lib/vue_sample_web/templates/page/index.html.eex
…
<script>
…
		methods: 
		{
			onUpdate: async function( evt )
			{
				this.results.forEach( ( result, i ) => 
				{
					axios.put( '/posts/' + result.id, 
						{ 
							'post':
							{
								'title': result.title, 
								'body':  result.body
							} 
						} )
				} )
			}, 
		}, 
…
</script>
…
```

## 画面表示／入力の確認

では、上記ファイルを保存し、以下のような画面がブラウザ表示されることを確認しましょう
![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/34a748e7-54ba-bc01-a317-063bd44b48fa.png)

入力フィールドの値を書き換え、「全件更新」ボタンをクリックすると、APIが呼び出され、データが更新されます

ただし、更新がかかっても、ページ遷移等が無いため、画面上は、何も起きなかったように見えるので、更新後、ブラウザをリロードして、更新したデータが維持されていることを確認してください（もし、更新に失敗していたら、リロードすると、入力したデータが元に戻るはずです）

# データ削除

データ削除は、各データ毎に行えるようにします

「削除」ボタンは、データのidを引数指定して、onDeleteに渡すことで、該当データを削除できるようにします

onDeleteは、DELETEメソッドで削除APIをid指定付きで呼び出し、その後、データ取得し直します

resultsが更新されると、自動的にv-modelで定義したHTML側が表示更新され、削除されたデータが表示されなくなります

ここで、メソッドに「async」、削除API呼出のaxios.deleteに「await」が付いているのは、削除が完全に終わってから、データ取得するよう、「同期処理」にするためです

ここを同期にせず、「非同期処理」で走らせると、データ削除が完了しない状態で、データ取得を行ってしまう可能性があるため、そうならないよう、同期処理化しています

```html:lib/vue_sample_web/templates/page/index.html.eex
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>

<div id="app">

<h1>Posts</h1>
<table border="1">
<tr v-for="result in results">
	<td style="padding: 10px;"><input type="text" v-model="result.title"></td>
	<td style="padding: 10px;"><input type="text" v-model="result.body"></td>
	<td><button v-on:click="onDelete( result.id )">削除</button></td>
</tr>
</table>
<button v-on:click="onUpdate">全件更新</button>

</div>

<script>
	var app = new Vue
	( {
		el: '#app',
		data: 
		{
			results: [], 
		}, 
		mounted()
		{
			axios.get( '/posts' )
			.then( response => { this.results = response.data.data } )
		}, 
		methods: 
		{
			onUpdate: function( evt )
			{
				this.results.forEach( ( result, i ) => 
				{
					axios.put( '/posts/' + result.id, 
						{ 
							'post':
							{
								'title': result.title, 
								'body':  result.body
							} 
						} )
				} )
			}, 
			onDelete: async function( id )
			{
				await axios.delete( '/posts/' + id )

				axios.get( '/posts' )
				.then( response => { this.results = response.data.data } )
			}, 
		}, 
	} )
</script>
```

上記ファイルを保存すると、以下のような画面がブラウザ表示されます
![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/4cfaa8c9-2a2f-1445-77ea-217591765ac2.png)

# データ追加

最後に、データ追加です

「追加」ボタンでonCreateを呼び出しますが、追加用フィールドは、resultsとは別のモデルを用意します

onCreateは、POSTメソッドで追加APIを呼び出し、その後、データ取得し直します（ここも同期処理化しています）

```html:lib/vue_sample_web/templates/page/index.html.eex
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>

<div id="app">

<h1>Posts</h1>
<table border="1">
<tr v-for="result in results">
	<td style="padding: 10px;"><input type="text" v-model="result.title"></td>
	<td style="padding: 10px;"><input type="text" v-model="result.body"></td>
	<td><button v-on:click="onDelete( result.id )">削除</button></td>
</tr>
<tr>
	<td style="padding: 10px;"><input type="text" v-model="new_title"></td>
	<td style="padding: 10px;"><input type="text" v-model="new_body"></td>
	<td><button v-on:click="onCreate">追加</button></td>
</tr>
</table>
<button v-on:click="onUpdate">全件更新</button>

</div>

<script>
	var app = new Vue
	( {
		el: '#app',
		data: 
		{
			results:   [], 
			new_title: '', 
			new_body:  '', 
		}, 
		mounted()
		{
			axios.get( '/posts' )
			.then( response => { this.results = response.data.data } )
		}, 
		methods: 
		{
			onUpdate: function( evt )
			{
				this.results.forEach( ( result, i ) => 
				{
					axios.put( '/posts/' + result.id, 
						{ 
							'post':
							{
								'title': result.title, 
								'body':  result.body, 
							} 
						} )
				} )
			}, 
			onDelete: async function( id )
			{
				await axios.delete( '/posts/' + id )

				axios.get( '/posts' )
				.then( response => { this.results = response.data.data } )
			}, 
			onCreate: async function( evt )
			{
				await axios.post( '/posts/', 
					{ 
						'post':
						{
							'title': this.new_title, 
							'body':  this.new_body, 
						} 
					} )

				this.new_title = ''
				this.new_body  = ''

				axios.get( '/posts' )
				.then( response => { this.results = response.data.data } )
			}, 
		}, 
	} )
</script>
```

上記ファイルを保存すると、以下のような画面がブラウザ表示されます
![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/f97310f8-a3d5-cf01-4899-0259a0613e14.png)


さて、これで、データの追加／更新／削除ができるようになるので、色々遊んでみてください

# 終わり

今回は、Web入力をできるようにし、Vue.js＋内部APIによるデータ更新／削除／追加を実装しました

この内容は、Vue.js＋REST APIによるSPA（Single Page Application）データ操作の基本になりますので、覚えておくと、モダンWebアプリ開発の強力な武器になります

さて、ここまでで、Elixir／Phoenix＋Vue.jsによるSPA開発の基本は終わりです

[次回は、Phoenixアプリを**「Gigalixir」というPaaSにリリース** して、公開するためのやり方を解説したいと思います](https://qiita.com/piacerex/items/1a9cbcc740ca3707eaec)