Phoenix LiveViewのCollocating templates（render/1の省略）

Phoenix LiveView、楽しいですね。

Phoenix LiveViewの0.10.0で追加された新機能として「**Collocating templates**」があります。

https://github.com/phoenixframework/phoenix_live_view/blob/master/CHANGELOG.md#0100-2020-03-18

これは、LiveViewで利用するテンプレートを「.html.leex」ファイルとして外出しした際に、**特定の条件**下であれば`render/1`による明示的なテンプレートの宣言を省略できる機能です。

具体的な条件としては、以下になります。

1. LiveView用のexファイル中で、モジュール名の末尾に`Live`が含まれていること。（例：*HelloWeb.FizzbuzzLive*）
2. LiveView用のexファイルで定義したモジュール名から変換できる「html.leex」のファイル名であること（例：*HelloWeb.FizzbuzzLive*の場合、*fizzbuzz_live.html.leex*となる）
3. テンプレートファイルが、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で動作するように修正してみます。

なお、ベースとなるソースコードは、[こちらの記事](https://qiita.com/MzRyuKa/items/428080a0adc3969f918d)のコードを利用しています。

### 通常の`render/1`を利用した場合

モジュール名:`HelloWeb.Fizzbuzz`および`render/1`を定義したLiveView用の処理を「fizzbuzz.ex」として用意します。

```elixir:lib/hello_web/live/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`としています。

```elixir:lib/hello_web/router.ex（抜粋）
  scope "/", HelloWeb do
    pipe_through :browser

    live "/", PageLive, :index

    live "/fizzbuzz", Fizzbuzz
  end
```

なお、この機能では`Calendar.Strftime`を利用しているので、`mix.exs`に対して`:calendar`の宣言も忘れずに追加しておきます。

```elixir:mix.exs（抜粋）
  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の機能で動く画面が表示されます。

<img width="708" alt="スクリーンショット 2020-05-11 15.31.31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316636/cfe8c160-6e63-55b7-976b-315fb02d98c0.png">

### Collocating templatesを利用する場合

これを、Collocating templatesを利用したコードに修正します。
変更点は以下の3点です。

1. 「lib/hello_web/live/fizzbuzz.ex」のモジュール名変更とrender/1の削除（**修正**）
2. 「lib/hello_web/live/fizzbuzz_live.html.leex」の作成（**新規**）
3. 「lib/hello_web/router.ex」のアクセスパスのモジュール名変更（**修正**）

まず、モジュール名を「HelloWeb.Fizzbuzz」から「**HelloWeb.FizzbuzzLive**」へ変更します。
つづいて、関数`render/1`を関数定義ごとバッサリと削除します。

fizzbuzz.exに対しての修正はこれで終わりです。

```elixir:lib/hello_web/live/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`へ定義していたテンプレート内容と同じものを記述します。

```elixir:lib/hello_web/live/fizzbuzz_live.html.leex
    <h2>ここに現在時刻が表示されます。</h2>
    <span><%= @time %></span>
    <h2>ここにFizzBuzzの値が表示されます。</h2>
    <span><%= @num %>:<%= @val %></span>
```

さいごに、「router.ex」中に定義した、モジュール名を修正します。
`/fizzbuzz`に対して設定したモジュール名`Fizzbuzz`を`FizzbuzzLive`に変更します。

```elixir:lib/hello_web/router.ex（抜粋）
  scope "/", HelloWeb do
    pipe_through :browser

    live "/", PageLive, :index

    live "/fizzbuzz", FizzbuzzLive, :index
  end
```

修正完了後、`iex -S mix phx.server`の実施後、`localhost:4000/fizzbuzz`へアクセスすれば、LiveViewの機能で動く画面が表示されます。

<img width="699" alt="スクリーンショット 2020-05-11 20.46.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316636/1887bc87-27f9-cdb5-f082-33d856a42145.png">


## 余談：モジュール名とファイル名と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」となります。

たとえば、以下のように設定を変更してみます。

```elixir:lib/hello_web/live/fizzbuzz.ex（抜粋）
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
```

```elixir:lib/hello_web/live/fizzbuzz.html.leex
    <h2>これはfizzbuzz.html.leexの内容です</h2>
    <h2>こここここに現在時刻が表示されます。</h2>
    <span><%= @time %></span>
    <h2>ここにFizzBuzzの値が表示されます。</h2>
    <span><%= @num %>:<%= @val %></span>
```

```elixir:lib/hello_web/router.ex（抜粋）
  scope "/", HelloWeb do
    pipe_through :browser

    live "/", PageLive, :index

#    live "/fizzbuzz", FizzbuzzLive, :index
    live "/fizzbuzz", Fizzbuzz
  end
```

そうすると、サフィックスに`Live`がなくても、Collocating templatesとして動作します。

<img width="686" alt="スクリーンショット 2020-05-11 20.08.15.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316636/cdc9cdef-dd1b-ad88-b59e-b5f042c4b22d.png">

とはいうものの、公式ドキュメントでは`Live`をつけた形式で紹介しているので、LiveViewとして動かすモジュールやファイル名には`Live`や`_live`をつけておいた方が良いかもしれませんね。