27
14

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.

ElixirAdvent Calendar 2018

Day 4

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

Last updated at Posted at 2018-12-03

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

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

: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

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

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

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.postawait が付いているのは、データ追加が完全に終わってから、データ取得するような「同期処理」にするためです

ここを同期にせず、「非同期処理」で走らせると、データ追加が完了しない状態で、データ取得を行ってしまうため(これはJavaScriptの実行が非同期処理の影響)、そうならないよう同期処理化しています

「v-on:click」でクリック時のハンドラ呼出

「追加」ボタンは、クリックされたときに、v-on:click という属性を使うことで、クリック時のハンドラメソッドを指定できるので、これを使って onCreate を呼び出します

他にも、v-on:changev-on:focusv-on:blur 等、JavaScriptで利用可能なハンドラと同様のものをVue.jsでも利用できます

なお、v-on:click で指定するハンドラには、下記のように引数を指定することもできますが、今回のように引数指定無の場合は、イベント情報が暗黙の引数として渡されます

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

イベント情報は、下記の通りです(ハンドラ内で console.log すれば見れます)
image.png

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

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

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

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 に連携されるか見ていきましょう


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

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

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

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

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

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

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

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

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

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

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

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

27
14
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
27
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?