18
3

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.

fukuoka.ex Elixir/PhoenixAdvent Calendar 2021

Day 4

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

Last updated at Posted at 2021-12-03

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


##①プロジェクトを作成【作業】
mix phx.new コマンドを使ってPhoenixのプロジェクトを作成します ※なおWindows(WSL2未使用)をお使いの方は、先にコチラを実行してから下記を実施してください

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

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


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

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


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

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
#中略

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

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


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
#中略

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

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のテンプレート

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


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に1件データを追加します

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


Screenshot from 2021-11-26 08-58-49.png


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

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でデータを1件削除してます。

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

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


#改造してみよう


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

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


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

↓この画像をダウンロードし、piyo.pngとして保存
piyo.png


ダウンロードした画像をpriv/static/images/piyo.pngへ移動
Screenshot from 2021-11-26 09-23-29.png


更新前

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>

#中略

更新後

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


##おわり

明日は、@kn339264さんで「Elixir女子部のオーガナイザーをやってみた話」です

18
3
5

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
18
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?