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

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

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


**【関連記事】**
[Phoenix LiveView と キーボードイベント - Qiita](https://qiita.com/sand/items/a72a74ce7885a80eb969)
[Phoenix LiveView の JavaScript Hook - Qiita](https://qiita.com/sand/items/66049160bf5309e272ea)
[Phoenix LiveViewの基本設定 - Qiita](https://qiita.com/sand/items/5aea96852ceb2981021b)
[Phoenix1.6の基本的な仕組み - Qiita](https://qiita.com/sand/items/f42dc35c47589eab687e)


**実行環境**
Elixir 1.13.0 
Phoenix 1.6.12

# １．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](https://hexdocs.pm/phoenix_live_view/js-interop.html#client-hooks-via-phx-hook)

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

```javascript: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 を設定します。

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



# ２．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** の指定を忘れずに行います。

```elixir: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 を以下のものに置き換えます。
```elixir: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で作成しクライアントに渡します。クライアント側はそれをそのまま表示しているだけです。


```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 ドキュメント」](https://hexdocs.pm/phoenix_pubsub/Phoenix.PubSub.html)　を参照。

* **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} でブラウザに反映させます。


# ３．動作画像

サーバを起動します。
```
mix phx.server
```

ブラウザを2つ立ち上げ、それぞれ http://localhost:4000/ にアクセスします。2つのブラウザに同じ初期状態画面が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/301e8bd8-9857-698d-d11a-b2987421e195.png)


片方のブラウザにフォーカスを合わせ、キー入力で右下に移動させます。別のブラウザでも、自動的に連動して右下に移動していくのが観測できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/0e4a7218-33d8-93e6-bbec-5da5f0747098.png)


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