Phoenix LiveViewのJavaScript Hook の設定記事です。chartjsで簡単なチャートを描くプログラムですサーバ側で生成したデータをリアルタイムにクライアント側のチャートに反映させます。
JavaScript interoperability
【関連記事】
Phoenix LiveView と キーボードイベント - Qiita
Phoenix LiveView の JavaScript Hook - Qiita
Phoenix LiveViewの基本設定 - Qiita
Phoenix1.6の基本的な仕組み - Qiita
実行環境
Elixir 1.13.0
Phoenix 1.6.12
1.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 という名前で作成します。
defmodule LiveviewChartWeb.ChartLive do
use LiveviewChartWeb, :live_view
def mount(_params, _session, socket) do
{:ok, socket}
end
end
ここではrender の明示的な設定は行わず chart_live.html.heex という名前を合わせたファイルを作ることで暗示的に行います。
<h1>LiveView Chart Page</h1>
root.html.heex の body を以下のもので置き換えます。
<body>
<header>
<section class="container">
<h1>LiveView Chart Example</h1>
</section>
</header>
<%= @inner_content %>
</body>
router.ex パスを修正して、"/"パスを LiveView controller を指すようにします。
scope "/", LiveviewChartWeb do
pipe_through :browser
live "/", ChartLive
end
基本設定が終わったのでサーバを起動します。
mix phx.server
http://localhost:4000/ にアクセスします。
成功ですね
2.JavaScript Hook
これからは実際にchartjsを実装し、サーバから送られるイベントに従ってチャートをリアルタイムに更新していくことを考えていきます。
chart.jsをインストールします。
npm install --save --prefix assets chart.js
以下、Hookの説明が続きますが、完全な説明は以下のドキュメントを参照してください。
Client hooks via phx-hook - JavaScript interoperability
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が定義されています。
<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 の要素を指していることに注意してください。
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に設定します。
---
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イベントを送信するタイマーを設定します。
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/ にアクセスします。
2秒おきにグラフが更新されるのを確認できます。
push_event について
ここで push_event の動作を知るために以下のようにデバッグプリントを入れました。
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} でクライアントに送られているようです。
今回は以上です。