LoginSignup
4
2

More than 1 year has passed since last update.

Phoenix LiveViewで入力中のユーザーを表示するやつをつくる

Posted at

やりたいこと

他のユーザーがフォームに入力中だったら2人が入力中...みたいな感じで表示するやつをphoenixでつくりたい

slackとかdiscordみたいなやつ

※用語の使いまわしなどがあまり分かっていないので違和感のある表現の場合があります

実装イメージ。左のユーザーが入力すると右のユーザー(別アカウントでログイン)に表示されてます。

image.png

動作環境

$ elixir -v
Erlang/OTP 23 [erts-11.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe] [dtrace]

Elixir 1.11.4 (compiled with Erlang/OTP 23)

$ mix phx.new -v      
Phoenix v1.5.10

実装方針

Phoenix.PubSubを使います

  • 入力中の情報を更新
  • 入力中の情報を一定時間で削除

に分けて説明します

入力中のユーザーの情報は以下のようなmapassignsで管理して表示を更新していきます。

%{typing_users: %{user_id => ttl}}

今回は入力人数を表示するのでこの形式ですが、mapの中身を工夫すれば入力中のユーザー名の表示など改良ができそうな予感がしています。

ttlTime.add(Time.utc_now, 5)みたいな感じで表示する期間を管理します。

入力中の情報を更新

  1. ページ読み込み時にPhoenix.PubSub.subscribe/2で購読
  2. フォームの入力を受け取るイベントをhandle_event/3で受け取ってPhoenix.PubSub.broadcast/3で送信
  3. 2のイベントをhandle_info/2で受け取って入力中の情報をassignsに追加
  4. 更新後のassignsをみて表示切り替え

入力中の情報を一定時間で削除

  1. ページ読み込み時に1秒ごとに定期実行させる:hide_typingイベントを設定
  2. assigns.typing_usersからTTLが未来のものをフィルター
  3. 表示切り替え

実装

プロジェクト作成時に--liveを付けておくと予めLiveView周りの設定が完了された状態でプロジェクトが作成されます

mix phx.new my_app --live

routing、DB周り、ログイン周りは省略します

ログイン周りはphx_gen_authを使って以下のコマンドでUserを作成しています。

mix phx.gen.auth Users User users

1.入力中の情報を更新

1-1.ページ読み込み時にPhoenix.PubSub.subscribe/2で購読

mount/3(ページ読み込み時に呼ばれる)にてsubscribeを呼びます。
"topic"のところにその部屋のIDなどを入れることで部屋によって分けることができます。

lib/my_app_web/live/post_live/index.ex
def mount(_params, session, socket) do
  current_user = MyApp.Users.get_user_by_session_token(session["user_token"])
  # ...
  if connected?(socket) do
    Phoenix.PubSub.subscribe(MyApp.PubSub, "topic")
  end
  # ...
  socket
  |> assign(:current_user, current_user)
  |> assign(:typing_users, %{})
end

1-2. フォームの入力を受け取るイベントをhandle_event/3で受け取ってPhoenix.PubSub.broadcast/3で送信

フォーム側はphx_changeでイベントを送ります。
今回はテーブルに紐づく前提として作っているためform_forを使っています。
とりあえず動かしたい場合はform_forなくてもいけそうです。

lib/my_app_web/live/post_live/index.html.leex
<%= f = form_for Post.changeset(%Post{}, %{}), "#", phx_submit: "submit", phx_change: "typing" %>
  <%= textarea f, :body %>
  <%= submit "送信", phx_disable_with: "Submitting..." %>
<% end %>

補足:form_forを使っていると特定のtextareaに対してphx_changeが使えないみたいです

イベント受け取り側はhandle_event/3で受け取りbroadcastでuser_idを含めて送信します。
ここでのsocket.assigns.current_user.idは入力したユーザーになります。

:lib/my_app_web/live/post_live/index.ex
def handle_event("typing", _params, socket) do
  Phoenix.PubSub.broadcast(MyApp.PubSub, "topic", {:typing, socket.assigns.current_user.id})
  {:noreply, socket}
end

1-3. 2のイベントをhandle_info/2で受け取って入力中の情報をassignsに追加

1-2のイベントを受け取ります。
{:typing, socket.assigns.current_user.id}をパターンマッチで受け取ります。
Map.put/3で既存のuser_idはTTL更新して、存在しない場合は新規で作成します。

def handle_info({:typing, user_id}, socket) do
  cond do
    # 自分の場合は表示しない
    user_id == socket.assigns.current_user.id ->
      {:noreply, socket}
    true ->
      # とりあえずTTLは5秒
      {:noreply, socket |> update(:typing_users, fn users -> Map.put(users, user_id, Time.add(Time.utc_now, 5)) end)}
  end
end

1-4. 更新後のassignsをみて表示切り替え

assignsは更新されると表示側にも更新がかかります。(更新の範囲とかはあまり把握していません...)
if文で<p>タグの透明度を操作しています。表示の切り替えを行っても良いのですが、表示物がガタガタ動いてしまうので透明度いじってます。

lib/my_app_web/live/post_live/index.html.leex
<p style=<%= if Kernel.map_size(@typing_users) == 0, do: "opacity:0;", else: "opacity:1;" %>>
  <%= Kernel.map_size(@typing_users) %>人が入力中
</p>

2.入力中の情報を一定時間で削除

2-1. ページ読み込み時に1秒ごとに定期実行させる:hide_typingイベントを設定

チュートリアルにある:tickと同じような形でイベントを設定します。

lib/my_app_web/live/post_live/index.ex
def mount(_params, session, socket) do
  # ...
  if connected?(socket) do
    Phoenix.PubSub.subscribe(MyApp.PubSub, "topic")
    :timer.send_interval(1000, self(), :hide_typing) # <- add
  end
  # ...
end

2-2. assigns.typing_usersからTTLが未来のものをフィルター

handle_info/2で1-1のイベントを受け取る処理を書きます。
現在の時刻とassignsから取得できる時刻を比較してmapをフィルターします。

lib/my_app_web/live/post_live/index.ex
def handle_info(:hide_typing, socket) do
  {:noreply,
   socket
   |> update(:typing_users,
             fn users ->
               Enum.reduce(users, %{}, &(filter_active_users(&1, &2)))
             end)}
end

defp filter_active_users({user_id, time_ex}, acc) do
  cond do
    Time.compare(time_ex, Time.utc_now) == :lt ->
      Map.put(acc, user_id, time_ex)
    true ->
      acc
  end
end

2-3. 表示切り替え

1-4と同じく、assignsに更新がかかるので切り替わります

これで、入力中のユーザーの表示切り替えがリアルタイムで行うことができます。

所感

LiveViewとPubSubを使うことでイベントの受け取りと通知と表示切り替えがPhoenixだけで完結するのでめちゃくちゃ便利だなと思いました。

まだまだElixirの日本語の情報は少ないのでちょっとしたことでも書いていけたらと思います。

4
2
1

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
4
2