7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ExcelからElixir入門⑦:SPAからPhoenix製APIを呼び出す(更新編)【LiveView版】

Last updated at Posted at 2023-01-11

【本コラムは、5分で読めて、10分くらいでお試しいただけます】
piacereです、ご覧いただいてありがとございます :bow:

前回は、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アプリをリリース

:ocean::ocean::ocean: Elixir Advent Calendar: 言語カテゴリ1位 & 全カテゴリ2位! :ocean::ocean::ocean:

例年を遥かに超える盛り上がりを見せ、堂々のトップ獲得ッ! :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

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

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

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

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

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を起動します

mix deps.get
iex -S mix phx.server

データ追加UIを設置し、入力したデータでデータ追加APIを呼びます
image.png

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

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

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

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

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(入力フィールド群と「追加」ボタン)を追加します

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 について知りたい方は、下記コラムをご参考ください

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

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

藤井名人の次は、「2100年の科学ライフ」等で有名な理論物理学者、ミチオ・カク博士にjoinしてもらうとしましょうw :stuck_out_tongue_closed_eyes:
image.png

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

ここで気付いていただきたいのは、「追加」ボタンをクリックしても、データ表示は増えたのに、ページ遷移が起こらなかったことです … これこそがSPAの威力です
image.png

なお、REST APIクライアントからでも、追加されたデータが確認できます
image.png

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

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

MemberController モジュールに updatedelete を追加します

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

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

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

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」ボタンをクリックしてみましょう

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

200 OK が返却されます
image.png

Web上で確認すると、更新されていることが確認できました
image.png

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

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

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

204 No Content が返却されます
image.png

Web上で確認すると、ミチオ・カク博士が卒業されたことが確認できました :sob:
image.png

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

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

データ表示部分を、入力フィールドに全て変更し、「全件更新」ボタンをクリックしたら、全データに対してデータ更新API呼出を繰り返すことで更新します
image.png

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

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

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(データ表示部分を入力フィールドに差し替え、「全件更新」ボタン追加)を追加します

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=~ で指定します

+<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.exhandle_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上で確認

では、Web上で名前を英字表記にしてみましょう
image.png

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

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

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

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

データ削除は、各データ毎の「削除」ボタンでデータ削除APIを呼ぶようにします
image.png

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

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

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 を指定します

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

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

本コラムは、以下環境で検証しています(恐らく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
  • 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にリリース して、公開するためのやり方を解説したいと思います

7
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?