fukuoka.ex/kokura.exのpiacereです
ご覧いただいて、ありがとうございます
前作のPHP的気軽さでWebアプリを作る方法を、今度は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 ※最新版のインストール手順はコチラ
- Phoenix 1.4.15 ※最新版のインストール手順はコチラ
- 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で読み替えて実施してください)
LiveViewでSPA開発①: Elixirのみでフロントのリアルタイム入力/反映するSPAを実現
https://qiita.com/piacerex/items/3f8ee18c9443d63955bf
更に、LiveView用HTMLをテンプレートファイル化するために、下記コラムも実施します(こちらもPJ名は、LvSampleでは無く、上記で作成したBasicで読み替えて実施してください)
LiveViewでSPAを作る③: LiveView用のHTMLをテンプレートファイルに分離
https://qiita.com/piacerex/items/5b1aabb0c45bb9934bb9
ここまでが終わったら、ブラウザで「http://localhost:4000/realtime
」にアクセスすると、LiveViewで作成された、Qiitaリアルタイム検索SPAが表示されることを確認して、準備完了です
手順①:html.leexの表示をルーティング不要に
まずはルーティングで、前作同様、「*path_」というワイルドカード指定をすることで、多階層のURLパスを「path_」パラメータから取得できるようにします
接続先のコントローラは、LiveView専用のマッピングコントローラとします
なお、前作の続きでやられている場合は、getやpostのワイルドカード指定とぶつかるため、getやpostの分は、コメントアウトや削除しておいてください
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設定記述になります(前作の続きでやっている場合は、この手順自体が実施不要です)
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パラメータが利用可能となります
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自体が未指定)なので、空のページを追加しておきます
<h1>Now loading...</h1>
手順④:html.leexページを追加する
1)templates/live直下にhtml.leex追加
これで、page配下にhtml.leexを追加するだけで、新たなLiveViewページが追加可能となったので、追加してみます
<h1>This is another page</h1>
<%= inspect( System.build_info() ) %>
<hr>
<%= inspect( @params ) %>
ブラウザで「http://localhost:4000/another」
にアクセスすると、以下のようなページが表示されます
URLにパラメータを指定してアクセスすると、@ paramsパラメータに入るので、GET処理も気軽に使えるようになります
2)templates/live配下のサブフォルダ下にhtml.leex追加
liveフォルダ配下にフォルダ階層を掘って、そこにhtml.leexを配置しても動きます
「uvw」というフォルダを掘り、「xyz.html.leex」というLiveViewテンプレートを追加します
<h1>I'm xyz page</h1>
<%= inspect( @params ) %>
ブラウザで「http://localhost:4000/uvw/xyz」
にアクセスすると、以下のような結果が表示されます(「path_」パラメータにフォルダ階層がリストで入っていることも確認できます)
3)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パラメータ化して持ち回るようにします
<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登録などのサブミット時処理を入れます
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_」パラメータにフォルダ階層がリストで入っていることも確認できます)
「送信」ボタンを押下すると、サブミット処理に相当する処理が走り、画面表示が変わります
なお、「path_」をhiddenパラメータ化しないと、入力や「送信」ボタン押下した瞬間に、以下のブランクが表示されます
4)バリデーションチェックをリアルタイムで実行
よりSPAらしい造りにするため、フォーム内容が変更された際は、バリデーションチェックがリアルタイムで実行されるようにしてみましょう
formタグに「phx-change」を指定し、コントローラ側に変更時ハンドラを設けます
ちなみに入力都度、「phx-change」が呼び出されることで、入力内容が消えてしまうため、維持するためのコードも入れています(分かりやすくするために愚直な書き方をしています、実際はフォーム値のリストを渡してループ展開するような書き方が良いでしょう)
下記内容で、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を使うこともできます
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」パラメータがパス形式の文字列に変更されていることが確認できます)
5)フォーム毎にバリデーション/サブミットを書き分けるには?
複数のフォームがある場合、html.leexの方は簡単に追加できる一方、フォーム毎に入力項目は異なるため、バリデーション/サブミットを書き分ける必要がありますが、コントローラは1つのため、何らかの対応が必要です
最も簡単なのは、「path_」パラメータにパスがあるため、下記のように、各パス毎にスイッチしてあげることです
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作れた」を実現するコラムも書いていますので、どうぞご覧ください