ExcelからElixir入門⑦：SPAからPhoenix製APIを呼び出す（更新編）【LiveView版】

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

[前回](https://qiita.com/piacerex/items/64e77b85bfb4c6832662)は、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回](https://qiita.com/piacerex/items/3f8ee18c9443d63955bf)で実践します

**■「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)
|> [**⑥SPAからPhoenix製APIを呼び出す（表示編）【LiveView版】**](https://qiita.com/piacerex/items/64e77b85bfb4c6832662)
|> [**⑦SPAからPhoenix製APIを呼び出す（更新編）【LiveView版】**](https://qiita.com/piacerex/items/78ee0d2ad8cca0927980)
|> [<b>⑧Gigalixirに本番リリース</b>](https://qiita.com/piacerex/items/1a9cbcc740ca3707eaec)
|> [**⑨ElixirサーバサイドのみでReactと同じSPA／リアルタイムUIが作れる「LiveView」**](https://qiita.com/piacerex/items/3f8ee18c9443d63955bf)
|> [**⑩ElixirサーバサイドSPAをスマホで見るためにGigalixirリリース**](https://qiita.com/piacerex/items/583174d72e6fc0cd289e)
|> [**⑪Gigalixir上のLiveViewアプリに独自ドメイン名を付与して正式なアプリ公開**](https://qiita.com/piacerex/items/b61348c282395167fd8c)
|> [**⑫Elixir／PhoenixのCRUD Webアプリをリリース**](https://qiita.com/piacerex/items/511f24888c7350639d0b)

<h3>:ocean::ocean::ocean: Elixir Advent Calendar: 言語カテゴリ1位 & 全カテゴリ2位！ :ocean::ocean::ocean:</h3>

例年を遥かに超える盛り上がりを見せ、堂々のトップ獲得ッ！ :qiita: :tada: :confetti_ball:

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
[![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/356a15c6-0dbb-097e-38f3-a1b4fed47555.png)](https://qiita.com/advent-calendar/2022/ranking/feedbacks/categories/programming_languages)

# データ追加APIをSPAから呼べるようにする

最初は、前回作ったデータ追加APIを使って、Web上からデータ追加できるようにしましょう

なお、以前のバージョンのコラムや、このシリーズの第3回までは、`DbMnesia`／`Db` モジュールをコラム中で実装していましたが、入門編を超えた範囲になるため、「Sqlex」としてOSS化しました … これをインストールして済ませます

https://github.com/piacerex/sqlex

mix.exsの `def deps do` 配下の `:phoenix` の直上に追記します

```diff_elixir:mix.exs
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を起動します

```sh
mix deps.get
iex -S mix phx.server
```

データ追加UIを設置し、入力したデータでデータ追加APIを呼びます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/34f575b9-95ca-00a4-0369-38f870b67de3.png)

前回、作ったLiveViewモジュールに、データ追加APIを呼び出すハンドラ `onCreate` を追加します

LiveViewのハンドラは、`handle_event` の第1引数にハンドラ名を指定します

第2引数である `params` には、サブミット時の入力データ群や、クリック時に指定された入力データ群が渡されてきます

`Req.post!` の第1引数にデータ追加APIのURLを指定し、第2引数の `body` に入力データのJSON形式を指定します（`headers` には `["Content-Type": "application/json"]` を指定します）

```diff_elixir:lib/basic_web/live/index.ex
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（入力フィールド群と「追加」ボタン）を追加します

```diff_html:lib/basic_web/live/index.html.heex
<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` について知りたい方は、下記コラムをご参考ください

https://qiita.com/piacerex/items/d00458bd2c6293435557

## Web上からデータを追加し、Web上で確認

では、Web上からデータ追加してみましょう

藤井名人の次は、[「2100年の科学ライフ」](https://www.amazon.co.jp/exec/obidos/ASIN/4140815728/onelinebiz-22)等で有名な理論物理学者、ミチオ・カク博士にjoinしてもらうとしましょうｗ :stuck_out_tongue_closed_eyes:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/c578af8a-6900-b432-ecd0-392b1d185015.png)

「追加」ボタンをクリックすると、ミチオ・カク博士がチームにjoinしました … 素晴らしいッ

ここで気付いていただきたいのは、「追加」ボタンをクリックしても、データ表示は増えたのに、ページ遷移が起こらなかったことです … これこそがSPAの威力です
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/aa8555d7-6f0f-2875-f567-ece820c3e83b.png)

なお、REST APIクライアントからでも、追加されたデータが確認できます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/633545ab-c28f-a769-0f83-b92e29841e90.png)

# データ更新APIとデータ削除APIを実装する

次は、データ更新APIとデータ削除APIを作りましょう

`MemberController` モジュールに `update` と `delete` を追加します

`update` は、`members` へのupdate文を `Db.query` に渡し、その後、ステータスコードとして `200 OK` を返却します

`delete` は、`members` へのdelete文を `Db.query`  に渡し、その後、ステータスコードとして `204 No Content` を返却します

なお、ルーティングは `resources` で一括で作られているので、ここでの追加は不要です

```diff_elixir:lib/basic_web/controllers/member_controller.ex
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」ボタンをクリックしてみましょう

```json
{
  "name": "たくと", 
  "age": 40, 
  "team": "カラビナテクノロジー株式会社", 
  "position": "リードエンジニア、プロジェクトマネージャ"
}
```

`200 OK` が返却されます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/dae31bb7-c005-85e0-b4d6-0890d8ca791d.png)

Web上で確認すると、更新されていることが確認できました
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/89f9cc54-c273-f3c4-17ad-f5b62fe27379.png)

## データ削除APIを叩いた後、Web上で確認

APIでデータ削除してみましょう

「PUT」を「DELETE」に変更し、URLを `localhost:4000/members/6` に変更した上で、「Send」ボタンをクリックしてみましょう

`204 No Content` が返却されます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/04372b41-6347-66df-c2ad-aa5fd151fc9c.png)

Web上で確認すると、ミチオ・カク博士が卒業されたことが確認できました :sob:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/74101995-a2f4-53dc-98ad-27daff625588.png)

# データ更新APIをSPAから呼べるようにする

次は、Web上からデータ更新できるようにしましょう

データ表示部分を、入力フィールドに全て変更し、「全件更新」ボタンをクリックしたら、全データに対してデータ更新API呼出を繰り返すことで更新します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/2c3634a2-6454-41a7-488c-0fcac8421e80.png)

LiveViewモジュールに、データ更新APIを全データ件数分だけ繰り返すハンドラ `onUpdate` を追加します

コード内容の詳細は、この後、解説します

```diff_elixir:lib/basic_web/live/index_live.ex
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（データ表示部分を入力フィールドに差し替え、「全件更新」ボタン追加）を追加します

```diff_html:lib/basic_web/live/index.html.heex
+<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=～` で指定します

```diff_elixir
+<form phx-submit="onUpdate">
…
```

## HTMLからLiveViewモジュールへの複数件データ連携

「全件更新」ボタンをクリックしたときに、下記コードがどのようなデータが `index_live.ex` に連携されるか見ていきましょう

```diff_elixir
…
<%= 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` に渡します

```diff_elixir
%{
  "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"]` を指定します）

```diff_elixir
…
+ 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上で確認

では、Web上で名前を英字表記にしてみましょう
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/ee62bc9c-0194-b9d8-a45a-49717b68e85c.png)

「全件更新」ボタンをクリックすると、特に画面上は何も変わっていませんが、更新は走っています

もし更新が走っていなければ、リロードしたら、更新前に戻るハズなので、リロードしてみると、ちゃんと更新できていることが確認できます

# データ削除APIをSPAから呼べるようにする

最後に、Web上からデータ更新できるようにしましょう

データ削除は、各データ毎の「削除」ボタンでデータ削除APIを呼ぶようにします
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/9c622e32-b8bb-0339-e95c-25023aa8ce2a.png)

LiveViewモジュールに、データ削除APIを呼ぶ `onDelete` を追加します

`Req.delete!` の第1引数には、データ削除APIのURLに削除対象データのid付きで指定します（第2引数の `headers` には `["Content-Type": "application/json"]` を指定します）

```diff_elixir:lib/basic_web/live/index_live.ex
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` を指定します

```diff_html:lib/basic_web/live/index.html.heex
<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上で確認

Web上で藤井名人を削除すると、藤井名人も卒業です :sob:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/319513a2-154c-4357-6f47-ae0a341f8896.png)

# 【参考】本コラムの検証環境

本コラムは、以下環境で検証しています（恐らく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)　※[**最新版のインストール手順はコチラ**](https://qiita.com/piacerex/items/01e89435af0f7a454ad2)
    - 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

- Windows11
  - WSL2／Ubuntu 22.04＋Docker Compose＋Elixir 1.13.4 (Erlang/OTP 25)
    - Phoenix 1.6.15
    - LiveView 0.17.12

# 終わり

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

今回の内容は、LiveView＋REST APIによるSPA（Single Page Application）開発の基本になりますので、覚えておくと、モダンWebアプリ開発の強力な武器になります

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