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

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

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

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

なお、本コラムと全く同じ内容のVue.js版もあります

**LiveView版**
https://qiita.com/piacerex/items/78ee0d2ad8cca0927980

**■「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を呼び出す（表示編）【Vue.js版】**](https://qiita.com/piacere_ex/items/50d847170291c41fef64)
|> [**⑦SPAからPhoenix製APIを呼び出す（更新編）【Vue.js版】**](https://qiita.com/piacerex/items/7cd1162ce6d66a334a07)
|> [<b>⑧Gigalixirに本番リリース</b>](https://qiita.com/piacerex/items/1a9cbcc740ca3707eaec)
|> [**⑨「LiveView」ElixirサーバサイドのみでReact的SPA／リアルタイムUIが作れる**](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)

前回、作ったHTMLに、データ追加UI HTML（入力フィールド群と「追加」ボタン）とデータ追加UIで入力されるデータを保持する `add` 変数、そしてデータ追加APIを呼び出すハンドラ `onCreate` を追加します（Vue2でも変更箇所は同じなので割愛）

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

```diff_html:lib/basic_web/templates/page/index.html.heex
<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` で指定するハンドラには、下記のように引数を指定することもできますが、今回のように引数指定無の場合は、イベント情報が暗黙の引数として渡されます

```html:lib/basic_web/templates/page/index.html.eex
…
<td><button v-on:click="onCreate('ダミー1', 'ダミー2')">追加</button></td>
…
```

イベント情報は、下記の通りです（ハンドラ内で `console.log` すれば見れます）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/05c81fba-69a2-7d3a-6327-dd69e28ce776.png)

## 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)

HTMLに、データ更新UI HTML（データ表示部分を入力フィールドに差し替え、「全件更新」ボタン追加）を追加し、データ更新APIを全データ件数分だけ繰り返すハンドラ `onUpdate` を追加します（Vue2でも変更箇所は同じなので割愛）

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

```diff_html:lib/basic_web/templates/page/index.html.heex
<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` に連携されるか見ていきましょう

```diff_elixir
…
  <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形式を指定します

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

では、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)

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

`axios.delete` の第1引数には、データ削除APIのURLに削除対象データのid付きで指定します

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

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
  - WSL2／Ubuntu 20.04＋Elixir 1.14.2 (Erlang/OTP 25)　※[**最新版のインストール手順はコチラ**](https://qiita.com/piacerex/items/01e89435af0f7a454ad2)
    - Phoenix 1.6.15
  - Docker／Debian 11.6＋Elixir 1.14.2 (Erlang/OTP 25)
    - Phoenix 1.6.15

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

# 終わり

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

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

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