【本コラムは、10分で読めて、20分くらいでお試しいただけます】
piacereです、ご覧いただいてありがとございます
前回は、Vue.jsからPhoenix内部APIを呼び出し、Web表示しました
今回は、Web入力をできるようにし、APIによるデータ更新を行う … いわゆるSPA(Single Page Application)を実装します
なお、本コラムと全く同じ内容のVue.js版もあります
LiveView版
https://qiita.com/piacerex/items/78ee0d2ad8cca0927980
■「ExcelからElixir入門」シリーズの目次
①データ並替え/絞り込み
|> ②データ列抽出、Web表示
|> ③WebにDBデータ表示
|> ④Webに外部APIデータ表示
|> ⑤Webにグラフ表示
|> ⑥SPAからPhoenix製APIを呼び出す(表示編)【Vue.js版】
|> ⑦SPAからPhoenix製APIを呼び出す(更新編)【Vue.js版】
|> ⑧Gigalixirに本番リリース
|> ⑨「LiveView」ElixirサーバサイドのみでReact的SPA/リアルタイムUIが作れる
|> ⑩ElixirサーバサイドSPAをスマホで見るためにGigalixirリリース
|> ⑪Gigalixir上のLiveViewアプリに独自ドメイン名を付与して正式なアプリ公開
|> ⑫Elixir/PhoenixのCRUD Webアプリをリリース
Elixir Advent Calendar: 言語カテゴリ1位 & 全カテゴリ2位!
例年を遥かに超える盛り上がりを見せ、堂々のトップ獲得ッ!
https://qiita.com/advent-calendar/2022/elixir
https://qiita.com/advent-calendar/2022/ranking/feedbacks
https://qiita.com/advent-calendar/2022/ranking/feedbacks/categories/programming_languages
データ追加APIをSPAから呼べるようにする
最初は、前回作ったデータ追加APIを使って、Web上からデータ追加できるようにしましょう
なお、以前のバージョンのコラムや、このシリーズの第3回までは、DbMnesia
/Db
モジュールをコラム中で実装していましたが、入門編を超えた範囲になるため、「Sqlex」としてOSS化しました … これをインストールして済ませます
mix.exsの def deps do
配下の :phoenix
の直上に追記します
defmodule Basic.Mixfile do
use Mix.Project
…
defp deps do
[
{:req, "~> 0.3"},
+ {:sqlex, "~> 0.1.0"},
{:phoenix, "~> 1.6.15"},
…
]
end
…
PhoenixをCtrl+C2回で止めて、ライブラリを取得(要ネット接続)し、Phoenixを起動します
mix deps.get
iex -S mix phx.server
データ追加UIを設置し、入力したデータでデータ追加APIを呼びます
前回、作ったHTMLに、データ追加UI HTML(入力フィールド群と「追加」ボタン)とデータ追加UIで入力されるデータを保持する add
変数、そしてデータ追加APIを呼び出すハンドラ onCreate
を追加します(Vue2でも変更箇所は同じなので割愛)
コード内容の詳細は、この後、解説します
<script src="https://cdn.jsdelivr.net/npm/vue@3.2/dist/vue.global.js"></script>
<script type="importmap">
{
"imports": {
"vue": "https://cdn.jsdelivr.net/npm/vue@3.2/dist/vue.esm-browser.prod.js"
}
}
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/1.2.2/axios.min.js"></script>
<div id="app">
<h1>Members</h1>
<table>
<tr>
<th>id</th>
<th>name</th>
<th>age</th>
<th>team</th>
<th>position</th>
</tr>
<tr v-for="n in members">
<td>{{n.id}}</td>
<td>{{n.name}}</td>
<td>{{n.age}}</td>
<td>{{n.team}}</td>
<td>{{n.position}}</td>
</tr>
+ <tr>
+ <td>-</td>
+ <td><input type="text" v-model="add.name"></td>
+ <td><input type="text" v-model="add.age"></td>
+ <td><input type="text" v-model="add.team"></td>
+ <td><input type="text" v-model="add.position"></td>
+ <td><button v-on:click="onCreate">追加</button></td>
+ </tr>
</table>
</div>
<script type="module">
import {createApp} from 'vue'
createApp
({
data()
{
return {
members: [],
+ add: {name: '', age: '', team: '', position: ''},
}
},
mounted(){
this.get()
},
methods:
{
get: function() {
axios.get('/members').then(response => {this.members = response.data})
},
+ onCreate: async function(e)
+ {
+ await axios.post('/members',
+ {
+ 'name': this.add.name,
+ 'age': this.add.age,
+ 'team': this.add.team,
+ 'position': this.add.position,
+ })
+
+ this.add = {name: '', age: '', team: '', position: ''}
+
+ axios.get('/members')
+ .then(response => {this.members = response.data})
+ },
},
}).mount('#app')
</script>
「v-model」で入力データをVue.js側に連携する
入力フィールドに入れられたデータをVue.js側に反映するには、v-model
という属性を使います
v-model
の中に、Vue.jsの data
部で定義した変数を置くと、入力都度、リアルタイムに入力内容が変数に連携されるようになります
なお v-model
は、HTMLからVue.jsへの一方通行の反映では無く、Vue.jsからHTMLへの反映も兼ねているので、Vue.js側で data
部のモデルを書き換えると、HTMLに自動反映され、画面表示が更新されます(双方向バインディングと呼ばれます)
この v-model
の機能を使って、ロジックを実行してもページ遷移をさせずに画面内の一部変更で済ませるSPAを実現します(これは後述のデータ更新で改めて説明します)
データ追加APIを呼ぶハンドラ
methods:
内に定義されたハンドラ onCreate
は、v-model
で連携された add
のデータを、axios.post
によるPOSTメソッドでデータ追加APIに送り出します
axios.post
の第1引数にURLを指定し、第2引数に入力データのJSON形式を指定します
ここで、メソッドに async
、API呼出の axios.post
に await
が付いているのは、データ追加が完全に終わってから、データ取得するような「同期処理」にするためです
ここを同期にせず、「非同期処理」で走らせると、データ追加が完了しない状態で、データ取得を行ってしまうため(これはJavaScriptの実行が非同期処理の影響)、そうならないよう同期処理化しています
「v-on:click」でクリック時のハンドラ呼出
「追加」ボタンは、クリックされたときに、v-on:click
という属性を使うことで、クリック時のハンドラメソッドを指定できるので、これを使って onCreate
を呼び出します
他にも、v-on:change
や v-on:focus
、v-on:blur
等、JavaScriptで利用可能なハンドラと同様のものをVue.jsでも利用できます
なお、v-on:click
で指定するハンドラには、下記のように引数を指定することもできますが、今回のように引数指定無の場合は、イベント情報が暗黙の引数として渡されます
…
<td><button v-on:click="onCreate('ダミー1', 'ダミー2')">追加</button></td>
…
イベント情報は、下記の通りです(ハンドラ内で console.log
すれば見れます)
Web上からデータを追加し、Web上で確認
では、Web上からデータ追加してみましょう
藤井名人の次は、「2100年の科学ライフ」等で有名な理論物理学者、ミチオ・カク博士にjoinしてもらうとしましょうw
「追加」ボタンをクリックすると、ミチオ・カク博士がチームにjoinしました … 素晴らしいッ
ここで気付いていただきたいのは、「追加」ボタンをクリックしても、データ表示は増えたのに、ページ遷移が起こらなかったことです … これこそがSPAの威力です
なお、REST APIクライアントからでも、追加されたデータが確認できます
データ更新APIとデータ削除APIを実装する
次は、データ更新APIとデータ削除APIを作りましょう
MemberController
モジュールに update
と delete
を追加します
update
は、members
へのupdate文を Db.query
に渡し、その後、ステータスコードとして 200 OK
を返却します
delete
は、members
へのdelete文を Db.query
に渡し、その後、ステータスコードとして 204 No Content
を返却します
なお、ルーティングは resources
で一括で作られているので、ここでの追加は不要です
defmodule BasicWeb.MemberController do
use BasicWeb, :controller
def index(conn, _p) do
conn
|> json(
"select * from members"
|> Db.query
|> Db.columns_rows
|> Enum.sort(fn current, next -> current["id"] < next["id"] end)
)
end
def create(conn, p) do
"insert into members values('#{p["name"]}', #{p["age"]}, '#{p["team"]}', '#{p["position"]}')"
|> Db.query
send_resp(conn, :created, "")
end
+ def update(conn, p) do
+ "update members set name = '#{p["name"]}', age = #{p["age"]}, team = '#{p["team"]}', position = '#{p["position"]}' where id = #{p["id"]}"
+ |> Db.query()
+ send_resp(conn, :ok, "")
+ end
+ def delete(conn, p) do
+ "delete from members where id = #{p["id"]}"
+ |> Db.query
+ send_resp(conn, :no_content, "")
+ end
end
データ更新APIを叩いた後、Web上で確認
APIでデータ更新してみましょう
「POST」を「PUT」に変更し、URLを localhost:4000/members/3
に変更した上で、下記内容をbodyに入力して、「Send」ボタンをクリックしてみましょう
{
"name": "たくと",
"age": 40,
"team": "カラビナテクノロジー株式会社",
"position": "リードエンジニア、プロジェクトマネージャ"
}
データ削除APIを叩いた後、Web上で確認
APIでデータ削除してみましょう
「PUT」を「DELETE」に変更し、URLを localhost:4000/members/6
に変更した上で、「Send」ボタンをクリックしてみましょう
Web上で確認すると、ミチオ・カク博士が卒業されたことが確認できました
データ更新APIをSPAから呼べるようにする
次は、Web上からデータ更新できるようにしましょう
データ表示部分を、入力フィールドに全て変更し、「全件更新」ボタンをクリックしたら、全データに対してデータ更新API呼出を繰り返すことで更新します
HTMLに、データ更新UI HTML(データ表示部分を入力フィールドに差し替え、「全件更新」ボタン追加)を追加し、データ更新APIを全データ件数分だけ繰り返すハンドラ onUpdate
を追加します(Vue2でも変更箇所は同じなので割愛)
コード内容の詳細は、この後、解説します
<script src="https://cdn.jsdelivr.net/npm/vue@3.2/dist/vue.global.js"></script>
<script type="importmap">
{
"imports": {
"vue": "https://cdn.jsdelivr.net/npm/vue@3.2/dist/vue.esm-browser.prod.js"
}
}
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/1.2.2/axios.min.js"></script>
<div id="app">
<h1>Members</h1>
<table>
<tr>
<th>id</th>
<th>name</th>
<th>age</th>
<th>team</th>
<th>position</th>
</tr>
<tr v-for="n in members">
<td>{{n.id}}</td>
+ <td><input type="text" v-model="n.name"></td>
+ <td><input type="text" v-model="n.age"></td>
+ <td><input type="text" v-model="n.team"></td>
+ <td><input type="text" v-model="n.position"></td>
</tr>
+ <tr>
+ <td colspan="6"><button v-on:click="onUpdate">全件更新</button></td>
+ </tr>
<tr>
<td>-</td>
<td><input type="text" v-model="add.name"></td>
<td><input type="text" v-model="add.age"></td>
<td><input type="text" v-model="add.team"></td>
<td><input type="text" v-model="add.position"></td>
<td><button v-on:click="onCreate">追加</button></td>
</tr>
</table>
</div>
<script type="module">
import {createApp} from 'vue'
createApp
({
data()
{
return {
members: [],
add: {name: '', age: '', team: '', position: ''},
}
},
mounted(){
this.get()
},
methods:
{
get: function() {
axios.get('/members').then(response => {this.members = response.data})
},
onCreate: async function(e)
{
await axios.post('/members',
{
'name': this.add.name,
'age': this.add.age,
'team': this.add.team,
'position': this.add.position,
})
this.add = {name: '', age: '', team: '', position: ''}
axios.get('/members')
.then(response => {this.members = response.data})
},
+ onUpdate: async function(e)
+ {
+ for (const n of this.members)
+ {
+ await axios.put('/members/' + n.id,
+ {
+ 'name': n.name,
+ 'age': n.age,
+ 'team': n.team,
+ 'position': n.position,
+ })
+ }
+
+ axios.get('/members')
+ .then(response => {this.members = response.data})
+ },
},
}).mount('#app')
</script>
HTMLからLiveViewモジュールへの複数件データ連携
「全件更新」ボタンをクリックしたときに、下記コードがどのようなデータが index_live.ex
に連携されるか見ていきましょう
…
<tr v-for="n in members">
<td>{{n.id}}</td>
+ <td><input type="text" v-model="n.name"></td>
+ <td><input type="text" v-model="n.age"></td>
+ <td><input type="text" v-model="n.team"></td>
+ <td><input type="text" v-model="n.position"></td>
</tr>
+ <tr>
+ <td colspan="6"><button v-on:click="onUpdate">全件更新</button></td>
+ </tr>
…
上記コードは、入力された全データを入力都度 v-model
属性で拾い、deta()
部で定義された members
変数に反映します(ここがVue.jsの凄いところ)
そして、渡したデータを1件ずつバラして、データ更新APIを呼び出す axios.put
に渡します
第1引数に更新対象となるデータのidも含むデータ更新APIのURLを指定し、第2引数に入力データのJSON形式を指定します
…
+ onUpdate: async function(e)
+ {
+ for (const n of this.members)
+ {
+ await axios.put('/members/' + n.id,
+ {
+ 'name': n.name,
+ 'age': n.age,
+ 'team': n.team,
+ 'position': n.position,
+ })
+ }
+
+ axios.get('/members')
+ .then(response => {this.members = response.data})
+ },
…
Web上からデータを更新し、Web上で確認
「全件更新」ボタンをクリックすると、特に画面上は何も変わっていませんが、更新は走っています
もし更新が走っていなければ、リロードしたら、更新前に戻るハズなので、リロードしてみると、ちゃんと更新できていることが確認できます
データ削除APIをSPAから呼べるようにする
最後に、Web上からデータ更新できるようにしましょう
データ削除は、各データ毎の「削除」ボタンでデータ削除APIを呼ぶようにします
「削除」ボタンは、データのidを引数指定して、onDelete
に渡すことで、該当データを削除できるようにします
axios.delete
の第1引数には、データ削除APIのURLに削除対象データのid付きで指定します
<script src="https://cdn.jsdelivr.net/npm/vue@3.2/dist/vue.global.js"></script>
<script type="importmap">
{
"imports": {
"vue": "https://cdn.jsdelivr.net/npm/vue@3.2/dist/vue.esm-browser.prod.js"
}
}
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/1.2.2/axios.min.js"></script>
<div id="app">
<h1>Members</h1>
<table>
<tr>
<th>id</th>
<th>name</th>
<th>age</th>
<th>team</th>
<th>position</th>
</tr>
<tr v-for="n in members">
<td>{{n.id}}</td>
<td><input type="text" v-model="n.name"></td>
<td><input type="text" v-model="n.age"></td>
<td><input type="text" v-model="n.team"></td>
<td><input type="text" v-model="n.position"></td>
+ <td><button v-on:click="onDelete(n.id)">削除</button></td>
</tr>
<tr>
<td colspan="6"><button v-on:click="onUpdate">全件更新</button></td>
</tr>
<tr>
<td>-</td>
<td><input type="text" v-model="add.name"></td>
<td><input type="text" v-model="add.age"></td>
<td><input type="text" v-model="add.team"></td>
<td><input type="text" v-model="add.position"></td>
<td><button v-on:click="onCreate">追加</button></td>
</tr>
</table>
</div>
<script type="module">
import {createApp} from 'vue'
createApp
({
data()
{
return {
members: [],
add: {name: '', age: '', team: '', position: ''},
}
},
mounted(){
this.get()
},
methods:
{
get: function() {
axios.get('/members').then(response => {this.members = response.data})
},
onCreate: async function(e)
{
await axios.post('/members',
{
'name': this.add.name,
'age': this.add.age,
'team': this.add.team,
'position': this.add.position,
})
this.add = {name: '', age: '', team: '', position: ''}
axios.get('/members')
.then(response => {this.members = response.data})
},
onUpdate: async function(e)
{
for (const n of this.members)
{
await axios.put('/members/' + n.id,
{
'name': n.name,
'age': n.age,
'team': n.team,
'position': n.position,
})
}
axios.get('/members')
.then(response => {this.members = response.data})
},
+ onDelete: async function(id)
+ {
+ await axios.delete('/members/' + id)
+
+ axios.get('/members')
+ .then(response => {this.members = response.data})
+ },
},
}).mount('#app')
</script>
Web上からデータを削除し、Web上で確認
【参考】本コラムの検証環境
本コラムは、以下環境で検証しています(恐らくUbuntu実機やMacでも動きます)
-
Windows 10
- 実機+Elixir 1.14.2 (Erlang/OTP 25)
- Phoenix 1.6.15
- WSL2/Ubuntu 20.04+Elixir 1.14.2 (Erlang/OTP 25) ※最新版のインストール手順はコチラ
- Phoenix 1.6.15
- Docker/Debian 11.6+Elixir 1.14.2 (Erlang/OTP 25)
- Phoenix 1.6.15
- 実機+Elixir 1.14.2 (Erlang/OTP 25)
-
Windows11
- WSL2/Ubuntu 22.04+Docker Compose+Elixir 1.13.4 (Erlang/OTP 25)
- Phoenix 1.6.15
- WSL2/Ubuntu 22.04+Docker Compose+Elixir 1.13.4 (Erlang/OTP 25)
終わり
さて、これでWeb上からのデータ追加/更新/削除ができるようになるので、色々遊んでみてください
今回の内容は、Vue.js+REST APIによるSPA(Single Page Application)開発の基本になりますので、覚えておくと、モダンWebアプリ開発の強力な武器になります
次回は、Phoenixアプリを 「Gigalixir」というPaaSにリリース して、公開するためのやり方を解説したいと思います