Phoenix.LiveViewは現在なお開発がすすんでおり、本内容が古くなっている可能性があります。おかしいなと思ったら公式ドキュメントを参照してください。
LiveViewを使ってアプリケーションを作りたいのですが、
ドキュメントをかじり読みしていると知識の抜けがあったりで遠回りになってしまうことがあるのでエイヤで
https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html
を翻訳してみました。
Google翻訳を多用しつつ、自分で読み返して変な日本語はできるだけ修正/意訳しています。
間違いやおかしな点は指摘をお願いします。
Phoenix.LiveView behaviour
LiveViewはリッチでリアルタイムなユーザー体験をサーバーサイドでレンダリングされたHTMLで提供します。
LiveViewのプログラミングモデルは宣言的です。LiveViewにおけるイベントはステートの変化を引き起こす可能性のある通常のメッセージです。ステートが変化すると、LiveViewは関係するHTMLテンプレート部分を再レンダリングしブラウザーに送ります。それは最も効率的な方法でHTMLを更新します。これは、開発者が他のサーバーサイドでレンダリングされるHTMLのようにLiveViewのテンプレートを書けること、そしてLiveViewがステートのトラッキングおよび関連する差分をブラウザーに送るハードワークを行うことを意味します。
結局のところ、LiveViewはメッセージでイベントを受信し、ステートを更新するプロセス以外のなにものでもありません。ステートそのものは関数的で不変なElixirのデータ構造体にすぎません。イベントは(通常、Phoenix.PubSubによって発行される)内部アプリケーションのメッセージか、クライアント/ブラウザーによって送信されるメッセージのいずれかです。
LiveViewはリッチでリアルタイムなユーザー体験の構築を優れたものにする多くの機能を提供します。
- LiveViewはElixirのプロセスとPhoenix.Channels上に構築することによって、垂直に(小さいインスタンスから大きいインスタンスに)、そして水平に(インスタンスを追加することで)スケールします。
- LiveViewはまず通常のHTTPリクエストの一部として静的にレンダリングされます。これによって、「最初の重要な描画」にかかる時間を短縮し、そして検索/インデックスエンジンにも役立ちます。
- LiveViewは差分トラッキングを行います。LiveViewのステートが変化した場合、テンプレート全体を再レンダリングせず、変化したステートの影響を受ける部分のみが再レンダリングされます。これは通信のレイテンシーと送信するデータ量の減らします。
- LiveViewは静的コンテンツと動的コンテンツをトラッキングします。サーバーレンダリングされたHTMLは静的な(決して変更されない)部分と動的な部分から構成されます。最初のレンダリングでLiveViewは静的コンテンツを送信し、後の更新では変更された動的コンテンツのみが再送信されます。
- (Coming soon)LiveViewはクライアントにメッセージを送るためにErlang Term Formatを使います。このバイナリーベースのフォーマットはサーバー上で非常に効率的で、通信で使用するデータを少なくします。
- (Coming soon)LiveViewにレイテンシーシミュレータが含まれます。これによりレイテンシー増加時にアプリケーションがどのように振る舞うかがシミュレートでき、イベントの処理を待つユーザーに意味のあるフィードバックを提供できるようになります。
さらに、クライアントとサーバー間の永続的なコネクションを維持することで、LiveViewアプリケーションは、認証、デコード、ロードそしてエンコードを全てのリクエストにおいて行う必要のあるステートレスリクエストと比較して、やることが少なく送信するデータが少ないためより素早く反応することができます。反面、LiveViewはステートレスなリクエストと比較してサーバーのメモリを多く使用します。
Use cases
現時点で、LiveViewが非常に適しているユースケースがたくさんあります。
- ユーザーインタラクションとinputs, buttons, formsの処理
入力のバリデーション、動的フォーム、自動補完等 - サーバーによりプッシュされるイベントと更新
通知やダッシュボード等 - ページとデータの移動
ページ間の移動、ページネーション等はLiveViewで構築できますが、現在 back/forwardボタンと移動中にページにリンクする機能が失われます。pushStateのサポートはロードマップにあります。
(現在)サポートが限定されていますが、LiveViewがさらに開発されるにつれファーストクラスになる予定のユースケースがあります。
トランジションとローディングステート
LiveViewのプログラミングモデルはトランジションとローディングステートに優れた基盤を提供します。あるユーザーアクションの後に行われたUIの変化は、サーバーがそのユーザーアクションの更新を送った時点で元に戻されるためです。たとえば、サーバーからの更新が到着した際に自動的に元に戻るボタンをクリックするのは明快です。これはレイテンシーがある場合のユーザーフィードバックとして特に重要です。これらのステートをモデリングする完全な機能セットは将来のバージョンで提供されます。Optimistic UI
トランジションとローディングステートを追加すると、Optimistic UIの構築に必要なビルディングブロックの多くはLiveViewの一部となります。しかしOptimistic UIはサーバーが利用できないときにクライアントで動作するため、Optimistic UIの完全なサポートはJavascriptを書かかずには達成できません。 JSフックを統合する方法については、"JS Interop and client controlled DOM"を参照してください。
(訳者注:Optimistic UI とは
LiveViewが適さないケース
- アニメーション
そもそもサーバーを必要としないアニメーション、メニュー、一般的なイベントはLiveViewに適しません。それらはCSSやCSSトランジションで完全に実現できるからです。
Life-cycle
LiveViewは通常のHTTPリクエストとHTMLレスポンスとして始まり、クライアントの接続によってステートフルなviewにアップグレードします。たとえJavaScriptが無効化されていても通常のHTMLページを保証します。ステートフルなviewが変化したりそのソケットのassignsが更新したりした際はいつでも、viewは自動的に再レンダリングされ、その更新はクライアントにプッシュされます。
LiveViewのレンダリングはviewにセッションデータを提供しつつ、routerやcontrollerから開始します。セッションデータとはviewに必要なパラメータやクッキーのセッション情報等のリクエストの情報に相当します。セッションデータは署名されクライアントに保存された後に、クライアントが接続する際かステートフルviewに再接続する際にサーバーに返されます。コントローラーからviewがレンダリングされた時、 mount/2 コールバックが与えられたセッションデータとLiveViewのソケットを引数に呼び出されます。 mount/2 コールバックはviewのレンダリングに必要なソケットassignsを紐付けます。マウント後に render/1 が呼び出されHTMLが通常のHTMLレスポンスとしてクライアントに送信されます。
署名済セッションで静的ページをレンダリングした後で、LiveViewはレンダリングされた更新をブラウザーにプッシュするためにステートフルなviewが生成されるクライアントから接続し、クライアントのイベントをphxバインディング経由で受け取ります。コントローラーのフローと同様に、 mount/2 は署名済セッションとsockekを引数に呼び出され、レンダリングのための値をアサインします。ただし、接続されたクライアントの場合、LiveViewプロセスは、サーバー上で生成され、 render/1 の結果をクライアントにプッシュし、接続中はプロセスは持続します。ステートフルライフサイクルのいずれかの時点でクラッシュが発生するかまたはクライアント接続が切断された場合、クライアントはサーバーに正常に再接続し、署名済セッションを mount/2 に渡します。
Example
まず、LiveViewは2つのコールバックを必要とします。 mount/2 と render/1 です。
defmodule AppWeb.ThermostatLive do
use Phoenix.LiveView
def render(assigns) do
~L"""
Current temperature: <%= @temperature %>
"""
end
def mount(%{id: id, current_user_id: user_id}, socket) do
temperature = Thermostat.get_user_reading(user_id, id)
{:ok, assign(socket, :temperature, temperature)}
end
end
render/1 コールバックは socket.assigns を受け取り、レンダリングされたコンテンツを返します。LiveViewテンプレートをインライン化するのに Phoenix.LiveView.sigil_L/2 を使うことができます。もし、Phoenix.HTML ヘルパーを使用する場合は、LiveViewの最上位で use Phoenix.HTML することを忘れないでください。
アプリケーションの既存のPhoenix.Viewモジュールにデリゲートすることにより、render/1 コールバック内で別の.leex HTMLテンプレートをレンダリングすることもできます。例えば、
defmodule AppWeb.ThermostatLive do
use Phoenix.LiveView
def render(assigns) do
Phoenix.View.render(AppWeb.PageView, "page.html", assigns)
end
end
LiveViewを定義したら、まずエンドポイントで socket path を定義し、Phoenix.LiveView.Socket に向けます。
defmodule AppWeb.Endpoint do
use Phoenix.Endpoint
socket "/live", Phoenix.LiveView.Socket
...
end
そしてendpointでsigning_saltを設定します。
config :my_app, AppWeb.Endpoint,
...,
live_view: [signing_salt: ...]
mix phx.gen.secret 32 タスクでセキュアでランダムなサイニングソルトを生成することができます。
次に、どこでLiveViewを使うか決めます。
routerから直接LiveViewをサービスできます。必要であれば :session 値を渡すことができます。(オプションについてさらに知りたい場合は Phoenix.LiveView.Router を参照してください)
defmodule AppWeb.Router do
use Phoenix.Router
import Phoenix.LiveView.Router
scope "/", AppWeb do
live "/thermostat", ThermostatLive, session: [:user_id]
end
end
いずれのテンプレートからも live_render を使うこともできます。
<h1>Temperature Control</h1>
<%= Phoenix.LiveView.live_render(@conn, AppWeb.ThermostatLive, session: %{user_id: @user.id}) %>
また、どのコントローラーからでもviewをlive_renderすることができます。
defmodule AppWeb.ThermostatController do
...
import Phoenix.LiveView.Controller
def show(conn, %{"id" => id}) do
live_render(conn, AppWeb.ThermostatLive, session: %{
id: id,
current_user_id: get_session(conn, :user_id),
})
end
end
life-cycleセクションで見たように、viewに対するリクエストの:session データを渡すことができます。そのデータはクッキーにおける現在のユーザidであったり、リクエストのパラメーターです。通常のHTMLレスポンスは署名済みトークンとともに送信されます。そのトークンはLiveViewのセッションデータを含むDOMに埋め込まれています。さらなる情報はlive_render/3を参照ください。
次に、サーバに接続するクライアントのコードです。
import {Socket} from "phoenix"
import LiveSocket from "phoenix_live_view"
let liveSocket = new LiveSocket("/live", Socket)
liveSocket.connect()
NOTE: 包括的なJavaScriptクライアントの使用については、後のセクションで説明します。
クライアントの接続後、 mount/2 は生成されたLiveViewプロセス内で呼び出されます。この時点で、pubsubトピックのサブスクライブ、メッセージ送信等のステートフルな作業を条件に応じて実行させるためにconnected?/1 を使うことができます。たとえば、timerを使ってLiveViewを周期的に更新できます。
defmodule DemoWeb.ThermostatLive do
use Phoenix.LiveView
...
def mount(%{id: id, current_user_id: user_id}, socket) do
if connected?(socket), do: :timer.send_interval(30000, self(), :update)
case Thermostat.get_user_reading(user_id, id) do
{:ok, temperature} ->
{:ok, assign(socket, temperature: temperature, id: id)}
{:error, reason} ->
{:error, reason}
end
end
def handle_info(:update, socket) do
{:ok, temperature} = Thermostat.get_reading(socket.assigns.id)
{:noreply, assign(socket, :temperature, temperature)}
end
end
mount で接続されたステートにソケットがあれば30秒毎にviewにメッセージを送信するのに connected?(socket) を使いました。GenServerと同様に、handle_infoで:updateを受け取り、socket assignsを更新します。socket assignsが変化するたびに、render/1 が自動的に呼び出され、更新がクライアントに送信されます。
Assigns and LiveEEx Templates
LiveViewにおける全てのデータはソケットにassignsとして保存されます。assign/2 と assign/3 はこれらの値を保存するのに役立ちます。これらの値にはLiveViewで socket.assigns.name でアクセスできますが、もっとも一般的なアクセス方法はLiveViewテンプレート内で @name を使うことです。
.leex拡張子または~L シジルで提供されるPhoenix.LiveViewの組み込みテンプレートは、Live EExです。通常の.eexテンプレートと似ていますが、動的な部分から静的な部分を分割し、変更をトラッキングすることで、通信で送信されるデータの量を最小限に抑えるように設計されています。
最初のleexテンプレートのレンダリングで、テンプレートの静的部分と動的部分のすべてがクライアントに送信されます。
その後、サーバー上で行った変更は、動的な部分のみと動的な部分が変更された場合のみ送信します。
変更の追跡は、assignsによって行われます。このテンプレートを想像してください
<div id="user_<%= @user.id %>">
<%= @user.name %>
</div>
@user assignが変化した場合、LiveViewは@user.idと@user.nameを再レンダリングしてブラウザーに送信します。
他のテンプレートも.leexテンプレートかつすべてのassignsが子/内部テンプレートに渡される限り、変化のトラッキングは他のテンプレートをレンダリングする際にも機能します。
<%= render "child_template.html", assigns %>
assignをトラッキングする機能はテンプレート内で直接的な操作を行うことを避けなければならないことを意味しています。たとえば、テンプレート内でデータベースのクエリを実行するような場合
<%= for user <- Repo.all(User) do %>
<%= user.name %>
<% end %>
データベース内のユーザー数が変化しても、Phoenixは上記セクションの再レンダリングを決してしません。そのためテンプレートをレンダリングする前に、LiveViewにユーザーをassignsとして保存する必要があります。
assign(socket, :users, Repo.all(User))
一般的に、LiveViewを使用しているかどうかにかかわらず、データのロードはテンプレート内で行わないでください。LiveViewと他との違いは、それらをベストプラクティスとして強制していることです。
Bindings
Phoenixはclient-serverインタラクションのためにDOM要素へのバインディングをサポートしています。たとえば、ボタンのクリックに反応するには、要素をレンダリングし、
<button phx-click="inc_temperature">+</button>
全てのLiveViewバインディングはサーバーでhandle_eventで処理されます。たとえば
def handle_event("inc_temperature", _value, socket) do
{:ok, new_temp} = Thermostat.inc_temperature(socket.assigns.id)
{:noreply, assign(socket, :temperature, new_temp)}
end
BINDING | ATTRIBUTES |
---|---|
Params | phx-value-* |
Click Events | phx-click |
Focus/Blur Events | phx-blur, phx-focus, phx-target |
Form Events | phx-change, phx-submit, data-phx-error-for, phx-disable-with |
Key Events | phx-keydown, phx-keyup, phx-target |
Rate Limiting | phx-debounce, phx-throttle |
Custom DOM Patching | phx-update |
JS Interop | phx-hook |
Click Events
phx-clickバインディングはクリックイベントをサーバに送信するために使われます。クライアントのイベント、例えばphx-clickのようなイベントがプッシュされた際、その値はサーバーに以下のプライオリティで送られます。
- phx-value- プレフィクスを持つ属性の数値、例えば
<div phx-click="inc" phx-value-myvar1="val1" phx-value-myvar2="val2">
はサーバーに以下のマップを送ります。
def handle_event("inc", %{"myvar1" => "val1", "myvar2" => "val2"}, socket) do
phx-value-プレフィクスが使われた場合、要素にvalue属性があればサーバーのペイロードはそれも含みます。
- サーバーでmapを受け取った際にペイロードはクリックイベントのメタデータも含みます。例えばクリックイベントの clientX であったりキーダウンイベントの KeyCode です。
Focus and Blur Events
フォーカスとブラーイベントはphx-blur, phx-focusのバインディングを使ってDOM要素に紐付けられ、発行されます。たとえば
<input name="email" phx-focus="myfocus" phx-blur="myblur"/>
ページそのものがフォーカスやブラーを検知するためにはphx-targetで"window"を指定します。他のphx-value-*のようなバインディングは要素に紐付けることができ、ペイロードの一部として送られます。たとえば、
<div class="container"
phx-focus="page-active"
phx-blur="page-inactive"
phx-value-page="123"
phx-target="window">
...
</div>
Form Events
フォームの変化と投稿(submission)を処理するには、phx-changeイベントとphx-submitイベントを使用します。一般に、フォームレベルで入力の変化を処理することが好まれます。すべてのフォームフィールドは、1つでも入力の変更が与えられると、LiveViewのコールバックに渡されます。たとえば、リアルタイムのフォームのバリデーションと保存を処理するには、テンプレートでphx_changeとphx_submitの両方のバインディングを使用します。
<%= f = form_for @changeset, "#", [phx_change: :validate, phx_submit: :save] %>
<%= label f, :username %>
<%= text_input f, :username %>
<%= error_tag f, :username %>
<%= label f, :email %>
<%= text_input f, :email %>
<%= error_tag f, :email %>
<%= submit "Save" %>
</form>
次にLiveViewのhandle_eventコールバックでそのイベントを拾います。
def render(assigns) ...
def mount(_session, socket) do
{:ok, assign(socket, %{changeset: Accounts.change_user(%User{})})}
end
def handle_event("validate", %{"user" => params}, socket) do
changeset =
%User{}
|> Accounts.change_user(params)
|> Map.put(:action, :insert)
{:noreply, assign(socket, changeset: changeset)}
end
def handle_event("save", %{"user" => user_params}, socket) do
case Accounts.create_user(user_params) do
{:ok, user} ->
{:stop,
socket
|> put_flash(:info, "user created")
|> redirect(to: Routes.user_path(AppWeb.Endpoint, AppWeb.User.ShowView, user))}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, changeset: changeset)}
end
end
バリデートコールバックは単純にすべてのフォームの入力値をchangesetにもとづいて更新し、socketに新しいchangesetをアサインします。changesetが変化した場合、例えばエラーが発生するなどの場合、 render/1 が呼び出されformは再レンダリングされます。
phx-submit バインディングも同様に、同じコールバックが呼び出され、永続化が試されます。成功した場合、:stopタブルが返され、ソケットはPhoenix.LiveView.redirectで新しいユーザーのページにリダイレクトするようアノテートされます。失敗した場合はソケットのassingsはエラーのあるchangesetで更新され、クライアントに再レンダリングされます。
NOTE: フォームのエラータグを適切に更新するには、エラータグがどのinputに所属するか分かるようにしなければなりません。これはdata-phx-error-for属性でできます。例えば、AppWeb.ErrorHelpersはこの関数を使用できます。
def error_tag(form, field) do
Enum.map(Keyword.get_values(form.errors, field), fn error ->
content_tag(:span, translate_error(error),
class: "help-block",
data: [phx_error_for: input_id(form, field)]
)
end)
end
Number inputs
LiveViewのフォームにおいて、数値のinputは特殊なケースです。プログラマティックな更新において、ブラウザーは無効なinputをクリアするので、入力が無効な場合LiveViewのクライアントはイベントを送りません。かわりに、ユーザーインタラクションを動作させるためにブラウザーネイティブのバリデーションを許可します。入力が有効になった際には、changeとsubmitイベントが通常どおり送られます。
Password inputs
パスワードinputもPhoenix.HTMLにおいて特殊なケースです。セキュリティーのため、パスワードのinputタグがレンダリングされる際はパスワードフィールドの値は使いまわされません。このためマークアップには明示的に:valueを設定する必要があります。例えば
<%= password_input f, :password, value: input_value(f, :password) %>
<%= password_input f, :password_confirmation, value: input_value(f, :password_confirmation) %>
<%= error_tag f, :password %>
<%= error_tag f, :password_confirmation %>
Key Events
キーダウンとキーアップイベントはphx-keydownとphx-keyupバインディングによってサポートされます。キーが押されたときにクライアントのイベントオブジェクトのメターデータを含んだ値がサーバに送られます。例えば、エスケープキーを押した場合には以下のようになります。
%{
"altKey" => false, "charCode" => 0, "code" => "Escape",
"ctrlKey" => false, "key" => "Escape", "keyCode" => 27,
"location" => 0, "metaKey" => false, "repeat" => false,
"shiftKey" => false, "which" => 27
}
デフォルトでは、バインドされた要素はイベントリスナーになりますが、オプションのphx-targetが提供されます。これは、「document」、「window」、またはターゲット要素のDOM idになります。例えば、
def render(assigns) do
~L"""
<div id="thermostat" phx-keyup="update_temp" phx-target="document">
Current temperature: <%= @temperature %>
</div>
"""
end
def handle_event("update_temp", %{"code" => "ArrowUp"}, socket) do
{:ok, new_temp} = Thermostat.inc_temperature(socket.assigns.id)
{:noreply, assign(socket, :temperature, new_temp)}
end
def handle_event("update_temp", %{"code" => "ArrowDown"}, socket) do
{:ok, new_temp} = Thermostat.dec_temperature(socket.assigns.id)
{:noreply, assign(socket, :temperature, new_temp)}
end
def handle_event("update_temp", _key, socket) do
{:noreply, socket}
end
Compartmentalizing markup and events with render, live_render, and live_component
renderを呼び出すだけでLiveViewテンプレートから他のテンプレートを直接レンダリングできます。
render "child_template", assigns
render SomeOtherView, "child_template", assigns
他のテンプレートの.拡張子がleexなら、LiveViewの変化トラッキングはテンプレートをまたいでも機能します。
子テンプレートをレンダリングした際に、子テンプレート内でバインドされたイベントは全て親テンプレートに送られます。言い換えると通常のPhoenixテンプレートのように、通常のrenderの呼び出しは他のLiveViewを開始しません。このためrenderはview間でマークアップを共用するのに便利です。
この問題(訳者注:renderがLiveViewを開始しないこと)を解決する一つの選択肢は、LiveViewテンプレートから render/3 を呼ぶ代わりに親LiveView内で子LiveViewを live_render/3 でレンダリングすることです。子LiveViewはmountとhandle_eventを持つ完全に親から独立したプロセスとして走ります。子LiveViewがクラッシュしても親には影響しません。親がクラッシュした場合は子は終了させられます。
子LiveViewがレンダリングされた際に子を一意に識別するために :id オプションが必要です。子LiveViewは、IDが変更されない限り、一度だけレンダリングおよびマウントされます。子のセッションへの更新はクライアントでマージされますが、クラッシュして再マウントするか、接続がドロップして回復するまで返されません。子を新しいセッションデータで強制的に再マウントするには、新しいIDが与えられなければなりません。
LiveViewは自身のプロセス上で走り、完全に切り離されたUI要素を作る優れたツールですが、マークアップとイベントを区分化することだけが必要な場合は少し高価な抽象化です。たとえば、システム内のすべてのユーザーを含むテーブルを表示していて、それぞれ独自のプロセスを持つ個別のLiveViewを使用してこのロジックを区分化したい場合は、高価すぎる可能性があります。これらケースのために、LiveViewは、 live_component/3 でレンダリングするPhoenix.LiveComponentを提供します。
<%= live_component(@socket, UserComponent, id: user.id, user: user) %>
コンポーネントには、独自のmountとhandle_eventコールバック、および変更トラッキングのサポートを備えた独自のステートがあります。コンポーネントは、親LiveViewと同じプロセスで「実行」されるため軽量です。ただし、これはコンポーネントのエラーが原因でビュー全体のレンダリングが失敗することを意味します。コンポーネントの完全な概要については、Phoenix.LiveComponentを参照してください。
まとめると、
- render マークアップを区分化する
- live_component ステート、マークアップ、イベントを区分化する
- live_render ステート、マークアップ、イベント、エラーアイソレーションを区分化する
Rate limiting events with Debounce and Throttle
phx-debounceおよびphx-throttleバインディングを使用することで、すべてのイベントはクライアントでレート制限できます。
- phx-debounce
文字列整数のタイムアウト値か"blur"のいずれかを許容します。 整数が提供されると、指定されたミリ秒だけイベントの発行を遅延させます。 "blur"が指定されている場合、ユーザーによってフィールドがフォーカスを失うまで、入力の変更イベントの発行を遅らせます。 - phx-throttle
整数のタイムアウト値を受け入れて、イベントをミリ秒単位で調整します。debounceとは異なり、throttleはすぐにイベントを発行し、指定されたタイムアウト毎に1イベントとしレート制限します。
たとえば、フォーカスを失うまでemailアドレスのバリデーションを回避し、ユーザーがフィールドを変更した後、最大2秒ごとにユーザー名をバリデーションするには
<form phx-change="validate" phx-submit="save">
<input type="text" name="user[email]" phx-debounce="blur"/>
<input type="text" name="user[username]" phx-debounce="2000"/>
</form>
1秒おきにボタンクリックのレート制限をするには
<button phx-click="search" phx-throttle="1000">Search</button>
同様に、押したままのキーダウンは
<div phx-keydown="keydown" phx-target="window" phx-throttle="500">
...
</div>
押し下げられたキーが必要でない限り、一般的には、キーアップ時にのみトリガーするphx-keyupバインディングを使用するほうが自己制限的です。ただし、phx-keydownは、ゲームや、キーを絶えず押すことが必要なその他のユースケースに役立ちます。そのような場合、スロットルはいつでも使用されるべきです。
Debounce and Throttle special behavior
次の特殊なふるまいは、フォームおよびキーダウンバインディングに対して実行されます。
- 異なる入力に対してphx-submitまたはphx-changeがトリガーされると、既存の入力の現在までのデバウンスまたはスロットルタイマーがリセットされます。
- phx-keydownバインディングは、キーの繰り返しに対してのみ調整されます。一つのキーを連続して押すと、押されたキーイベントがディスパッチされます。
DOM patching and temporary assigns
コンテナはphx-updateでマークすることができ、DOMパッチ操作により、LiveViewの一部の更新や削除を回避したり、既存のコンテンツを置き換えるのではなく更新を追加または追加したりできます。これは、独自のDOM操作を行う既存のライブラリとのクライアント側の相互運用に役立ちます。次のphx-updateの値がサポートされています。
- replace デフォルトの処理です。コンテンツで要素を置き換えます。
- ignore 新しいコンテンツの変更に関係なく、DOMへの更新を無視します。
- append 置き換える代わりに、新しいDOMコンテンツをappendします。
- prepend 置き換えるのではなく、新しいDOMコンテンツをprependします
コンテナにすでに存在するIDを含む要素をappendまたはprependする場合、LiveViewは既存の要素を新しいコンテンツに置き換えます。
NOTE: phx-updateを使用する場合、常に一意のDOM IDを設定する必要があります。
"ignore" のふるまいは、別のJSライブラリと統合する必要がある場合によく使用されます。 "append" および "prepend"機能は、大量のデータを処理するために"Temporary assigns"でよく使用されます。
Temporary assigns
デフォルトでは、すべてのLiveViewのassignsはステートフルです。場合によっては、assignsを一時的なものとしてマークすると便利です。これは、更新後にデフォルト値にリセットされることを意味します。そうすることで、クライアントにパッチを適用した後、それ以外の場合は大きくても頻繁に更新されない値を破棄できます。
LiveViewでチャットアプリケーションを実装するとします。次のように各メッセージをレンダリングできます
<%= for message <- @messages do %>
<p><span><%= message.username %>:</span> <%= message.text %></p>
<% end %>
新しいメッセージがあるたびに、それを@messagesに追加して、すべてのメッセージを再レンダリングします。
想像のとおり、チャットの会話全体をメモリに保持し、更新ごとに再送信することは、LiveViewのスマートな変更トラッキングを使用している場合でも、非常にコストがかかります。temprary assignsとphx-updateを使用することにより、メッセージをメモリに保持する必要がなく、新しいメッセージがある場合にのみUIに追加されるメッセージを送信します。
これを行うための最初のステップは、どのassignsが一時的なものであり、マウント時にリセットすべき値は何かをマークすることです。
def mount(_session, socket) do
socket = assign(socket, :messages, load_last_20_messages())
{:ok, socket, temporary_assigns: [messages: []]}
end
マウント時に、送信するメッセージの初期量もロードします。最初のレンダリングの後、メッセージの最初のバッチは空のリストにリセットされます。
これで、1つ以上の新しいメッセージがあるたびに、新しいメッセージのみを@messagesにアサインします。
socket = assign(socket, :messages, new_messages)
テンプレートでは、すべてのメッセージをコンテナにラップし、このコンテンツにphx-updateとIDのタグを付けます
<div id="chat-messages" phx-update="append">
<%= for message <- @messages do %>
<p><span><%= message.username %>:</span> <%= message.text %></p>
<% end %>
</div>
こうすることでクライアントは新しいメッセージを受信すると、古いコンテンツを置き換えるのではなく、それにappendする必要があることを認識します。
Live navigation
live_link/2 と live_redirect/2 はブラウザーのpushState APIを使うことによりページナビゲーションを可能にします。 live navigationでページをフル更新することなく更新します。
live navigationを使うにはPhoenix.HTML.link/3 とPhoenix.LiveView.redirect/2 をliveの対応するものに置き換えるだけです。
たとえば、テンプレートに次のように記述できます。
<%= live_link "next", to: Routes.live_path(@socket, MyLive, @page + 1) %>
LiveView内なら
{:noreply, live_redirect(socket, to: Routes.live_path(socket, MyLive, page + 1))}
live linkがクリックされると以下のコントロールフローが起こります。
- routeが既存のroot LiveViewに属し、LiveViewがアプリケーションのrouterで定義されている場合、新しいLiveViewをマウントせずに handle_params/3 コールバックが呼び出されます。次のセクションを参照してください。
- routeが現在実行中のrootとは異なるLiveViewに属している場合、既存のroot LiveViewがシャットダウンされ、完全な静的レンダリングを実行せずに新しいLiveViewに関する必要な情報を要求するAjaxリクエストが行われます。情報が取得されると、新しいLiveViewがマウントされます。
live_link/3 および live_redirect/2は、デフォルトでは、live/3マクロでルーターに定義されたLiveViewでのみ使用できます。
handle_params/3
mount/2 のあとで handle_params/3 コールバックが呼び出されます。第一引数にリクエストのパスパラメータとクエリパラメータ、第二引数にurl、第三引数にソケットを受け取ります。他のhandle_* コールバックのように handle_params/3 内のstateを変更するとサーバーレンダリングを引き起こします。
live linkがクリックされるたびlive redirectが発生するたびに新しいLiveViewを構築するのを回避するために、LiveViewは以下である限り既存のLiveViewで handle_param/3 を呼び出します。
- 現在表示しているのと同じroot live viewに移動しようとしている
- LiveViewがルーターで定義されている
例えば、システムのすべてのユーザーを表示するUserTable LiveViewがあるとし、ルーターに以下のように定義すると
live "/users", UserTable
live ソーティングを追加するには
<%= live_link "Sort by name", to: Routes.live_path(@socket, UserTable, %{sort_by: "name"}) %>
クリックすると、現在のLiveViewに移動しようとするため、 handle_params/3 が呼び出されます。受け取ったパラメーターを決して信頼しないでください。コールバックを使用してユーザー入力をバリデーションし、それに応じてステートを変更できます。
def handle_params(params, _uri, socket) do
case params["sort_by"] do
sort_by when sort_by in ~w(name company) ->
{:noreply, socket |> assign(:sort_by, sort) |> recompute_users()}
_ ->
{:noreply, socket}
end
end
Replace page address
LiveViewでは、現在のブラウザーのURLを置き換えることもできます。これは、ブラウザの履歴を汚染することなく、特定のイベントでURLを変更したい場合に便利です。たとえば、送信時にページの状態を変更するフォームがあるとします。これらの変更がデータベースなどに保存されていない場合、ユーザーがページを更新するか、別の場所に移動するか、URLを他の人と共有するとすぐに、変更は失われます。
これに対処するために、ユーザーは live_redirect/2 を呼び出すことができます。アイデアは、フォームデータが受け取ったら、ステートを変更せず、代わりに新しいURLを使用して自分自身にライブリダイレクトを実行することです。自分自身に移動しているため、新しいパラメータを使用してhandle_params/3が呼び出され、ステートを計算してページを再レンダリングするために使用できます。
たとえば、前のページの「並べ替え」の例を変更して、フォーム全体で並べ替えを実行します。つまり、「名前で並べ替え」ボタンをクリックして並べ替える代わりに、2つのラジオボタンを備えたフォームがあり、名前で並べ替えるか会社で並べ替えるかを選択できます。
フォームが送信されると、新しいURLを計算できます。
def handle_event("sorting", params, socket) do
{:noreply, live_redirect(socket, to: Routes.live_path(socket, __MODULE__, params))}
end
前のセクションと同様の handle_params/3 実装を使用して、新しいparamsに基づいてユーザーを再計算し、変更がある場合はサーバーレンダリングを実行します。
live_link/2 と live_redirect/2 はどちらもreplace:trueオプションをサポートしています。このオプションは、ブラウザの履歴を汚染せずに現在のURLを変更する場合に使用できます。
def handle_event("sorting", params, socket) do
{:noreply, live_redirect(socket, to: Routes.live_path(socket, __MODULE__, params), replace: true)}
end
JavaScript Client Specific
前に見たように、単一のLiveSocketインスタンスをインスタンス化して、LiveViewクライアント/サーバーの対話を有効にすることから始めます。たとえば、
import {Socket} from "phoenix"
import LiveSocket from "phoenix_live_view"
let liveSocket = new LiveSocket("/live", Socket)
liveSocket.connect()
次のLiveView固有のオプションを除き、すべてのオプションはPhoenix.Socketコンストラクターに直接渡されます。
- bindingPrefix フェニックスバインディングに使用するプレフィックス。デフォルトは"phx-"
- params ビューのマウントコールバックに渡すconnect_params。リテラルオブジェクトまたはオブジェクトを返すクロージャの場合があります。クロージャが与えられると、クロージャはビューのphx-viewの名前を受け取ります。
- hooks サーバー/クライアント相互運用のためのクライアントコールバックを含む、ユーザー定義のフック名前空間への参照。詳細については、以下のinteropセクションを参照してください。
Forms and input handling
JavaScriptクライアントは、常に現在のinputのsource of truthです。フォーカスがある特定のinputについて、LiveViewは、サーバーdでレンダリングされた更新から逸脱していても、現在のinputの値を上書きしません。これは、フォームバリデーションエラーや、ユーザーがフォームに入力する際のinputの周辺にある追加のUXなど、主要な副作用が予想されない更新に適しています。これらのユースケースでは、phx-changeは、サーバーへのイベントが進行中の間、入力編集を無効にすることに関与しません。phx-changeイベントがサーバーに送信されると、"_ target"パラメーターが、変更イベントをトリガーしたinputのnameのキースペースを含むルートペイロードになります。たとえば、次のinputが変更イベントをトリガーした場合
<input name="user[username]"/>
サーバーの handle_event/3 はペイロードを受け取ります。
%{"_target" => ["user", "username"], "user" => %{"name" => "Name"}}
phx-submitイベントは、新しいコンテナのレンダリング、外部サービスの呼び出し、または新しいページへのリダイレクトなど、主な副作用が通常発生するフォーム送信に使用されます。これらのユースケースでは、フォームのinputは送信時に読み取り専用に設定され、サーバーがphx-submitイベントを処理したという確認をクライアントが受け取るまで送信ボタンは無効になります。確認後、更新は通常どおりDOMにパッチされ、ユーザーが送信中に新しい入力に焦点を合わせていない場合は、フォーカスのある最後の入力が復元されます。
潜在的なフォームの送信を処理するために、フォームの送信中に要素のinnerTextを指定された値と交換するphx-disable-withを使用して、HTMLタグに注釈を付けることができます。たとえば、次のコードは「保存」ボタンを「保存中」に変更し、確認時に「保存」に復元します
<button type="submit" phx-disable-with="Saving...">Save</button>
Loading state and errors
デフォルトでは、次のクラスがLiveViewの親コンテナに適用されます。
- "phx-connected" ビューがサーバーに接続したときに適用されます
- "phx-disconnected" ビューがサーバーに接続されていない場合に適用されます
- "phx-error" サーバーでエラーが発生したときに適用されます。サーバーへの接続が失われた場合、このクラスは「phx-disconnected」とともに適用されます。
phx-submitでバインドされたフォームが送信されると、「phx-loading」クラスがフォームに適用され、更新時に削除されます。
JS Interop and client controlled DOM
サーバーによって要素が追加、更新、または削除されたときにカスタムのクライアント側javascriptを処理するために、次のライフサイクルコールバックでフックオブジェクトを提供できます。
- mounted 要素がDOMに追加され、そのサーバーLiveViewのマウントが完了しました
- updated 要素は、サーバーによってDOMで更新されました
- destoryed 要素は、親の更新によって、または親が完全に削除されて、ページから削除されました
- diesconnected 要素の親LiveViewがサーバーから切断されました
- reconnected エレメントの親LiveViewがサーバーに再接続しました
さらにそのコールバックはスコープ内に次の属性を含みます。
- el バインドされたDOMノードを参照する属性、
- viewName domノードのphx-view値に一致する属性
- pushEvent(event、payload) イベントをクライアントからLiveViewサーバーにプッシュするメソッド
たとえば、電話番号の書式設定のためのinputは、マークアップにアノテートします。
<input type="text" name="user[phone_number]" id="user-phone-number" phx-hook="PhoneNumber" />
次に、フックコールバックオブジェクトを定義して、ソケットに渡すことができます。
let Hooks = {}
Hooks.PhoneNumber = {
mounted() {
this.el.addEventListener("input", e => {
let match = this.el.value.replace(/\D/g, "").match(/^(\d{3})(\d{3})(\d{4})$/)
if(match) {
this.el.value = `${match[1]}-${match[2]}-${match[3]}`
}
})
}
}
let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks})
...
Note: phx-hookを使うときは必ずユニークなDOM IDを設定することが必要です。
Summary
訳不要のため省略
Functions
訳不要のため省略
Callbacks
訳不要のため省略
訳者まとめ
フロントを別のフレームワークに頼らない、LiveViewアプリケーションを作ってみたいという人の助けになったらすごく嬉しいです。
「いいね」よろしくお願いします。