エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

Phoenix.LiveView 事始め

先月末にバージョン 0.1 が公開され、Hex からインストールできるようになりました。

これを機に Phoenix.LiveView に挑戦です。

この記事では、app 作成から LiveView で表示を動かすまでの作業を、ただ淡々と書いてゆきます。

準備

適当な Phoenix app を用意します。

以降の説明のために、ここでは DB (Ecto) を利用しない app を新規に作成します。

$ mix phx.new my_app --no-ecto
$ cd my_app

パッケージを追加する

mix.exs を編集して phoenix_live_view を追加します。

--- a/mix.exs
+++ b/mix.exs
@@ -38,7 +38,8 @@ defmodule MyApp.MixProject do
       {:phoenix_live_reload, "~> 1.2", only: :dev},
       {:gettext, "~> 0.11"},
       {:jason, "~> 1.0"},
-      {:plug_cowboy, "~> 2.0"}
+      {:plug_cowboy, "~> 2.0"},
+      {:phoenix_live_view, "~> 0.1"}
     ]
   end
 end
$ mix deps.get

assets/package.jsonJavaScript で利用するパッケージの依存情報を追加します。

--- a/assets/package.json
+++ b/assets/package.json
@@ -7,7 +7,8 @@
   },
   "dependencies": {
     "phoenix": "file:../deps/phoenix",
-    "phoenix_html": "file:../deps/phoenix_html"
+    "phoenix_html": "file:../deps/phoenix_html",
+    "phoenix_live_view": "file:../deps/phoenix_live_view"
   },
$ npm install --prefix assets

Endpoint を編集する

lib/my_app_web/endpoint.ex を編集し、 LiveView で使うソケットを設定します。

--- a/lib/my_app_web/endpoint.ex
+++ b/lib/my_app_web/endpoint.ex
@@ -5,6 +5,8 @@ defmodule MyAppWeb.Endpoint do
     websocket: true,
     longpoll: false
 
+  socket "/live", Phoenix.LiveView.Socket

ソケットのクライアントを設定する

assets/js/app.js を編集し、ソケットに接続するためのクライアントのコードを追加します。

--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -15,3 +15,8 @@ import "phoenix_html"
 //
 // Local files can be imported directly using relative paths, for example:
 // import socket from "./socket"
+
+import LiveSocket from "phoenix_live_view"
+
+let liveSocket = new LiveSocket("/live")
+liveSocket.connect()

salt を設定する

config/config.exs の endpoint の設定の項目に :live_view を追加し、キーワードリストに :signing_salt を設定します。

--- a/config/config.exs
+++ b/config/config.exs
@@ -10,11 +10,12 @@ use Mix.Config
 # Configures the endpoint
 config :my_app, MyAppWeb.Endpoint,
   url: [host: "localhost"],
   secret_key_base: "QshB2lH5dWIXtZFX9mewWHeY/Lt/EpyQv/ErFXjXMfDmstWH3eQ9dVqM2rfdGLBX",
   render_errors: [view: MyAppWeb.ErrorView, accepts: ~w(html json)],
-  pubsub: [name: MyApp.PubSub, adapter: Phoenix.PubSub.PG2]
+  pubsub: [name: MyApp.PubSub, adapter: Phoenix.PubSub.PG2],
+  live_view: [signing_salt: "some-secure-salt"]

モジュールを追加する

ディレクトlib/my_app_web/live を作成し LiveView のモジュール MyAppWeb.SampleLIveを定義したファイル lib/my_app_web/live/sample_live.ex を作成します。

defmodule MyAppWeb.SampleLive do
  use Phoenix.LiveView

  def render(assigns) do
    ~L"""
    Hello Phoenix.LiveView world!
    """
  end

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

router を設定する

lib/my_app_web/router.ex を編集し、MyAppWeb.SampleLIve を利用する routing を追加します。

--- a/lib/my_app_web/router.ex
+++ b/lib/my_app_web/router.ex
@@ -1,26 +1,28 @@
 defmodule MyAppWeb.Router do
   use MyAppWeb, :router
+  import Phoenix.LiveView.Router
 
   pipeline :browser do
     plug :accepts, ["html"]
     plug :fetch_session
     plug :fetch_flash
     plug :protect_from_forgery
     plug :put_secure_browser_headers
   end
 
   pipeline :api do
     plug :accepts, ["json"]
   end
 
   scope "/", MyAppWeb do
     pipe_through :browser
 
     get "/", PageController, :index
+    live "/sample", SampleLive
   end

最初の表示

app を起動し、http://localhost:4000/sample にアクセスします。

$ iex -S mix phx.server

MyAppWeb.SampleLIverender/1 に記述した内容がレンダリングされ表示されました。

view と template を追加する

LiveView のモジュール内に sigil ( ~L ) で記述する代わりに view と template で表示するように変更します。

新規に view のファイル lib/my_app_web/views/sample_view.ex を作成してモジュール MyAppWeb.SampleView を定義します。

defmodule MyAppWeb.SampleView do
  use MyAppWeb, :view
end

同じく新規に template のファイル lib/my_app_web/templates/sample/show.html.leex を作成してテンプレートを記述します。LiveView のテンプレートは拡張子が .leex になります。

<h1><%= @message %></h1>

モジュール MyAppWeb.SampleLiverender/1 の記述を上で追加した view と template を使うように変更します。

  def render(assigns) do
    Phoenix.View.render(MyAppWeb.SampleView, "show.html", message: "Hello Phoenix.LiveView world!")
  end

:message で与えた文字列が、テンプレートに記述したタグ <h1> で装飾されて表示されることが確認できます。

click をハンドリングする

lib/my_app_web/templates/sample/show.html.leex を次のように変更します。

<h1><%= @message %></h1>
<button phx-click="add" phx-value-char="A">A</button>
<button phx-click="add" phx-value-char="B">B</button>
<button phx-click="add" phx-value-char="C">C</button>
<button phx-click="clear">clear</button>
<div>
  > <%= @str %>
</div>

変更すると右のようにボタンが表示されます。

タグ button に指定した phx-click が click のイベントとして LiveView のモジュールで定義するハンドラ handle_event/3 に渡されます。 また phx-value-* で指定した値がイベントの値として一緒にハンドラに渡されます。

MyAppWeb.SampleLivehandle_event/3 を実際に追加します。

defmodule MyAppWeb.SampleLive do
  use Phoenix.LiveView

  def render(assigns) do
    Phoenix.View.render(MyAppWeb.SampleView, "show.html", message: "Hello Phoenix.LiveView world!", str: assigns.str)
  end

  def mount(_session, socket) do
    {:ok, assign(socket, :str, "")}
  end

  def handle_event("add", %{"char" => char}, socket) do
    str = socket.assigns.str
    new_socket =
      socket
      |> assign(:str, str <> char)
    {:noreply, new_socket}
  end

  def handle_event("clear", _, socket) do
    {:noreply, assign(socket, :str, "")}
  end
end

A, B, C の各ボタンで文字列に文字が追加され、clear ボタンで文字列がクリアされます。

フォームのイベントをハンドリングする

lib/my_app_web/templates/sample/show.html.leex にフォームを追加します。また入力結果を表示するための要素も追加します。 フォームで変更が発生すると phx-change で指定されたイベントが発生します。また submit した場合は phx-submit で指定したイベントが発生します。

<h1><%= @message %></h1>
<button phx-click="add" phx-value-char="A">A</button>
<button phx-click="add" phx-value-char="B">B</button>
<button phx-click="add" phx-value-char="C">C</button>
<button phx-click="clear">clear</button>
<div>
  > <%= @str %>
</div>

<hr />

<form phx-change="update" phx-submit="submit">
  <input type="text" name="text">
</form>
<div>
  > <%= @text %>
</div>
<ul>
  <%= Enum.map(@texts, fn text -> %>
    <li><%= text %></li>
  <% end) %>
</ul>

LiveView のモジュールにハンドラを追加します。

イベント update が発生すると input タグの name で指定した値をパラメータとしてハンドラが呼び出されます。

ここでは input タグに文字列の入力があると、それを反転した文字列を表示し、submit されるとその反転された文字列をリストに追加する処理を追加しました。

defmodule MyAppWeb.SampleLive do
  use Phoenix.LiveView

  def render(assigns) do
    params = [
      message: "Hello Phoenix.LiveView world!",
      str: assigns.str,
      text: assigns.text,
      texts: assigns.texts
    ]
    Phoenix.View.render(MyAppWeb.SampleView, "show.html", params)
  end

  def mount(_session, socket) do
    new_socket =
      socket
      |> assign(:str, "")
      |> assign(:text, "")
      |> assign(:texts, [])
    {:ok, new_socket}
  end

  def handle_event("add", %{"char" => char}, socket) do
    str = socket.assigns.str
    new_socket =
      socket
      |> assign(:str, str <> char)
    {:noreply, new_socket}
  end

  def handle_event("clear", _, socket) do
    {:noreply, assign(socket, :str, "")}
  end

  def handle_event("update", %{"text" => text}, socket) do
    new_socket =
      socket
      |> assign(:text, String.reverse(text))
    {:noreply, new_socket}
  end

  def handle_event("submit", _, socket) do
    new_socket =
      socket
      |> assign(:text, "")
      |> assign(:texts, [socket.assigns.text | socket.assigns.texts])
    {:noreply, new_socket}
  end
end

実行結果です。

ここでは form タグを直接記述したので phx-change, phx-submit という形で記述しましたが、Phoenix.HTML.Form.form_for/3 を利用する場合はドキュメントにあるように、オプションに :phx_change, :phx_submit とハイフンをアンダスコアに置き換えたキーで指定します。

いつか読むはずっと読まない:博物館の後ろ側、あるいは本当の顔

研究者から見た大英自然史博物館の様子を語った一冊。

博物館には一般の来場者の目の届かないところに膨大な資料が保存され様々な人々の営みがあることに目を向けさせられます。

乾燥標本収蔵1号室―大英自然史博物館 迷宮への招待

乾燥標本収蔵1号室―大英自然史博物館 迷宮への招待

へんなものみっけ! (4) (ビッグコミックス)

へんなものみっけ! (4) (ビッグコミックス)