LiveViewもPHP的に気軽なWebアプリ開発ができるんです

[fukuoka.ex／kokura.ex](https://fukuokaex.connpass.com/)のpiacereです
ご覧いただいて、ありがとうございます :bow:

[前作のPHP的気軽さでWebアプリを作る方法](https://qiita.com/piacerex/items/25c82153976dcb6d57c1)を、今度はLiveViewでも展開してみたいと思います

前作同様、LiveViewにおいても、ルーティングは不要になります

なおLiveViewでは、パラメータ渡しによるMPA（Multiple Page Application）的ハンドリングでは無く、handle_event()等によるSPA（Single Page Application）的イベントハンドラが中心となるのですが、ここでは、できるだけMPAのようなテイストになるように作ってみます

LiveViewでのSPAフォーム開発に抵抗が高い方もまだまだ多いんでは無いかと思いますが、このスタートだと案外、気楽になれるかもです

# 本コラムの検証環境

本コラムは、以下環境で検証しています（Windowsで実施していますが、Linuxやmacでも動作する想定です）

- Windows 10
- Elixir 1.10.1 　　　　 ※[<b>最新版のインストール手順はコチラ</b>](https://qiita.com/piacere_ex/items/6714e1440e3f25fb46a1#%E4%BA%8B%E5%89%8D%E6%BA%96%E5%82%99elixir%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)
- Phoenix 1.4.15　　　※[<b>最新版のインストール手順はコチラ</b>](https://qiita.com/piacerex/items/b7787580fce5f148242f#phoenix%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)
- Node.js 12.14.0

なおPhoenixは、1.3系でも動作します（ElixirもPhoenixのバージョンに準じた古いものでも大丈夫です）

# 事前準備：Phoenix PJの作成、LiveView導入

まずPhoenix PJを作成します

```
mix phx.new basic --no-ecto
…
Fetch and install dependencies? [Yn] 【Nを入力してEnter】
cd basic
```

続けて、下記コラムの手順に従って、Phoenix PJにLiveViewを導入します（ただしPJ名は、LvSampleでは無く、上記で作成したBasicで読み替えて実施してください）

[<b>LiveViewでSPA開発①: Elixirのみでフロントのリアルタイム入力／反映するSPAを実現</b>](https://qiita.com/piacerex/items/3f8ee18c9443d63955bf)
https://qiita.com/piacerex/items/3f8ee18c9443d63955bf

更に、LiveView用HTMLをテンプレートファイル化するために、下記コラムも実施します（こちらもPJ名は、LvSampleでは無く、上記で作成したBasicで読み替えて実施してください）

[<b>LiveViewでSPAを作る③: LiveView用のHTMLをテンプレートファイルに分離</b>](https://qiita.com/piacerex/items/5b1aabb0c45bb9934bb9)
https://qiita.com/piacerex/items/5b1aabb0c45bb9934bb9

ここまでが終わったら、ブラウザで「``http://localhost:4000/realtime``」にアクセスすると、LiveViewで作成された、Qiitaリアルタイム検索SPAが表示されることを確認して、準備完了です
![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/20088637-9ac6-2c70-ef8c-e5ea555b09c0.png)

# 手順①：html.leexの表示をルーティング不要に

まずはルーティングで、前作同様、「*path_」というワイルドカード指定をすることで、多階層のURLパスを「path_」パラメータから取得できるようにします

接続先のコントローラは、LiveView専用のマッピングコントローラとします

なお、前作の続きでやられている場合は、getやpostのワイルドカード指定とぶつかるため、getやpostの分は、コメントアウトや削除しておいてください

```elixir:lib/basic_web/router.ex
defmodule BasicWeb.Router do
…
  scope "/", BasicWeb do
    pipe_through :browser

#    get "/", PageController, :index  # <-- remove here
#    post "/*path_", PageController, :index  # <-- remove here
#    get "/*path_", PageController, :index  # <-- remove here
    live "/*path_", LiveViewController  # <-- add here
…
```

# 手順②：html.leexの複数階層フォルダ指定可能に

render()の第2引数に指定するパスの複数階層指定は、前作と全く同じView設定記述になります（前作の続きでやっている場合は、この手順自体が実施不要です）

```elixir:lib/basic_web.ex
defmodule BasicWeb do
…
  def view do
    quote do
      use Phoenix.View,
        pattern: "**/*",  # <-- add here
        root: "lib/basic_web/templates",
        namespace: BasicWeb, 
…
```

# 手順③：パスの自動マッピング、GETパラメータの素通し

LiveView専用のマッピングコントローラを追加します

「path_」パラメータのフォルダ階層リストを、html.leexのパスにマッピングします

通常だと、mount()の第1引数であるparamsは、「_params」と利用不可にしますが、ここではGETパラメータを素通ししたいので、アンダースコア無しの「params」で定義し、テンプレートに引き渡します

これにより、render()内で、assignsのparamsでGETパラメータが利用可能となります

```elixir:lib/basic_web/live/liveview_controller.ex
defmodule BasicWeb.LiveViewController do
  use Phoenix.LiveView

  def render( assigns ) do
    params = assigns.params
    template = if params[ "path_" ] == nil, do: "blank.html", else: Path.join( params[ "path_" ] ) <> ".html"
    BasicWeb.LiveView.render( template, assigns )
  end

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

LiveView起動直後は、assignsに「path_」パラメータが未指定（というかparams自体が未指定）なので、空のページを追加しておきます

```erb:lib/basic_web/templates/live/blank.html.leex
<h1>Now loading...</h1>
```

# 手順④：html.leexページを追加する

## １）templates/live直下にhtml.leex追加

これで、page配下にhtml.leexを追加するだけで、新たなLiveViewページが追加可能となったので、追加してみます

```erb:lib/basic_web/templates/live/another.html.leex
<h1>This is another page</h1>

<%= inspect( System.build_info() ) %>
<hr>
<%= inspect( @params ) %>
```

ブラウザで``「http://localhost:4000/another」``にアクセスすると、以下のようなページが表示されます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/c7cf5542-3ed5-5376-9320-77211c55a7ac.png)

URLにパラメータを指定してアクセスすると、@ paramsパラメータに入るので、GET処理も気軽に使えるようになります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/aff62c93-8848-0a24-e09e-cc2662e3d6f4.png)

## ２）templates/live配下のサブフォルダ下にhtml.leex追加

liveフォルダ配下にフォルダ階層を掘って、そこにhtml.leexを配置しても動きます

「uvw」というフォルダを掘り、「xyz.html.leex」というLiveViewテンプレートを追加します

```erb:lib/basic_web/templates/live/uvw/xyz.html.leex
<h1>I'm xyz page</h1>

<%= inspect( @params ) %>
```

ブラウザで``「http://localhost:4000/uvw/xyz」``にアクセスすると、以下のような結果が表示されます（「path_」パラメータにフォルダ階層がリストで入っていることも確認できます）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/af3466f0-f95b-797a-d796-6c41748458f3.png)

## ３）MPAフォームのノリでSPAフォームを気軽に書く

ここまでの手順により、LiveViewページは、html.leex追加で増やせるようになりましたが、前作で扱ったようなサーバサイドレンダリング（SSR）でのMPAと異なり、LiveViewはSPAなので、GETやPOSTによるページ間のパラメータ渡しは行いません

代わりに、LiveViewコントローラにハンドラを追加したり、mount()にパラメータ追加することで処理するため、MPAとは書き方が根本的に異なります

しかし、パラメータ設計によっては、MPAソックリな記述も可能なため、これを実現してみます

まず、引き渡すパラメータは、paramsのみとすることで、mount()のパラメータリストをいじる必要が無くなります

次に、submitのハンドラは、formタグに「phx-submit」を指定し、コントローラ側にハンドラを設けることで実現します

なお、「path_」パラメータは、「phx-submit」や「phx-change」のたびにrouter.exから来る訳では無いため、パス形式に直しておき、hiddenパラメータ化して持ち回るようにします

```erb:lib/basic_web/templates/live/uvw/form.html.leex
<h1>I'm form page</h1>

<font color="blue"><h2><%= @params[ "submited" ] %></h2></font>

<form phx-submit="submit">

<p>
メモ：
<input name="memo" type="text">
</p>

<p>
好きな言語：
<input name="language[]" type="checkbox" value="Elixir">Elixir
<input name="language[]" type="checkbox" value="Rust">Rust
<input name="language[]" type="checkbox" value="Julia">Julia
</p>

<p>
年代：
<select name="age">
  <option value="10">10代
  <option value="20">20代
  <option value="30">30代
  <option value="30">40代
  <option value="50">50代
  <option value="60">60代
  <option value="70">70代以上
</select>
</p>

<input type="submit" value="送信">

<input name="path_" type="hidden" value="<%= @path_ %>">

</form>

<%= inspect @params %>
```

LiveView用コントローラの内容は、下記で置き換えます

まずrender()では、「path_」パラメータをリスト形式とパス形式の両方を受け付けられるように変更します）

mount()は、変更無しです

handle_event()が、「phx-submit」のためのハンドラで、ここではフォームサブミット時に、メッセージが変わるようにしているだけですが、実際はDB登録などのサブミット時処理を入れます

```elixir:lib/basic_web/live/liveview_controller.ex
defmodule BasicWeb.LiveViewController do
  use Phoenix.LiveView

  def render( assigns ) do
    params = assigns.params
    page = case params[ "path_" ] do
      nil -> "blank.html"
      _   ->
        case is_list( params[ "path_" ] ) do
          true -> to_path_string( params[ "path_" ] ) <> ".html"
          _    -> params[ "path_" ] <> ".html"
        end
    end
    BasicWeb.LiveView.render( page, assigns )
  end

  def mount( params, _session, socket ) do
    { :ok, assign( socket, params: params, path_: to_path_string( params[ "path_" ] ) ) }
  end

  def to_path_string( path_list ), do: String.slice( Enum.reduce( path_list, "", & "#{ &2 }#{ &1 }/" ), 0..-2 )

  def handle_event( "submit", params, socket ) do
    new_params = 
      params
      |> Map.put( "submited",  "submited" )
    { :noreply, assign( socket, params: new_params ) }
  end
end
```

ブラウザで``「http://localhost:4000/uvw/xyz」``にアクセスすると、以下のような結果が表示されます（「path_」パラメータにフォルダ階層がリストで入っていることも確認できます）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/0d76f25c-ae3b-b3c1-f7b6-a0ee9d989b95.png)

「送信」ボタンを押下すると、サブミット処理に相当する処理が走り、画面表示が変わります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/534d7667-e6a4-3456-d2f4-d76e9f5743ad.png)

なお、「path_」をhiddenパラメータ化しないと、入力や「送信」ボタン押下した瞬間に、以下のブランクが表示されます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/eb57f0df-36b1-148a-6990-1eda3c4973f9.png)

## ４）バリデーションチェックをリアルタイムで実行

よりSPAらしい造りにするため、フォーム内容が変更された際は、バリデーションチェックがリアルタイムで実行されるようにしてみましょう

formタグに「phx-change」を指定し、コントローラ側に変更時ハンドラを設けます

ちなみに入力都度、「phx-change」が呼び出されることで、入力内容が消えてしまうため、維持するためのコードも入れています（分かりやすくするために愚直な書き方をしています、実際はフォーム値のリストを渡してループ展開するような書き方が良いでしょう）

下記内容で、form.html.leexを置き換えてください

```erb:lib/basic_web/templates/live/uvw/form.html.leex
<h1>I'm form page</h1>

<font color="red"><%= @params[ "validated" ] %></font><font color="blue"><h2><%= @params[ "submited" ] %></h2></font>

<form phx-submit="submit" phx-change="change">

<p>
メモ：
<input name="memo" type="text" value="<%= @params[ "memo" ] %>">
</p>

<p>
好きな言語：
<input name="language[]" type="checkbox" value="Elixir" <%= if @params[ "language" ] != nil && Enum.any?( @params[ "language" ], & &1 == "Elixir" ), do: "checked", else: "" %>>Elixir
<input name="language[]" type="checkbox" value="Rust" <%= if @params[ "language" ] != nil && Enum.any?( @params[ "language" ], & &1 == "Rust" ), do: "checked", else: "" %>>Rust
<input name="language[]" type="checkbox" value="Julia" <%= if @params[ "language" ] != nil && Enum.any?( @params[ "language" ], & &1 == "Julia" ), do: "checked", else: "" %>>Julia
</p>

<p>
年代：
<select name="age">
  <option value="10" <%= if @params[ "age" ] == "10", do: "selected", else: "" %>>10代
  <option value="20" <%= if @params[ "age" ] == "20", do: "selected", else: "" %>>20代
  <option value="30" <%= if @params[ "age" ] == "30", do: "selected", else: "" %>>30代
  <option value="30" <%= if @params[ "age" ] == "40", do: "selected", else: "" %>>40代
  <option value="50" <%= if @params[ "age" ] == "50", do: "selected", else: "" %>>50代
  <option value="60" <%= if @params[ "age" ] == "60", do: "selected", else: "" %>>60代
  <option value="70" <%= if @params[ "age" ] == "70", do: "selected", else: "" %>>70代以上
</select>
</p>

<input type="submit" value="送信">

<input name="path_" type="hidden" value="<%= @path_ %>">

</form>

<%= inspect @params %>
```

バリデーションチェック用のhandle_event()も追加します

ここでも、バリデーションチェックエラー時のメッセージを愚直に作っていますが、もっとスマートな書き方をすることもできますし、Ectoのchangesetを使うこともできます

```elixir:lib/basic_web/live/liveview_controller.ex
defmodule BasicWeb.LiveViewController do
…
  def handle_event( "change", params, socket ) do
    validated = ( if String.length( params[ "memo" ] ) > 30, do: "（メモは30文字以内にしてください）", else: "" ) <>
      ( if params[ "language" ] == [], do: "（言語は必ず1つ選択してください）", else: "" )
    new_params = 
      params
      |> Map.put( "validated", validated )
      |> Map.put( "submited",  "" )
    { :noreply, assign( socket, params: new_params ) }
  end
…
```

ブラウザで``「http://localhost:4000/uvw/xyz」``にアクセスし、入力を行うと、バリデーションチェックに相当する処理が走り、画面表示が変わります（「_path」パラメータがパス形式の文字列に変更されていることが確認できます）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/0e5b9bc5-fe6e-946d-428c-646968d014d7.png)

## ５）フォーム毎にバリデーション／サブミットを書き分けるには？

複数のフォームがある場合、html.leexの方は簡単に追加できる一方、フォーム毎に入力項目は異なるため、バリデーション／サブミットを書き分ける必要がありますが、コントローラは1つのため、何らかの対応が必要です

最も簡単なのは、「path_」パラメータにパスがあるため、下記のように、各パス毎にスイッチしてあげることです

```elixir:lib/basic_web/live/liveview_controller.ex
defmodule BasicWeb.LiveViewController do
…
  def handle_event( "change", params, socket ) do
    validated= case params[ "path_" ] do
      "uvw/form" => 
        ( if String.length( params[ "memo" ] ) > 30, do: "（メモは30文字以内にしてください）", else: "" ) <>
        ( if params[ "language" ] == [], do: "（言語は必ず1つ選択してください）", else: "" )
      _         => "(undefined validatd)"
    end
    new_params = 
      params
      |> Map.put( "validated", validated)
      |> Map.put( "submited",  "" )
    { :noreply, assign( socket, params: new_params ) }
  end
```

もちろん、もっとスマートに書ける訳ですが、入門的な内容としては、ここまでで充分かと思います

# 終わり

LiveViewでも、PHPみたいに気軽なWebアプリ開発ができるようになりました

このテクニックを使って、LiveView SPAによるWebアプリ構築にも慣れていっていただけたら幸いです

Vue.jsやReact等でリアルタイムフロントに慣れている方であれば、ハンドラを多少覚えるだけで使いこなせるハズです

なお、ここまでは画面系でのお気軽対応でしたが、API開発においても、同様の気軽さ … つまり[「JSON書いたらAPI作れた」を実現するコラム](https://qiita.com/piacerex/items/144089aa6f2052f4d1cb)も書いていますので、どうぞご覧ください

# p.s.このコラムが、面白かったり、役に立ったら…

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/67568d54-6584-ad33-77c7-b3b3afe70287.png) や ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/f5b819ef-d655-ef72-294c-b1a9d2c5b563.png) にて、どうぞ応援よろしくお願いします:bow: