LiveViewで認証付きのチャットアプリを構築する(LiveViewの作成)

はじめに

前回までで、コンテキスト周りが一通り完成しました。
今回は、いよいよLiveViewでチャットの機能を実装していきます。
本記事で、LiveViewを用いたコメントの表示が可能になります。

本テーマについて

本記テーマは7記事構成になっています。
7記事それぞれで取り扱う内容は以下です。

  1. プロジェクト作成からログイン処理まで
  2. 認証機能の作成
  3. チャットボードのコンテキスト周り作成
  4. チャットボードの画面周り作成(本記事)
  5. Phoenix.PubSubを使用したリアルタイム更新
  6. Phoenix.Presenceを使用したアクティブユーザー表示
  7. 入力ステータスの表示

本テーマは以下の方針で記述します。

  • 機能実装は本テーマに沿った必要最小限のみ
  • UIもほぼそのまま
  • 機能へのアクセスはURLで直に指定を想定

完成版の画面イメージは以下のようになっています。

lv_chat

LiveViewによるチャットボード画面の作成

前回の記事で作成されていたBoardControllerをLiveView用に修正します。

コントローラーの変更

もともとの render/2 を、

# /lib/lv_chat_web/controllers/board_controller.ex
def show(conn, %{"id" => id}, _current_user) do
  board = Meeting.get_board!(id)
  render(conn, "show.html", board: board)end

live_render/3 で置き換えます。

# /lib/lv_chat_web/controllers/board_controller.ex
alias Phoenix.LiveView

def show(conn, %{"id" => id}, current_user) do
  board = Meeting.get_board!(id)
  LiveView.Controller.live_render(    conn,    LvChatWeb.BoardLiveView,    session: %{      board: board,      current_user: current_user,    }  )end

BoardLiveViewモジュールの追加

/lib/lv_chat_web/views/board_live_view.ex というファイルを作成します。

以下の3つの関数を実装します。

  • render/1
  • mount/2
  • handle_event/3

render/1

Phoenix.View.renderを使用します。

def render(assigns) do
  Phoenix.View.render(BoardView, "show.html", assigns)
end

mount/2

初期表示時は、すべてのコメントを読み込むようにします。

def mount(session = %{current_user: _user, board: board}, socket) do
  assigns =
    Map.merge(
      session,
      %{
        comments: board |> Meeting.list_comments,
        comment:  LvChat.Meeting.change_comment
      }
    )

  {:ok, assign(socket, assigns)}
end

handle_event/3

form側で指定されている phx-submit="add_comment" に対応するイベントハンドラーを作成します。
チャットボードにコメントを追加し、成功したら状態を更新します。

def handle_event(
      "add_comment",
      %{"comment" => cmnt},
      socket = %{assigns: %{board: board, current_user: user}}
    ) do
  case Meeting.comment_to_board(user, board.id, cmnt) do
    {:ok, comment} ->
      comments = board |> Meeting.list_comments
      {:noreply, assign(socket, comment: LvChat.Meeting.change_comment, comments: comments)}

    {:error, _changeset} ->
      {:noreply, socket}
  end
end

BoardLiveViewモジュール全体

BoardLiveView全体は以下になります。

defmodule LvChatWeb.BoardLiveView do
  use Phoenix.LiveView
  alias LvChatWeb.BoardView
  alias LvChat.Meeting

  def render(assigns) do
    Phoenix.View.render(BoardView, "show.html", assigns)
  end

  def mount(session = %{current_user: _user, board: board}, socket) do
    assigns =
      Map.merge(
        session,
        %{
          comments: board |> Meeting.list_comments,
          comment:  LvChat.Meeting.change_comment
        }
      )

    {:ok, assign(socket, assigns)}
  end

  def handle_event(
        "add_comment",
        %{"comment" => cmnt},
        socket = %{assigns: %{board: board, current_user: user}}
      ) do
    case Meeting.comment_to_board(user, board.id, cmnt) do
      {:ok, comment} ->
        comments = board |> Meeting.list_comments
        {:noreply, assign(socket, comment: LvChat.Meeting.change_comment(), comments: comments)}

      {:error, _changeset} ->
        {:noreply, socket}
    end
  end

