【本コラムは、5分で読めて、10分くらいでお試しいただけます】
piacereです、ご覧いただいてありがとございます
前回は、LiveViewからPhoenix内部APIを呼び出し、Web表示しました
今回は、Web入力をできるようにし、APIによるデータ更新を行う … いわゆるSPA(Single Page Application)を実装します
なお、本コラムと全く同じ内容のVue.js版もあります
Vue.js版
https://qiita.com/piacerex/items/7cd1162ce6d66a334a07
ちなみにVue.jsやReactと同じ構成にするため、LiveViewでもAPIによる操作を行っていますが、本来LiveViewはAPI無しでSPAを開発できます … これは、シリーズのこの先にある第9回で実践します
■「ExcelからElixir入門」シリーズの目次
①データ並替え/絞り込み
|> ②データ列抽出、Web表示
|> ③WebにDBデータ表示
|> ④Webに外部APIデータ表示
|> ⑤Webにグラフ表示
|> ⑥SPAからPhoenix製APIを呼び出す(表示編)【LiveView版】
|> ⑦SPAからPhoenix製APIを呼び出す(更新編)【LiveView版】
|> ⑧Gigalixirに本番リリース
|> ⑨ElixirサーバサイドのみでReactと同じSPA/リアルタイムUIが作れる「LiveView」
|> ⑩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を呼びます
前回、作ったLiveViewモジュールに、データ追加APIを呼び出すハンドラ onCreate
を追加します
LiveViewのハンドラは、handle_event
の第1引数にハンドラ名を指定します
第2引数である params
には、サブミット時の入力データ群や、クリック時に指定された入力データ群が渡されてきます
Req.post!
の第1引数にデータ追加APIのURLを指定し、第2引数の body
に入力データのJSON形式を指定します(headers
には ["Content-Type": "application/json"]
を指定します)
defmodule BasicWeb.IndexLive do
use Phoenix.LiveView
use BasicWeb, :live_view
@url "http://localhost:4000/members"
@headers ["Content-Type": "application/json"]
def get() do
Req.get!(@url, headers: @headers).body
|> Enum.map(& &1 |> Map.new(fn {k, v} -> {String.to_atom(k), v} end))
end
def mount(_params, _session, socket) do
+ {:ok, assign(socket, [members: get(), add: %{name: "", age: "", team: "", position: ""}])}
end
+ def handle_event("onCreate", params, socket) do
+ Req.post!(@url, headers: @headers, body: Jason.encode!(params))
+ {:noreply, assign(socket, [members: get()])}
+ end
end
前回、作ったHTMLの末尾に、データ追加UI HTML(入力フィールド群と「追加」ボタン)を追加します
<h1>Members</h1>
<table>
<tr>
<th>id</th>
<th>name</th>
<th>age</th>
<th>team</th>
<th>position</th>
</tr>
<%= for n <- @members do %>
<tr>
<td><%= n.id %></td>
<td><%= n.name %></td>
<td><%= n.age %></td>
<td><%= n.team %></td>
<td><%= n.position %></td>
</tr>
<% end %>
</table>
+<form phx-submit="onCreate">
+<table>
+ <tr>
+ <td>-</td>
+ <td><input type="text" name="name" value={@add.name}></td>
+ <td><input type="text" name="age" value={@add.age}></td>
+ <td><input type="text" name="team" value={@add.team}></td>
+ <td><input type="text" name="position" value={@add.position}></td>
+ <td><input type="submit" value="追加"></td>
+ </tr>
+</table>
+</form>
なお、mix phx.gen.live
等で使われている <.form>
や text_input
は、今回使わず、Vue.jsやReactと同じような条件のプレーンなHTMLで構築しています
<.form>
や text_input
について知りたい方は、下記コラムをご参考ください
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呼出を繰り返すことで更新します
LiveViewモジュールに、データ更新APIを全データ件数分だけ繰り返すハンドラ onUpdate
を追加します
コード内容の詳細は、この後、解説します
defmodule BasicWeb.IndexLive do
use Phoenix.LiveView
use BasicWeb, :live_view
@url "http://localhost:4000/members"
@headers ["Content-Type": "application/json"]
def get() do
Req.get!(@url, headers: @headers).body
|> Enum.map(& &1 |> Map.new(fn {k, v} -> {String.to_atom(k), v} end))
end
def mount(_params, _session, socket) do
{:ok, assign(socket, [members: get(), add: %{name: "", age: "", team: "", position: ""}])}
end
def handle_event("onCreate", params, socket) do
Req.post!(@url, headers: @headers, body: Jason.encode!(params))
{:noreply, assign(socket, [members: get()])}
end
+ def handle_event("onUpdate", params, socket) do
+ Enum.map(params, fn {k, v} -> Req.put!("#{@url}/#{k}", headers: @headers, body: Jason.encode!(v)) end)
+ {:noreply, socket}
+ end
end
HTMLに、データ更新UI HTML(データ表示部分を入力フィールドに差し替え、「全件更新」ボタン追加)を追加します
+<form phx-submit="onUpdate">
<h1>Members</h1>
<table>
<tr>
<th>id</th>
<th>name</th>
<th>age</th>
<th>team</th>
<th>position</th>
</tr>
<%= for n <- @members do %>
<tr>
+ <td><%= n.id %></td>
+ <td><input type="text" name={"#{n.id}[name]"} value={n.name}></td>
+ <td><input type="text" name={"#{n.id}[age]"} value={n.age}></td>
+ <td><input type="text" name={"#{n.id}[team]"} value={n.team}></td>
+ <td><input type="text" name={"#{n.id}[position]"} value={n.position}></td>
</tr>
<% end %>
+ <tr>
+ <td colspan="6"><input type="submit" value="全件更新"></td>
+ </tr>
</table>
+</form>
<form phx-submit="onCreate">
<table>
<tr>
<td>-</td>
<td><input type="text" name="name" value={@add.name}></td>
<td><input type="text" name="age" value={@add.age}></td>
<td><input type="text" name="team" value={@add.team}></td>
<td><input type="text" name="position" value={@add.position}></td>
<td><input type="submit" value="追加"></td>
</tr>
</table>
</form>
「phx-submit」でサブミット時のハンドラを指定
「全件更新」ボタンをクリックしたときに呼び出されるハンドラである handle_event("onUpdate", params ~
は、下記 <form phx-submit=~
で指定します
+<form phx-submit="onUpdate">
…
HTMLからLiveViewモジュールへの複数件データ連携
「全件更新」ボタンをクリックしたときに、下記コードがどのようなデータが index_live.ex
に連携されるか見ていきましょう
…
<%= for n <- @members do %>
<tr>
+ <td><%= n.id %></td>
+ <td><input type="text" name={"#{n.id}[name]"} value={n.name}></td>
+ <td><input type="text" name={"#{n.id}[age]"} value={n.age}></td>
+ <td><input type="text" name={"#{n.id}[team]"} value={n.team}></td>
+ <td><input type="text" name={"#{n.id}[position]"} value={n.position}></td>
</tr>
<% end %>
+ <tr>
+ <td colspan="6"><input type="submit" value="全件更新"></td>
+ </tr>
…
上記コードは、入力された全データを、下記フォーマットで index_live.ex
の handle_event("onUpdate", params ~
の params
に渡します
%{
"1" => %{
"age" => "54",
"name" => "enぺだーし",
"position" => "代表取締役、性能探求者",
"team" => "有限会社デライトシステムズ"
},
"2" => %{
"age" => "50",
"name" => "ざっきー",
"position" => "准教授、カーネルハッカー",
"team" => "公立大学法人 北九州市立大学"
},
…
}
そして、渡したデータを1件ずつバラして、データ更新APIを呼び出す Req.put!
に渡します
第1引数に更新対象となるデータのidも含むデータ更新APIのURLを指定し、第2引数の body
に入力データのJSON形式を指定します(headers
には ["Content-Type": "application/json"]
を指定します)
…
+ def handle_event("onUpdate", params, socket) do
+ Enum.map(params, fn {k, v} -> Req.put!("#{@url}/#{k}", [headers: @headers, body: Jason.encode!(v)]) end)
…
Web上からデータを更新し、Web上で確認
「全件更新」ボタンをクリックすると、特に画面上は何も変わっていませんが、更新は走っています
もし更新が走っていなければ、リロードしたら、更新前に戻るハズなので、リロードしてみると、ちゃんと更新できていることが確認できます
データ削除APIをSPAから呼べるようにする
最後に、Web上からデータ更新できるようにしましょう
データ削除は、各データ毎の「削除」ボタンでデータ削除APIを呼ぶようにします
LiveViewモジュールに、データ削除APIを呼ぶ onDelete
を追加します
Req.delete!
の第1引数には、データ削除APIのURLに削除対象データのid付きで指定します(第2引数の headers
には ["Content-Type": "application/json"]
を指定します)
defmodule BasicWeb.IndexLive do
use Phoenix.LiveView
use BasicWeb, :live_view
@url "http://localhost:4000/members"
@headers ["Content-Type": "application/json"]
def get() do
Req.get!(@url, headers: @headers).body
|> Enum.map(& &1 |> Map.new(fn {k, v} -> {String.to_atom(k), v} end))
end
def mount(_params, _session, socket) do
{:ok, assign(socket, [members: get(), add: %{name: "", age: "", team: "", position: ""}])}
end
def handle_event("onCreate", params, socket) do
Req.post!(@url, headers: @headers, body: Jason.encode!(params))
{:noreply, assign(socket, [members: get()])}
end
def handle_event("onUpdate", params, socket) do
columns = Map.keys(params) |> Enum.map(& String.to_atom(&1))
rows = Map.values(params) |> List.zip |> Enum.map(& Tuple.to_list(&1))
Enum.map(rows, fn row -> Enum.into(List.zip([columns, row]), %{}) end)
|> Enum.map(& Req.put!("#{@url}/#{&1.id}", headers: @headers, body: Jason.encode!(&1)))
{:noreply, socket}
end
+ def handle_event("onDelete", params, socket) do
+ Req.delete!("#{@url}/#{params["id"]}", headers: @headers)
+ {:noreply, assign(socket, [members: get()])}
+ end
end
HTMLに追加する「削除」ボタンは、link
の第1引数に raw("<button>削除</button>")
と書くことでクリック可能なボタンが生成できます
なお、"<button>~</button>"
では無く、ただの文字列として記述すると、ボタンでは無く、クリック可能なリンクが生成されます
raw(~)
は、文字列をHTMLエンコードしないための関数で、"<button>~</button>"
のようなHTMLを書く場合に必要となります
link
の第2引数では、データのidを phx_value_id
で引数指定して、onDelete
に渡すことで、該当データを削除できるようにします
phx_value_~
は ~
の部分が渡し先の params
のキー名として扱われるので、上記LiveViewモジュールの handle_event("onDelete", params ~
で params["id"]
と参照できます
link
の第2引数の phx_click
でクリック時のハンドラを指定できるので、onDelete
を指定します
<form phx-submit="onUpdate">
<h1>Members</h1>
<table>
<tr>
<th>id</th>
<th>name</th>
<th>age</th>
<th>team</th>
<th>position</th>
</tr>
<%= for n <- @members do %>
<tr>
<td><%= n.id %></td>
<td><input type="text" name={"#{n.id}[name]"} value={n.name}></td>
<td><input type="text" name={"#{n.id}[age]"} value={n.age}></td>
<td><input type="text" name={"#{n.id}[team]"} value={n.team}></td>
<td><input type="text" name={"#{n.id}[position]"} value={n.position}></td>
+ <td><%= link(raw("<button>削除</button>"), [to: "#", phx_click: "onDelete", phx_value_id: n.id]) %></td>
</tr>
<% end %>
<tr>
<td colspan="6"><input type="submit" value="全件更新"></td>
</tr>
</table>
</form>
<form phx-submit="onCreate">
<table>
<tr>
<td>-</td>
<td><input type="text" name="name" value={@add.name}></td>
<td><input type="text" name="age" value={@add.age}></td>
<td><input type="text" name="team" value={@add.team}></td>
<td><input type="text" name="position" value={@add.position}></td>
<td><input type="submit" value="追加"></td>
<%!-- </form> --%>
</tr>
</table>
</form>
Web上からデータを削除し、Web上で確認
【参考】本コラムの検証環境
本コラムは、以下環境で検証しています(恐らくUbuntu実機やMacでも動きます)
-
Windows 10
- 実機+Elixir 1.14.2 (Erlang/OTP 25)
- Phoenix 1.6.15
- LiveView 0.17.12
- WSL2/Ubuntu 20.04+Elixir 1.14.2 (Erlang/OTP 25) ※最新版のインストール手順はコチラ
- Phoenix 1.6.15
- LiveView 0.17.12
- Docker/Debian 11.6+Elixir 1.14.2 (Erlang/OTP 25)
- Phoenix 1.6.15
- LiveView 0.17.12
- 実機+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
- LiveView 0.17.12
- WSL2/Ubuntu 22.04+Docker Compose+Elixir 1.13.4 (Erlang/OTP 25)
終わり
さて、これでWeb上からのデータ追加/更新/削除ができるようになるので、色々遊んでみてください
今回の内容は、LiveView+REST APIによるSPA(Single Page Application)開発の基本になりますので、覚えておくと、モダンWebアプリ開発の強力な武器になります
次回は、Phoenixアプリを 「Gigalixir」というPaaSにリリース して、公開するためのやり方を解説したいと思います