3
1

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.

Phoenix LiveView と キーボードイベント

Last updated at Posted at 2022-10-17

Phoenix LiveView で キーボードイベントをひろうプログラムです。LiveViewJavaScript Hook を組み合わせたものとなります。クライアント要素の状態管理やロジックは Elixir 側で行い、JavaScript では単にキーボードイベントを拾い Elixir に渡すだけのシンプルなものとなります。Elixir 側で状態を更新することで、LiveView 画面が自動更新されます。

プログラムの全体像やポイントは、箇条書き的に書くと以下のようになります。

  • ブラウザ上のオブジェクトをキーボード操作で上下左右に動かす
  • オブジェクトはdiv要素とし、その絶対座標を動的に変更することで動作させる
  • キーボードのイベントはJavaScriptの window.addEventListener() を利用する
  • JavaScriptからElixirへはJavaScript Hooklife-cycle callbacks メソッドの pushEvent() を使う
  • イベントを受け取った Elixir 側では押されたキーに従って座標を更新する
  • 更新された座標は Phoenix.PubSub で全ブラウザに通知される
  • 各々のブラウザの画像は、LiveView のメカニズムにより、更新された座標で自動更新される。

【関連記事】
Phoenix LiveView と キーボードイベント - Qiita
Phoenix LiveView の JavaScript Hook - Qiita
Phoenix LiveViewの基本設定 - Qiita
Phoenix1.6の基本的な仕組み - Qiita

実行環境
Elixir 1.13.0
Phoenix 1.6.12

1.JavaScript Hookの設定

mix phx.new liveview_keyboard --no-ecto --no-mailer --no-dashboard
cd liveview_keyboard

まずはクライアントのJavaScript Hook の設定から行います。以下、Hookの完全な説明は以下のドキュメントを参照してください。
Client hooks via phx-hook - JavaScript interoperability

keyboard_hook.jsでは hooks.keyboard という名前で Hook object を設定します。これを受けて、後述するphx-hook の指定は phx-hook="keyboard" となります。これはキーボードイベントを拾ってElixirに送るプログラムです。life-cycle callbacksメソッドの pushEvent() の利用に注意してください。

assets/js/keyboard_hook.js
let hooks = {};
hooks.keyboard={
    mounted(){
        window.addEventListener("keydown", e => {
            console.log(e.key)
            this.pushEvent("key_down", {key: e.key})
        });
    }
}

export default hooks

app.js に Hook を設定します。

assets/js/app.js
import keyboard_hooks from './keyboard_hook'
---
let liveSocket = new LiveSocket("/live", Socket,
    {params: {_csrf_token: csrfToken}, hooks: keyboard_hooks})

2.LiveView controller の作成

まずは慣例に従って live ディレクトリを作成します。

mkdir lib/liveview_keyboard_web/live

renderのhtmlを作成します。親要素のdivを position relativeで作成し、子要素のdivをposition absoluteで作成します。これで子要素のx座標、y座標を left、top で指定することができるようになります。leftとtopの値をelixir側で動的に変更することで、この子要素を動作させることができます。
ここでphx-hook の指定を忘れずに行います。

lib/liveview_keyboard_web/live/keyboard_live.html.heex
<style>
  .absolute_parent {
    position:  relative;
    left:  30px;
    top: 30px;
    border: solid 2px red;
    height:  600px;
    width: 800px;
  }

  .absolute_child {
    position:  absolute;
    background-color: #ccc;
    padding:  5px;
    border: solid 5px black;
  }
</style>
<div id="myparent" class="absolute_parent" phx-hook="keyboard">
    <div id="mykeyboard" class="absolute_child" style={@style}>
        Me
    </div>
</div>

ちょっと見やすい画面にするために root.html.heex の body を以下のものに置き換えます。

lib/liveview_keyboard_web/templates/layout/root.html.heex
  <body>
    <header>
      <section class="container">
        <h1>LiveView Keyboard Example</h1>
      </section>
    </header>
    <%= @inner_content %>
  </body>

最後に LiveView controller を作成します。私的には、LiveView では JavaScript側はできるだけシンプルなコードにして、複雑な状態管理やロジックはElixir側で取り込む方針でできればと考えています。今回もJavaScriptやHtmlはとてもシンプルになっており、ほとんどの処理はこの LiveView controller で書いているといっても過言ではありません。

キーボードイベントの識別と、それによるdiv要素のx座標、y座標の更新を行っています。x座標、y座標は socket.assigns で管理します。CSSのstyleもElixirで作成しクライアントに渡します。クライアント側はそれをそのまま表示しているだけです。

lib/liveview_keyboard_web/live/keyboard_live.ex
defmodule LiveviewKeyboardWeb.KeyboardLive do
  use LiveviewKeyboardWeb, :live_view
  alias Phoenix.PubSub

  def mount(_params, _session, socket) do
    if connected?(socket) do
      PubSub.subscribe(LiveviewKeyboard.PubSub, "liveview_keyboard")
    end

    x_pos = 300
    y_pos = 100
    style = make_style(x_pos, y_pos)
    socket = assign(socket, style: style, x_pos: x_pos, y_pos: y_pos)
    {:ok, socket}
  end

  def handle_info(%{style: style , x_pos: x_pos , y_pos: y_pos}, socket) do
    socket = assign(socket, style: style, x_pos: x_pos, y_pos: y_pos)
    {:noreply, socket}
  end

  def handle_event("key_down", %{"key" => key}=params, %{assigns: assigns}=socket) do
    %{x_pos: x_pos, y_pos: y_pos} = assigns
    {x_pos, y_pos} =
      case key do
        "w"-> {x_pos, y_pos - 10}
        "s"-> {x_pos, y_pos + 10}
        "a"-> {x_pos - 10, y_pos}
        "d"-> {x_pos + 10, y_pos}
        _-> {x_pos, y_pos}
      end
    style = make_style(x_pos, y_pos)
    PubSub.broadcast(LiveviewKeyboard.PubSub, "liveview_keyboard",
                     %{style: style , x_pos: x_pos , y_pos: y_pos} )
    {:noreply, socket}
  end

  defp make_style(x_pos, y_pos) do
    "left: " <>  Integer.to_string(x_pos) <> "px; top: " <>
        Integer.to_string(y_pos) <> "px;"
  end
end

以下、もう少し詳細なコードの説明です。Phoenix.PubSub については「Phoenix.PubSub ドキュメント」 を参照。

  • mount() は2度呼ばれますが、2度目にsocketがつながった状態で呼ばれます。この時、PubSub.subscribe() で指定トピックに参加します。また初期値のx座標、y座標に基づいたstyleを作成し、socket.assigns に保存します。
  • handle_event() はクライアント側のJavaScriptから送られたイベントを受け取ります。ここで送られたイベントデータを元に新しいx座標、y座標を計算し、それに基づいた新しいstyleを作ります。これらの値は PubSub.broadcast() でトピックに参加している全員にブロードキャストされます。
  • handle_info() ではブロードキャストされた値を受け取り、それぞれ socket.assigns を更新します。後は更新された値を {:noreply, socket} でブラウザに反映させます。

3.動作画像

サーバを起動します。

mix phx.server

ブラウザを2つ立ち上げ、それぞれ http://localhost:4000/ にアクセスします。2つのブラウザに同じ初期状態画面が表示されます。
image.png

片方のブラウザにフォーカスを合わせ、キー入力で右下に移動させます。別のブラウザでも、自動的に連動して右下に移動していくのが観測できます。
image.png

うまく動作しました。
今回は以上です。

3
1
0

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?