Phoenix LiveView、楽しいですね。
Phoenix LiveViewの0.10.0で追加された新機能として「Collocating templates」があります。
これは、LiveViewで利用するテンプレートを「.html.leex」ファイルとして外出しした際に、特定の条件下であればrender/1
による明示的なテンプレートの宣言を省略できる機能です。
具体的な条件としては、以下になります。
- LiveView用のexファイル中で、モジュール名の末尾に
Live
が含まれていること。(例:HelloWeb.FizzbuzzLive) - LiveView用のexファイルで定義したモジュール名から変換できる「html.leex」のファイル名であること(例:HelloWeb.FizzbuzzLiveの場合、fizzbuzz_live.html.leexとなる)
- テンプレートファイルが、LiveView用のexファイルと同じディレクトリにあること
Collocating templatesに関する詳細はこちらを参照。
https://hexdocs.pm/phoenix_live_view/0.12.1/Phoenix.LiveView.html#module-collocating-templates
動作を確認したバージョン
Phoenix LiveViewはバージョンによる差異が大きいため、動作確認したバージョンを記載しておきます。
- Elixir 1.10.3
- Phoenix 1.5.1
- Phoenix LiveView 0.12.1
Collocating templatesを試す
それでは、Collocating templatesの機能を試してみます。
違いがわかるように、render/1
に~L
で直接テンプレートを記載した状態からCollocating templatesで動作するように修正してみます。
なお、ベースとなるソースコードは、こちらの記事のコードを利用しています。
通常のrender/1
を利用した場合
モジュール名:HelloWeb.Fizzbuzz
およびrender/1
を定義したLiveView用の処理を「fizzbuzz.ex」として用意します。
defmodule HelloWeb.Fizzbuzz do
use Phoenix.LiveView
import Calendar.Strftime
def render( assigns ) do
~L"""
<h2>ここに現在時刻が表示されます。</h2>
<span><%= @time %></span>
<h2>ここにFizzBuzzの値が表示されます。</h2>
<span><%= @num %>:<%= @val %></span>
"""
end
def mount(_params, _session, socket ) do
if connected?(socket) do
Process.send_after(self(), :tick, 1000)
Process.send_after(self(), :fizzbuzz, 3000)
end
{ :ok, assign( socket,
time: local_time(),
num: 0,
val: "initial" ) }
end
def handle_info(:tick, socket) do
Process.send_after(self(), :tick, 1000)
{:noreply, assign(socket, time: local_time())}
end
def handle_info(:fizzbuzz, socket) do
Process.send_after(self(), :fizzbuzz, 3000)
next_num = socket.assigns.num + 1
{num, val} = fizzbuzz(next_num)
{:noreply, assign( socket, num: num, val: val ) }
end
##### local
defp local_time() do
:calendar.local_time() |> strftime!("%r")
end
defp calc_fb({n, 0, 0}), do: {n, "FizzBuzz"}
defp calc_fb({n, 0, _}), do: {n, "Fizz"}
defp calc_fb({n, _, 0}), do: {n, "Buzz"}
defp calc_fb({n, _, _}), do: {n, to_string(n)}
defp fizzbuzz(n) do
cond do
n < 1 -> {n , "Cannot calculate"}
true -> calc_fb({n, rem(n,3), rem(n, 5)})
end
end
end
「router.ex」ファイルに、アクセスするURLパスと対応するモジュール名を宣言します。
今回はlive "/fizzbuzz", Fizzbuzz
としています。
scope "/", HelloWeb do
pipe_through :browser
live "/", PageLive, :index
live "/fizzbuzz", Fizzbuzz
end
なお、この機能ではCalendar.Strftime
を利用しているので、mix.exs
に対して:calendar
の宣言も忘れずに追加しておきます。
defp deps do
[
{:phoenix, "~> 1.5.1"},
{:phoenix_live_view, "~> 0.12.0"},
〜(中略)〜
{:jason, "~> 1.0"},
{:calendar, "~> 0.17.6"}, # <- これを追記
{:plug_cowboy, "~> 2.0"}
]
end
ここまでのファイルをPhoenixプロジェクト作成して、iex -S mix phx.server
を実施すれば、Phoenixが起動します。
起動後、localhost:4000/fizzbuzz
へアクセスすれば、LiveViewの機能で動く画面が表示されます。
Collocating templatesを利用する場合
これを、Collocating templatesを利用したコードに修正します。
変更点は以下の3点です。
- 「lib/hello_web/live/fizzbuzz.ex」のモジュール名変更とrender/1の削除(修正)
- 「lib/hello_web/live/fizzbuzz_live.html.leex」の作成(新規)
- 「lib/hello_web/router.ex」のアクセスパスのモジュール名変更(修正)
まず、モジュール名を「HelloWeb.Fizzbuzz」から「HelloWeb.FizzbuzzLive」へ変更します。
つづいて、関数render/1
を関数定義ごとバッサリと削除します。
fizzbuzz.exに対しての修正はこれで終わりです。
defmodule HelloWeb.FizzbuzzLive do
use Phoenix.LiveView
import Calendar.Strftime
def mount(_params, _session, socket ) do
if connected?(socket) do
Process.send_after(self(), :tick, 1000)
Process.send_after(self(), :fizzbuzz, 3000)
end
{ :ok, assign( socket,
time: local_time(),
num: 0,
val: "initial" ) }
end
〜(以下略)〜
end
つづいて、fizzbuzz.exで削除したテンプレート内容を持ったファイルを新規に作成します。
ファイル名は、モジュール名をキャメルケースからスネークケースへ変換した名前となります。
今回の場合では、モジュール名がHelloWeb.FizzbuzzLive
であり、FizzbuzzLive
をキャメルケースからスネークケースにしたfizzbuzz_live
となります。
これに拡張子「.html.leex」を付与した「fizzbuzz_live.html.leex」がファイル名となります。
ファイルの中身については、「fizzbuzz.ex」のrender/1
へ定義していたテンプレート内容と同じものを記述します。
<h2>ここに現在時刻が表示されます。</h2>
<span><%= @time %></span>
<h2>ここにFizzBuzzの値が表示されます。</h2>
<span><%= @num %>:<%= @val %></span>
さいごに、「router.ex」中に定義した、モジュール名を修正します。
/fizzbuzz
に対して設定したモジュール名Fizzbuzz
をFizzbuzzLive
に変更します。
scope "/", HelloWeb do
pipe_through :browser
live "/", PageLive, :index
live "/fizzbuzz", FizzbuzzLive, :index
end
修正完了後、iex -S mix phx.server
の実施後、localhost:4000/fizzbuzz
へアクセスすれば、LiveViewの機能で動く画面が表示されます。
余談:モジュール名とファイル名とrouter.exでの定義の関係について
fizzbuzz.exで定義したモジュール名をHelloWeb.Fizzbuzz
からHelloWeb.FizzbuzzLive
へ変更していますが、これはrouter.ex中での定義方法が関係しています。
たとえば、live "/fizzbuzz", FizzbuzzLive, :index
をlive "/fizzbuzz", Fizzbuzz, :index
とすると、実行時に以下のようなエラーメッセージが出力されます。
== Compilation error in file lib/hello_web/router.ex ==
** (ArgumentError) could not infer :as option because a live action was given and the LiveView does not have a "Live" suffix. Please pass :as explicitly or make sure your LiveView is named like "FooLive" or "FooLive.Index"
ようは、サフィックスとして「Live」が必要ということのようです。
では、:index
を付与しない宣言live "/fizzbuzz", Fizzbuzz
とした場合はどうなるか。
これは動きます。
その場合、テンプレートのファイルは「fizzbuzz.html.leex」となります。
たとえば、以下のように設定を変更してみます。
defmodule HelloWeb.Fizzbuzz do
use Phoenix.LiveView
import Calendar.Strftime
def mount(_params, _session, socket ) do
if connected?(socket) do
Process.send_after(self(), :tick, 1000)
Process.send_after(self(), :fizzbuzz, 3000)
end
{ :ok, assign( socket,
time: local_time(),
num: 0,
val: "initial" ) }
end
〜(以下略)〜
end
<h2>これはfizzbuzz.html.leexの内容です</h2>
<h2>こここここに現在時刻が表示されます。</h2>
<span><%= @time %></span>
<h2>ここにFizzBuzzの値が表示されます。</h2>
<span><%= @num %>:<%= @val %></span>
scope "/", HelloWeb do
pipe_through :browser
live "/", PageLive, :index
# live "/fizzbuzz", FizzbuzzLive, :index
live "/fizzbuzz", Fizzbuzz
end
そうすると、サフィックスにLive
がなくても、Collocating templatesとして動作します。
とはいうものの、公式ドキュメントではLive
をつけた形式で紹介しているので、LiveViewとして動かすモジュールやファイル名にはLive
や_live
をつけておいた方が良いかもしれませんね。