Phoenix LiveView の JavaScript Hook

Phoenix LiveViewのJavaScript Hook の設定記事です。chartjsで簡単なチャートを描くプログラムですサーバ側で生成したデータをリアルタイムにクライアント側のチャートに反映させます。
[JavaScript interoperability](https://hexdocs.pm/phoenix_live_view/js-interop.html)

**【関連記事】**
[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


# １．LiveViewの基本設定

まずプロジェクトを作成します。
```
mix phx.new liveview_chart --no-ecto --no-mailer --no-dashboard
cd liveview_chart
```

次に慣例に従ってliveディレクトリを作成します
```
mkdir lib/liveview_chart_web/live
```

**LiveView controller** を **chart_live.ex** という名前で作成します。

```elixir:lib/liveview_chart_web/live/chart_live.ex
defmodule LiveviewChartWeb.ChartLive do
  use LiveviewChartWeb, :live_view

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

ここでは**render** の明示的な設定は行わず **chart_live.html.heex** という名前を合わせたファイルを作ることで暗示的に行います。

```elixir:lib/liveview_chart_web/live/chart_live.html.heex
<h1>LiveView Chart Page</h1>
```

**root.html.heex** の body を以下のもので置き換えます。
```elixir:lib/liveview_chart_web/templates/layout/root.html.heex
  <body>
    <header>
      <section class="container">
        <h1>LiveView Chart Example</h1>
      </section>
    </header>
    <%= @inner_content %>
  </body>
```

**router.ex** パスを修正して、"/"パスを **LiveView controller** を指すようにします。

```elixir:lib/liveview_chart_web/router.ex
  scope "/", LiveviewChartWeb do
    pipe_through :browser

    live "/", ChartLive
  end
```

基本設定が終わったのでサーバを起動します。

```
mix phx.server
```

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/0684e1e4-6468-3c88-50a2-c2c69fcedaae.png)

成功ですね



# ２．JavaScript Hook

これからは実際に**chartjs**を実装し、サーバから送られるイベントに従ってチャートをリアルタイムに更新していくことを考えていきます。

**chart.js**をインストールします。
```
npm install --save --prefix assets chart.js
```

以下、Hookの説明が続きますが、完全な説明は以下のドキュメントを参照してください。
[Client hooks via phx-hook - JavaScript interoperability](https://hexdocs.pm/phoenix_live_view/js-interop.html#client-hooks-via-phx-hook)

#### phx-hook
chartを描くためのcanvasを用意するために、chart_live.html.heexを以下のように修正します。ここでcanvasのstyleも定義しておきます。**phx-hook="chart"** で **Hook object** を指定しています。あとで**chart_hook.js** を定義しますが、そこで **hooks.chart** という名前で **Hook object** を定義します。そこでは**mounted** という**life-cycle callbacks**が定義されています。

```elixir:lib/liveview_chart_web/live/chart_live.html.heex
<style>
  #chart-wrapper {
    display: inline-block;
    position: relative;
    width: 100%;
  }
</style>
<div id="chart-wrapper">
  <canvas id="myChart" phx-hook="chart"></canvas>
</div>
```


#### life-cycle callbacks
**phx-hook** で指定された **Hook object** は以下のような **life-cycle callbacks** を持ちます。

* **mounted** - DOM 要素が追加され、LiveView で mount が終了したときに呼ばれる
* **updated** - サーバによってDOM要素が update されたときに呼ばれる
* **destroyed** - DOM要素がページから削除されたとき（親要素のupdateや親要素全体の削除によって）

#### life-cycle callbacks の属性
life-cycle callbacks からは以下のような属性にアクセスできます。
* **el** - Hook object が束縛された DOM要素への参照
* **pushEvent(event, payload, (reply, ref) => ...)** - client から the LiveView server　へイベントをpushするためのメッソド 
* **handleEvent(event, (payload) => ...)** - server から push されたイベントをハンドルするためのメッソド


**chart_hook.js** では **Hook object** の **hooks.chart** を定義し、chartjsを用いて初期状態のチャートを描いています。また **this.handleEvent()** で、サーバ側から"points"イベントが送信されたときのイベントハンドラーを設定しています。
また **this.el** は canvas の要素を指していることに注意してください。

```elixir:assets/js/chart_hook.js
import Chart from 'chart.js/auto';

const labels = [
  'January',
  'February',
  'March',
  'April',
  'May',
  'June',
];

const data = {
  labels: labels,
  datasets: [{
    label: 'My First dataset',
    backgroundColor: 'rgb(255, 99, 132)',
    borderColor: 'rgb(255, 99, 132)',
    data: [0, 10, 5, 2, 20, 30, 45],
  }]
};

const config = {
  type: 'line',
  data: data,
  options: {}
};

let hooks = {}
hooks.chart = {
    mounted() {
        var ctx = this.el
        var chart = new Chart(ctx, config)
        this.handleEvent("points", ({points}) => {
            chart.data.datasets[0].data = points
            chart.update()
        })
    }
}

export default hooks
```



app.jsでは上で定義したhookを読み込んで、LiveSocketに設定します。


```elixir:assetss/js/app.js
---
import chart_hooks from './chart_hook';  // 追加

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")

let liveSocket = new LiveSocket("/live", Socket, 
    {params: {_csrf_token: csrfToken}, hooks: chart_hooks})  // 修正
---
```

**LiveView controller** のchart_live.exを以下のように変更します。
mountは2度呼ばれますが、2度目にsocketがつながった状態で呼ばれます。この時、2秒おきに自分にupdate_chartイベントを送信するタイマーを設定します。

```elixir:lib/liveview_chart_web/live/chart_live.ex
defmodule LiveviewChartWeb.ChartLive do
  use LiveviewChartWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    if connected?(socket) do
      :timer.send_interval(2000, self(), :update_chart)
    end
    {:ok, socket}
  end

  @impl true
  def handle_info(:update_chart, socket) do
    socket = push_event(socket, "points", %{points: get_points})
    {:noreply, socket}
  end

  defp get_points, do: 1..6 |> Enum.map(fn _ -> :rand.uniform(100) end)
end
```


設定が終わったのでサーバを起動します。

```
mix phx.server
```

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/b11707f4-9fb6-637c-46ea-d055d5cc6f99.png)


2秒おきにグラフが更新されるのを確認できます。



#### push_event について
ここで push_event の動作を知るために以下のようにデバッグプリントを入れました。

```elixir:
    IO.inspect(socket |> Map.from_struct())
    socket = push_event(socket, "points", %{points: get_points})
    IO.inspect(socket |> Map.from_struct())
```

push_event の前後でsocket.private.__changed__の値を見ると、
**push_event前**
```
 %{},
```

**push_event後**
```
%{ push_events: [["points", %{points: [14, 54, 16, 85, 93, 21]}]] },
```

最後に、この情報が {:noreply, socket} でクライアントに送られているようです。

今回は以上です。