piyopiyo.ex#2：LiveViewで匿名SNSを作ろう

fukuoka.ex Elixir／Phoenix Advent Calendar 2021、昨日@torifukukaiouさんで[「【Elixir】次の関数の第2引数なんだよな〜 2021年12月 (2021/12/03)」](https://qiita.com/torifukukaiou/items/5f4d1a76400fb72ba740)でした

---


##①プロジェクトを作成【作業】
mix phx.new コマンドを使ってPhoenixのプロジェクトを作成します　※なおWindows（WSL2未使用）をお使いの方は、先に[コチラを実行](https://qiita.com/t-yamanashi/items/89b63335d5a3d87efea6#Windows%EF%BC%88WSL2%E6%9C%AA%E4%BD%BF%E7%94%A8%EF%BC%89%E3%81%A7%E3%81%AFBuild%20Tools%20for%20Visual%20Studio%E3%81%8C%E5%BF%85%E8%A6%81)してから下記を実施してください

```
mix phx.new sns --database sqlite3
```
mix phx.new プロジェクト名　--database sqlite3
--database オプションでDBのプラットフォームを指定します
今回はsqlite3を使います


依存関係を取得してインストールするかを聞かれますのでYで続行

```
Fetch and install dependencies? [Yn] Y
```

---

プロジェクトの作成が完了するこのような画面が表示します

```
We are almost there! The following steps are missing:

    $ cd sns

Then configure your database in config/dev.exs and run:

    $ mix ecto.create

Start your Phoenix app with:

    $ mix phx.server

You can also run your app inside IEx (Interactive Elixir) as:

    $ iex -S mix phx.server
```

---
### Windows（WSL2未使用）ではBuild Tools for Visual Studioが必要

【2021/12/6追記】Build Tools for Visual Studio 2022にて下記を検証しました。

Windowsネイティブで動かしている方は、sqlite3のビルドに「Build Tools for Visual Studio」のインストールが必要です。

まず、下記コラムでインストールを行ってください。なお、「clang」のダウンロード／インストールは不要です。
https://qiita.com/piacerex/items/840a2679f8c4382c453a

次に、スタートメニューから「x86 Native Tools Command Prompt for VS 2022」を起動してから、上記手順を実施してください。

---

##②データベースの作成【作業】

まず、cdコマンドでsnsディレクトリに移動します


```
$ cd sns
```

次に mix ecto.crateでデータベースを作成します

```
$ mix ecto.create
```
---

sns_dev.dbが作成されます
このファイルはsqlite3で使われるデータベースのファイルです

確認方法
Mac / Linux

```
$ ls
```

Windows

```
dir
```

---

#③LiveViewのページを作成【作業】
mix phx.gen.liveコマンドを使う

```
mix phx.gen.live Messages Message messages contents:text
```

mix phx.gen.live コンテキストモジュール名　スキーマモジュール名　スキーマテーブル名　カラム名:型

---

LiveViewページ作成が完了するこのような画面が表示されます

```
Add the live routes to your browser scope in lib/sns_web/router.ex:

    live "/messages", MessageLive.Index, :index
    live "/messages/new", MessageLive.Index, :new
    live "/messages/:id/edit", MessageLive.Index, :edit

    live "/messages/:id", MessageLive.Show, :show
    live "/messages/:id/show/edit", MessageLive.Show, :edit


Remember to update your repository by running migrations:

    $ mix ecto.migrate
```

ここで２点作業が必要です
１、Router.exを書き換える
２、データベースのマイグレート

---

##④ルーターを書き換える【作業】
:memo:ルーターとは
　ブラウザーからのリクエストを元にどのモジュールの関数を実行するかを記述する場所です

lib/sns_web/router.exを開きます

変更前

```lib/sns_web/router.ex
#中略
  scope "/", SnsWeb do
    pipe_through :browser

#ここに記述します

    get "/", PageController, :index
  end
#中略
```

---

記述例

```lib/sns_web/router.ex
#中略
 scope "/", SnsWeb do
    pipe_through :browser

    live "/messages", MessageLive.Index, :index
    live "/messages/new", MessageLive.Index, :new
    live "/messages/:id/edit", MessageLive.Index, :edit

    live "/messages/:id", MessageLive.Show, :show
    live "/messages/:id/show/edit", MessageLive.Show, :edit

    get "/", PageController, :index
  end
#中略
```

---

##⑤データベースのマイグレート【作業】

mix ecto.migrateコマンドを使う

```
$ mix ecto.migrate
```

実行結果

```
Generated sns app

19:12:40.993 [info]  == Running 20211120095944 Sns.Repo.Migrations.CreateMessages.change/0 forward

19:12:41.000 [info]  create table messages

19:12:41.002 [info]  == Migrated 20211120095944 in 0.0s
```

---

##⑥Phoenixを起動【作業】
mix phx.serverコマンドを使う

```
$ mix phx.server
```

---

起動

```
[info] Running SnsWeb.Endpoint with cowboy 2.9.0 at 127.0.0.1:4000 (http)
[debug] Downloading esbuild from https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.12.18.tgz
[info] Access SnsWeb.Endpoint at http://localhost:4000
[watch] build finished, watching for changes...
[info] GET /messages
[debug] Processing with Phoenix.LiveView.Plug.index/2
  Parameters: %{}
  Pipelines: [:browser]
[debug] QUERY OK source="messages" db=0.0ms queue=0.1ms idle=1092.1ms
SELECT m0."id", m0."contents", m0."inserted_at", m0."updated_at" FROM "messages" AS m0 []
[info] Sent 200 in 103ms
[info] CONNECTED TO Phoenix.LiveView.Socket in 88µs
  Transport: :websocket
  Serializer: Phoenix.Socket.V2.JSONSerializer
  Parameters: %{"_csrf_token" => "Hxs3Yx8yByFpci8uHx8UFxg_BhMxMwogeLR5odLfY4nApW-HvMJWSpxn", "_mounts" => "0", "_track_static" => %{"0" => "http://localhost:4000/assets/app.css", "1" => "http://localhost:4000/assets/app.js"}, "vsn" => "2.0.0"}
[debug] QUERY OK source="messages" db=0.1ms idle=1406.9ms
SELECT m0."id", m0."contents", m0."inserted_at", m0."updated_at" FROM "messages" AS m0 []

```


---

ブラウザで ```http://localhost:4000/messages``` にアクセス

---

##⑦一覧画面の中身を見てみよう【説明】

![Screenshot from 2021-11-20 19-21-05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295814/962f4288-827b-c691-eace-24f5ee463390.png)

---

どのようにしてこのページが表示している？

```lib/sns_web/router.ex
#中略
  scope "/", SnsWeb do
    pipe_through :browser

    live "/messages", MessageLive.Index, :index
#中略
```

ルーターについて

・live 　LiveViewであることを示してる
・"/messages"　URL(エンドポイント)が messagesを示している
・MessageLive.Index　SnsWeb.MessageLive.Indexモジュールを呼び出すことを示してる
・ :index indexアクションを示してる

---

SnsWeb.MessageLive.Indexモジュールを見てみよう

```lib/sns_web/live/message_live/index.ex
defmodule SnsWeb.MessageLive.Index do
  use SnsWeb, :live_view

## 中略
  @impl true
  def mount(_params, _session, socket) do
    {:ok, assign(socket, :messages, list_messages())}
  end

#中略
  defp list_messages do
    Messages.list_messages()
  end
```
・LiveViewは初めにmountが呼ばれます
・list_messagesでデータベースから情報を取得します
・assignで表示する内容を渡しています


---

handle_params→apply_actionの:indexを呼びます

```lib/sns_web/live/message_live/index.ex
#中略

  def handle_params(params, _url, socket) do
    {:noreply, apply_action(socket, socket.assigns.live_action, params)}
  end


  defp apply_action(socket, :index, _params) do
    socket
    |> assign(:page_title, "Listing Messages")
    |> assign(:message, nil)
  end
#中略
```


---

データーベースはどうやって呼ばれてる？

Repo.all(Message)で結果を取得してます

```lib/sns/messages.ex
#中略
defmodule Sns.Messages do
  @moduledoc """
  The Messages context.
  """

  import Ecto.Query, warn: false
  alias Sns.Repo

  alias Sns.Messages.Message

  def list_messages do
    Repo.all(Message)
  end
#中略
```

「elixir ecto」で検索するとこの仕組みを調べるヒントになります

---

Sns.Messages.Messageってどうなってる？

テーブルはmessages
フィールドはcontentsで文字型であることがわかります

```lib/sns/messages/message.ex
defmodule Sns.Messages.Message do
  use Ecto.Schema
  import Ecto.Changeset

  schema "messages" do
    field :contents, :string

    timestamps()
  end
#中略
```

---

テンプレートを見てみよう

```leex:lib/sns_web/live/message_live/index.html.heex
#中略
    <%= for message <- @messages do %>
      <tr id={"message-#{message.id}"}>
        <td><%= message.contents %></td>

        <td>
          <span><%= live_redirect "Show", to: Routes.message_show_path(@socket, :show, message) %></span>
          <span><%= live_patch "Edit", to: Routes.message_index_path(@socket, :edit, message) %></span>
          <span><%= link "Delete", to: "#", phx_click: "delete", phx_value_id: message.id, data: [confirm: "Are you sure?"] %></span>
        </td>
      </tr>
    <% end %>
#中略
```
・list_messages()　の結果が@messagesに格納されてます
・forで@messagesの件数分<% end %>までの内容が出力されます
・messageは1件のデータです
・<%= message.contents %>　でcontentsフィールドの内容を表示します


---

##⑧投稿画面を見てみよう【説明】
![Screenshot from 2021-11-26 08-36-41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295814/f5870fc1-f483-b23b-95e4-853d40a89ef2.png)


---

router.exを見てみよう

```lib/sns_web/router.ex
#中略
  scope "/", SnsWeb do
#中略
    pipe_through :browser
#中略
    live "/messages/new", MessageLive.Index, :new
#中略
```

・live 　LiveViewであることを示してる
・"/messages/new"　URL(エンドポイント)が messagesを示している
・MessageLive.Index　SnsWeb.MessageLive.Indexモジュールを呼び出すことを示してる
・ :new newアクションを示してる

---

SnsWeb.MessageLive.Indexモジュールを見てみよう

defp apply_action(socket, :new, _params)が呼ばれます

```lib/sns_web/live/message_live/index.ex
#中略
  defp apply_action(socket, :new, _params) do
    socket
    |> assign(:page_title, "New Message")
    |> assign(:message, %Message{})
  end
#中略
```

---

テンプレートを見てみよう

```leex:lib/sns_web/live/message_live/index.html.heex
#中略
<h1>Listing Messages</h1>

<%= if @live_action in [:new, :edit] do %>
  <%= live_modal SnsWeb.MessageLive.FormComponent,
    id: @message.id || :new,
    title: @page_title,
    action: @live_action,
    message: @message,
    return_to: Routes.message_index_path(@socket, :index) %>
<% end %>
#中略
```

live_actionは:newに為live_modal SnsWeb.MessageLive.FormComponentが呼ばれます

----

FormComponentのテンプレート


```leex:lib/sns_web/live/message_live/form_component.html.heex
#中略
<div>
  <h2><%= @title %></h2>

  <.form
    let={f}
    for={@changeset}
    id="message-form"
    phx-target={@myself}
    phx-change="validate"
    phx-submit="save">
  
    <%= label f, :contents %>
    <%= textarea f, :contents %>
    <%= error_tag f, :contents %>
  
    <div>
      <%= submit "Save", phx_disable_with: "Saving..." %>
    </div>
  </.form>
</div>
#中略
```

---

![Screenshot from 2021-11-28 21-35-42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295814/c65b698b-c628-22c6-a05f-eee065d3792c.png)

---

form_componentを見てみよう

```lib/sns_web/live/message_live/form_component.ex
#中略
  def handle_event("validate", %{"message" => message_params}, socket) do
    changeset =
      socket.assigns.message
      |> Messages.change_message(message_params)
      |> Map.put(:action, :validate)

    {:noreply, assign(socket, :changeset, changeset)}
  end

  def handle_event("save", %{"message" => message_params}, socket) do
    save_message(socket, socket.assigns.action, message_params)
  end

  defp save_message(socket, :new, message_params) do
    case Messages.create_message(message_params) do
      {:ok, _message} ->
        {:noreply,
         socket
         |> put_flash(:info, "Message created successfully")
         |> push_redirect(to: socket.assigns.return_to)}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign(socket, changeset: changeset)}
    end
  end
#中略
`````

---
・handle_event("validate", %{"message" => message_params}, socket)
　入力チェック

・def handle_event("save", %{"message" => message_params}, socket) do
　保存ボタンを押した時

・defp save_message(socket, :new, message_params) do
　保存処理の内容
　Messages.create_message(message_params) を見てみよう

---

create_messageでDBに１件データを追加します

```lib/sns/messages.ex
#中略
  def create_message(attrs \\ %{}) do
    %Message{}
    |> Message.changeset(attrs)
    |> Repo.insert()
  end
#中略
```
---

##⑨投稿内容を削除を見てみよう【説明】
![Screenshot from 2021-11-26 08-58-14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295814/058a7349-ad6c-2354-b29f-c885a77debe8.png)

---

![Screenshot from 2021-11-26 08-58-49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295814/696e63bf-8dde-e0e8-fb94-418b31076b1f.png)

---

phx_click: "delete" でhandle_eventの"delete"と関連づけします

```leex:lib/sns_web/live/message_live/index.html.heex
#中略
<span><%= link "Delete", to: "#", phx_click: "delete", phx_value_id: message.id, data: [confirm: "Are you sure?"] %></span>
#中略
```

 Messages.delete_message(message)を見てみよう

```lib/sns_web/live/message_live/index.ex
#中略
  def handle_event("delete", %{"id" => id}, socket) do
    message = Messages.get_message!(id)
    {:ok, _} = Messages.delete_message(message)

    {:noreply, assign(socket, :messages, list_messages())}
  end
#中略
```

---

delete_messageでデータを１件削除してます。

```lib/sns/messages.ex
#中略
  def get_message!(id), do: Repo.get!(Message, id)

  def delete_message(%Message{} = message) do
    Repo.delete(message)
  end
#中略

```

---

#改造してみよう

---

##⑩表示内容を変更【作業】

```leex:lib/sns_web/live/message_live/index.html.heex
#中略
    <%= for message <- @messages do %>
      <tr id={"message-#{message.id}"}>
        <td>
          <span><%= live_redirect "Show", to: Routes.message_show_path(@socket, :show, message) %></span>
          <span><%= live_patch "Edit", to: Routes.message_index_path(@socket, :edit, message) %></span>
          <span><%= link "Delete", to: "#", phx_click: "delete", phx_value_id: message.id, data: [confirm: "Are you sure?"] %></span>
          <pre>
            <%= message.contents %>
          </pre>
        </td>
      </tr>
    <% end %>
#中略
```

---

![Screenshot from 2021-11-26 09-16-57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295814/1a8044a8-e32b-8f7e-66fe-88fb23a105e5.png)



---

##⑪ヘッダーの画像を変更【作業】

↓この画像をダウンロードし、piyo.pngとして保存
![piyo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295814/16a073a1-1486-2614-37e5-5d556cdcad12.png)

---

ダウンロードした画像をpriv/static/images/piyo.pngへ移動
![Screenshot from 2021-11-26 09-23-29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295814/cf2e60ae-1882-0c51-5ef9-8d33e35f2fa7.png)


---

更新前

```leex:lib/sns_web/templates/layout/root.html.heex
#中略
  <body>
    <header>
      <section class="container">
        <nav>
          <ul>
            <li><a href="https://hexdocs.pm/phoenix/overview.html">Get Started</a></li>
            <%= if function_exported?(Routes, :live_dashboard_path, 2) do %>
              <li><%= link "LiveDashboard", to: Routes.live_dashboard_path(@conn, :home) %></li>
            <% end %>
          </ul>
        </nav>
        <a href="https://phoenixframework.org/" class="phx-logo">
          <img src={Routes.static_path(@conn, "/images/phoenix.png")} alt="Phoenix Framework Logo"/>
        </a>
      </section>
    </header>
    <%= @inner_content %>
  </body>

#中略
```

更新後

```leex:lib/sns_web/templates/layout/root.html.heex
##中略
  <body>
    <header>
      <section class="container">
        <img src={Routes.static_path(@conn, "/images/piyo.png")} alt="PiyoPiyo Logo"/>
      </section>
    </header>
    <%= @inner_content %>
  </body>
##中略
```

---

![Screenshot from 2021-11-26 09-42-17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295814/47750d64-0d7c-496d-eba1-eb1bd5e4bf6e.png)



---


##おわり


明日は、@kn339264さんで「[Elixir女子部のオーガナイザーをやってみた話](https://qiita.com/kn339264/items/397a35386fddb4e60048)」です