Phoenix LiveViewの基本設定

Phoenix LiveViewの基本設定に関する備忘録です。**phx.gen.html**で生成したユーザ登録のHTMLをSPA化（LiveView化)したイメージです。ユーザ登録フォームとユーザ一覧からなるページです。ユーザ登録すると、ページ遷移することなしに、自動的にユーザ一覧が更新されます。
[Phoenix1.6の基本的な仕組み - Qiita](https://qiita.com/sand/items/f42dc35c47589eab687e)

**【参考チュートリアル】**
[LiveView Chat Tutorial](https://github.com/dwyl/phoenix-liveview-chat-example)

**【過去記事】**
* LiveView関連
[東京電力電力供給状況監視 - Phoenix LiveView](https://qiita.com/sand/items/ecac700bc3e32f106279)
[Phoenix LiveView と キーボードイベント - Qiita](https://qiita.com/sand/items/a72a74ce7885a80eb969)
[Phoenix LiveView の JavaScript Hook - Qiita](https://qiita.com/sand/items/66049160bf5309e272ea)
[**Phoenix LiveView とTailwind と daisyUI - Qiita**](https://qiita.com/sand/items/1c2fa09b9d8cc5e0c26b)
[Phoenix LiveViewの基本設定 - Qiita](https://qiita.com/sand/items/5aea96852ceb2981021b)
[**Phoenix1.6の基本的な仕組み - Qiita**](https://qiita.com/sand/items/f42dc35c47589eab687e)

* 認証関連
[Phoenix 認証システム - mix phx.gen.auth](https://qiita.com/sand/items/dda77cc3da3febf3c286)
[Elixir/Phoenix のシンプル認証 auth_plug](https://qiita.com/sand/items/160af41bd019e9c9f388)

* Ecto関連
[Elixir Ecto チュートリアル - Qiita](https://qiita.com/sand/items/0861ad7a16f7ac612f41)
[Elixir Ecto のまとめ - Qiita](https://qiita.com/sand/items/71d0b35d74a4781f3564)
[Elixir Ecto Association - Oiita](https://qiita.com/sand/items/5581497972473e308f05)


**実行環境**
Elixir 1.13.0 
Phoenix 1.6.12
postgreSQL設定済み

# １．LiveViewの基本設定

## １－１．プロジェクト作成

まずはプロジェクトを作成します。

```
mix phx.new liveview_people --no-mailer --no-dashboard
cd liveview_people
mix ecto.setup
```

::: note warn
PostgreSQL のポートが正しくないと、mix ecto.setup でエラーになります。私の Windows の環境ではデフォルトの 5432 ではなく 5433 だったので以下のように変更する必要がありました。
```elixir:config\dev.exs
config :liveview_people, LiveviewPeople.Repo,
  username: "postgres",
  password: "postgres",
  hostname: "localhost",
  database: "liveview_people_dev",
  port: 5433,
  stacktrace: true,
  show_sensitive_data_on_connection_error: true,
  pool_size: 10
```
:::

## １－２．LiveView controller

LiveViewの慣例に従ってlib/liveview_people_web の下に liveディレクトリを作成します。次に **LiveView controller** の **people_live.ex** を作成します。基本的なLiveViewの設定はここで行います。

```elixir:lib/liveview_people_web/live/people_live.ex
defmodule LiveviewPeopleWeb.PeopleLive do
  use LiveviewPeopleWeb, :live_view

  def mount(_params, _session, socket) do
    {:ok, socket}
  end

  def render(assigns) do
    LiveviewPeopleWeb.UserView.render("users.html", assigns)
  end
end
```


通常 **LiveView controller** には以下の3つを定義します。
* (1) **mount**関数
* (2) **render**関数
* (3) **event handler**

**mount**関数でviewの初期状態を定義します。今回の場合、後で見るように **changeset** と **users** の初期状態を設定することになります。
**render**関数では描画の仕方を指定します。ここではusers.html.heexを指定します。拡張子が**heex**であることに注意してください。

::: note warn
render関数を明示的に設定せずに、lib/liveview_people_web/live/people_live.html.heex というテンプレートファイルを作成し、暗示的に rennder関数を指定することも可能な場合もあります。--> [「Phoenix LiveView の JavaScript Hook」](https://qiita.com/sand/items/66049160bf5309e272ea) しかし私のWindows環境では明示的に指定しないとエラーになりましたが、現在原因の特定はできていません。
:::


**event handler**はHTMLに組み込まれたボタンを押したときに発生するイベントなどのcallbackを定義します。今回の場合、changesetとusersを更新することで、HTMLの表示に変更を与えます。


viewの基本的な設定を行います。
```elixir:lib/liveview_people_web/views/user_view.ex
defmodule LiveviewPeopleWeb.UserView do
  use LiveviewPeopleWeb, :view
end
```



ここでrender関数で指定した以下の外部ファイルを用意します。拡張子は**heex**です。
```elixir:lib/liveview_people_web/templates/user/users.html.heex
<h1>LiveView People Page</h1>
```

またroot.html.heexの body部分を以下のものに置き換えます。
```elixir:lib/liveview_people_web/templates/layout/root.html.heex
<body>
  <header>
    <section class="container">
      <h1>LiveView People Example</h1>
    </section>
  </header>
  <%= @inner_content %>
</body>
```


## １－３．routerの修正

router.exの"/"パスを以下のように**LiveView controller**を指すように修正します。
```elixir:lib/liveview_people_web/router.ex
  scope "/", LiveviewPeopleWeb do
    pipe_through :browser

    live "/", PeopleLive
  end
```


## １－４．基本設定の確認

以上でLivwViewの基本設定が終わったので、サーバを起動し確認します。

```
mix phx.server
```

http://localhost:4000/ にアクセスして確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/43353b31-3dc8-0076-0820-a5a8878ffffb.png)


無事ページの表示ができたので成功です。次のステップでLiveViewの肉付けを行っていきます。


# ２．次のステップ

## ２－１．Schema作成など
次のコマンドで**Schema**を作成します。ユーザ登録用のテーブルを用意します。

```
mix phx.gen.schema User users first_name:string last_name:string age:integer
mix ecto.migrate
```

Schema モジュールを以下のように修正します。特別なcontext moduleは用意しませんので、ここにcreate_userとlist_usersを定義します。

```elixir:lib/liveview_people/user.ex
defmodule LiveviewPeople.User do
  use Ecto.Schema
  import Ecto.Changeset
  import Ecto.Query
  alias LiveviewPeople.Repo
  alias __MODULE__


  schema "users" do
    field :age, :integer
    field :first_name, :string
    field :last_name, :string

    timestamps()
  end

  @doc false
  def changeset(user, attrs) do
    user
    |> cast(attrs, [:first_name, :last_name, :age])
    |> validate_required([:first_name, :last_name, :age])
  end

  def create_user(attrs) do
    %User{}
    |> changeset(attrs)
    |> Repo.insert()
  end

  def list_users do
    User
    |> Repo.all()
  end
end
```

**LiveView controller** を以下のように修正します。


```elixir:lib/liveview_people_web/live/people_live.ex
defmodule LiveviewPeopleWeb.PeopleLive do
  use LiveviewPeopleWeb, :live_view
  alias LiveviewPeople.User

  def mount(_params, _session, socket) do
    users = User.list_users() |> Enum.reverse()
    changeset = User.changeset(%User{}, %{})
    {:ok, assign(socket, changeset: changeset, users: users)}
  end

  def handle_event("new_user", %{"user" => params}, socket) do
    case User.create_user(params) do
      {:error, changeset} ->
        {:noreply, assign(socket, changeset: changeset)}

      {:ok, _user} ->
        changeset = User.changeset(%User{}, %{})
        users = User.list_users() |> Enum.reverse()
        {:noreply, assign(socket, changeset: changeset, users: users)}
      end
  end

  def render(assigns) do
    LiveviewPeopleWeb.UserView.render("users.html", assigns)
  end
end
```

**mount** では users と changeset の初期値を設定します。これが template に反映され初期画面となります。
またユーザ登録に対応した "new_user" イベントに対する event handler を定義します。エラー時にはchangeset にエラーの内容が渡されます。成功時は users を取得し直して template に反映させます。ページ遷移なしに反映されるのが **LiveView** の醍醐味ですね。


::: note info
**changeset = User.changeset(%User{}, %{})**
この changeset は「validation: :required」エラーを含んだまま、template の formタグに渡されますが、エラー表示はされません。**changeset.action=nil** だからです。 ==> [A note on :errors](https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html#module-a-note-on-errors) 
:::



templateを以下のように更新します。

```elixir:lib/liveview_people_web/templates/user/users.html.leex
<.form let={f} for={@changeset}  id="form" phx-submit="new_user">
  <%= if @changeset.action do %>
    <div class="alert alert-danger">
      <p>Oops, something went wrong! Please check the errors below.</p>
    </div>
  <% end %>

  <%= label f, :first_name %>
  <%= text_input f, :first_name, id: "first_name", placeholder: "first_name"  %>
  <%= error_tag f, :first_name %>

  <%= label f, :last_name %>
  <%= text_input f, :last_name, id: "last_name", placeholder: "last_name"  %>
  <%= error_tag f, :last_name %>

  <%= label f, :age %>
  <%= number_input f, :age, id: "age", placeholder: "age"  %>
  <%= error_tag f, :age %>

  <div>
    <%= submit "Save" %>
  </div>
</.form>

<h1>Listing Users</h1>
<table>
  <thead>
    <tr>
      <th>First name</th>
      <th>Last name</th>
      <th>Age</th>
    </tr>
  </thead>
  <tbody>
<%= for user <- @users do %>
    <tr>
      <td><%= user.first_name %></td>
      <td><%= user.last_name %></td>
      <td><%= user.age %></td>
    </tr>
<% end %>
  </tbody>
</table>
```
これは基本的には **「mix phx.gen.html」** コマンドで生成したindex.html.heexとform.html.heexを合成したHTMLです。特にこのform構文は **heex** でないと使えないので注意が必要です。詳細は以下の過去記事を参照してください。
[Phoenix1.6の基本的な仕組み - Qiita](https://qiita.com/sand/items/f42dc35c47589eab687e)


初期画面です。changesもusersも空なので、空白のページとなっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/6e24254b-dacf-6c76-cc7e-128fd3402744.png)

３人のユーザを登録した画面です。usersには3人入っている状態です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/e7196777-c50c-ba03-8b12-bfdf55371f49.png)



４人目を登録しようとして、Last Nameが未入力でエラーになった状態です。ちゃんとエラーがでて、エラーじゃないfieldは値が入ったままであることに注意してください。**changeset.changes** にはエラーでないfieldの値が保持され、**changeset.errors** にはエラーfieldに対するメッセージが入っています。エラーかどうかは**changeset.action** がnilであるかどうかで判断します。成功時はnilですが、今回のエラーの場合、**changeset.action** に"insert"が入っているのでRepo.insert()で失敗したことがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/717d82be-99be-93d7-298c-3a6f436c5754.png)


今回は以上です。