end

テンプレートの変更

拡張子の変更

フレームワークにLiveViewのテンプレートであることを認識させるために、コマンドで生成されたファイル名の拡張子を eex から leex に変更します。

/lib/lv_chat_web/templates/board/show.html.leex

入力フォーム部

入力フォームでボタン押下時には、phx-submit="add_comment" を指定します。

<div class="form-group">
  <%= f = form_for @comment, "#", [phx_submit: :add_comment] %>
    <%= text_input f, :body %>
    <%= submit "送信" %>
  </form>
</div>

メッセージ表示部

メッセージ表示部には、以下の定義を行います。

定義設定値備考
idmsg-container要素へのアクセスに使用します。
phx-updatereplaceデフォルト値です。古い定義を新しい定義で置き換えます。
phx-hookNewCommentDOMの変化等によりJs側の関数を呼び出すようになります。
styleheight: 200px; overflow-y: scroll;インラインスタイルです。
# /lib/lv_chat_web/templates/board/show.html.leex
<div
  id="msg-container"
  phx-update="replace"
  phx-hook="NewComment"
  style="height: 200px; overflow-y: scroll;"
>
  <ul style="list-style: none;">
  <%= for comment <- @comments do %>
    <li id="<%= comment.id %>"> <%= comment.body %> </li>
  <% end %>
</ul>
</div>

全体表示

全体は以下のようになります。

# /lib/lv_chat_web/templates/board/show.html.leex
<h1>Show Board</h1>

<ul>
  <li>
    <strong>Name:</strong>
    <%= @board.name %>
  </li>

  <li>
    <strong>Description:</strong>
    <%= @board.description %>
  </li>

</ul>

<div class="form-group">
  <%= f = form_for @comment, "#", [phx_submit: :add_comment] %>
    <%= text_input f, :body, value: @comment.changes[:body] %>
    <%= submit "送信" %>
  </form>
</div>
<div id="msg-container" phx-update="replace" style="height: 200px; overflow-y: scroll;" phx-hook="NewComment">
  <ul style="list-style: none;">
  <%= for comment <- @comments do %>
    <li id="<%= comment.id %>"> <%= comment.body %> </li>
  <% end %>
</ul>
</div>

フックの実装

投稿が増えてくると、新しい投稿があってもそのままでは表示されなくなります。
表示するようにするためには、投稿するたびに最下端までスクロールをする必要があります。
そのような場合に使用できるのが、phx-hook です。
この記述により、所定のタイミングでJavaScript側の指定した関数が呼び出されるようになります。

  • phx-hook="NewComment"で指定された動作の定義
  • マウント時に最下端までスクロール
  • DOMの状態が更新されたときに最下端までスクロール
// /assets/js/app.js
let Hooks = {}

const scrollToBottomByElement = (element) => {
  if (element) {
    element.scrollTop = element.scrollHeight
  }
}

Hooks.NewComment = {
  mounted() {
    scrollToBottomByElement(document.getElementById("msg-container"));
  },
  updated() {
    scrollToBottomByElement(document.getElementById("msg-container"));
    // this.pushEvent("some_event",{});
  }
}

let liveSocket = new LiveSocket("/live", Socket, { hooks: Hooks })
liveSocket.connect()

これで、LiveViewを使って投稿の表示も行うことができるようになりました。

終わりに

ここまでで、LiveViewを使って投稿を表示できるようになりました。
ただし、現状ではまだリアルタイムに他のユーザーにコメントを伝搬させることはできません。
そのためには、もう人手間必要になります。
それは次回に記述します。

Phoenix.PubSubを使用したリアルタイム更新