下記Elixirコミュニティ運営/所属の piacere です、ご覧いただいてありがとございます
ElixirでリアルタイムUIを作るLiveView/Livebookで盛り上がるコミュニティ「LiveView JP」
LiveViewが標準搭載されたPhoenix 1.6に、deps1行でライトにTailwind CSSを導入し、Tailwindを気軽に使ってみようと思います
Tailwind CSSは最近人気のCSSライブラリで、2021年12月にバージョン3.0がリリースされ、よりパワーアップされた元気の良いライブラリです
なお、このコラムはPhoenix 1.6から導入されたesbuildを使うことで、Node.js/npm無しでTailwind CSSが利用できる手順となっています
【2021/2/1追記】
Node.jsが10系(たとえばUbuntu 20.04のaptで入る最新版は10系)だと、Phoenix起動後のWatherでnpxがTailwindのビルドエラー(Object.fromEntries is not a function)を出すため、「ElixirでTailwindは1行で使える②」の方の手順をお試しください
内容が、面白かったり、役に立ったら、「LGTM」よろしくお願いします
本コラムの検証環境
本コラムは、以下環境で検証しています(Windowsで検証してますがLinuxやmacでも動作する想定です)
- Windows 10
- Elixir 1.13.0 on WSL2 Ubuntu 18.04 ※最新版のインストール手順はコチラ
- Phoenix 1.6.2、1.6.6 ※最新版のインストール手順はコチラ
- Node.js 14.17.3
ライブラリ「PhxGenTailwind」の導入
kokura.exオーガナイザ @im_miolab さんが書いた、下記コラムをベースにしつつ、ライブラリ「PhxGenTailwind」を使った手順で構築します
Tailwind CSSでトランジションとメディアクエリをサクッとPhoenixフレームワークに導入・実装する
https://qiita.com/im_miolab/items/504a8534802e433d7442
なお、上記コラムではPhoenix 1.5系で構築を行っていますが、ここではLiveViewが標準搭載されたPhoenix 1.6で試しているため、.heexの書き方などが元コラムと異なっています
Phoenix PJの作成
DB無しでPhoenix PJを作ります
mix phx.new basic --no-ecto
…
Fetch and install dependencies? [Yn] 【←Yを入力】
…
cd basic
PhxGenTailwindのインストール
PhxGenTailwindをライブラリ追加します
defmodule Basic.MixProject do
use Mix.Project
…
defp deps do
[
{:phx_gen_tailwind, "~> 0.1.3"}, # <- add here
{:phoenix, "~> 1.6.2"},
…
元コラムの手順では、TailwindのためのWebpack設定が沢山並びましたが、ライブラリ「PhxGenTailwind」を使うと、一通り省略できる(≒ライブラリ中で同様のコード生成を肩代わりしてくれる)ので、このままライブラリをインストールします
mix deps.get
mix phx.gen.tailwind
…
NPM install new dependencies? [Yn] 【←Yを入力】
…
Phoenixを起動します
iex -S mix phx.server
PhxGenTailwindインストール直後のトップページは、こんな感じで、Phoenix標準CSSライブラリであるmilligramが無効化されてる感じです
Tailwindを使う
それでは、トップページをTailwindを使用するページに変えてみましょう
まず、下記ページ左上の画像を、im.jpgという名前でダウンロードして、PJ配下に配置します
https://qiita.com/im_miolab
OSから直接異ファイル操作できないWSL2※では、Dドライブのルートにim.jpgという画像を保存し、下記コマンドでPJ配下に移動します(WSL2以外では普通にpriv/static/images
配下に画像を置いてください)
Phoenixは起動中なので、Ctrl+Cを二度押して、停止しておいてください
cp /mnt/d/im.jpg ./priv/static/images/
上記で、priv/static配下に直接置いてるのは、Phoenix 1.6以降では、画像フォルダであるimagesが、assetsフォルダ配下から無くなり、LiveReload時にassetsからpriv/static配下に画像コピーされる機能が無くなったためです(Webpackからesbuildに変更されたと同時に、この対応がされたと思われる)
※自宅の他PCでは、エクスプローラにWSL2フォルダがあったけど、コラム書いたPCでは見れず…
※下記コラムを参考に、「\wsl$」と入れるとエクスプローラで画像ファイル置けました
https://snowsystem.net/other/windows/wsl2-ubuntu-explorer/
次に、レイアウトを下記に差し替えます
<main role="main" class="container">
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
<!-- 追加ここから -->
<section>
<h1 class="text-center font-medium text-4xl">
<%= gettext "team %{name}", name: "im" %>
</h1>
</section>
<%= @inner_content %>
<%= @inner_content %>
<!-- 追加ここまで -->
<%= @inner_content %>
</main>
トップページを下記に差し替えます … 元コラムでは、<%= ~ %>
で書かれたHTMLアトリビュートへのElixirコードの展開(解釈)は、.heexでは、{~}
で記述し、ダブルクォートで囲まないよう注意してください(囲むとElixirコード解釈されません)
<section class="my-8">
<figure
class="md:flex transition duration-500 ease-in-out bg-gray-100 hover:bg-indigo-200 transform hover:-translate-y-1 hover:scale-105 rounded-xl p-8 md:p-0"
>
<!-- プロフィール画像は`/assets/static/images/`に準備 -->
<img
class="w-32 h-32 md:w-56 md:h-56 rounded-full md:rounded-none mx-auto md:mx-0"
alt="im image"
src={Routes.static_path(@conn, "/images/im.jpg")}
>
<div class="pt-6 md:p-8 text-center md:text-left space-y-4">
<p class="text-xl font-semibold">
"Aenean eleifend, massa id scelerisque lacinia, odio elit blandit diam, at varius nisi turpis ut neque. Nam at consequat erat."
</p>
<figcaption class="font-medium">
<div>
<p class="text-purple-600">im</p>
</div>
<div>
<p class="text-gray-500">Web Developer</p>
</div>
</figcaption>
</div>
</figure>
</section>
Phoenixを起動して、確認しましょう
iex -S mix phx.server
Tailwindで構成されたページが、イイ感じに表示されました
LiveViewっぽいリアルタイムフロントUIでTailwindを使う
せっかくなので、もっとLiveViewっぽいUIでTailwindを使ってみましょう
下記LiveViewコラムを、Phoenix 1.6+Tailwindバージョンで再構築してみます
LiveViewでSPAを作る②: API無しQiita検索SPAをフォームsubmitスタイルに換装
https://qiita.com/piacerex/items/21b0e308a36e486d8b25
まず、元コラム同様、Smallexをライブラリ追加します
…
defp deps do
[
{:smallex, "~> 0.2.3"}, # <- add here
…
インストールします
mix deps.get
レイアウトを元に戻します
<main role="main" class="container">
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
<%= @inner_content %>
</main>
routerを追加します
defmodule BasicWeb.Router do
use BasicWeb, :router
…
scope "/", BasicWeb do
pipe_through :browser
get "/", PageController, :index
live "/realtime", RealtimeLive.Index, :index # <- add here
end
…
ここまでが準備で、ここからが本番です
TailwindをLiveViewテンプレート中で書く
LiveViewテンプレートである.heexを作り、その中でTailwindのclass指定をしていきます
<div class="container mx-auto w-full max-w-screen-lg">
<%= if @message != "" do %>
<p class="alert alert-info">
<%= @message %>
</p>
<% end %>
<h2 class="mb-2 px-2 text-4xl">.leexテンプレート化したLiveViewアプリ</h2>
<form class="bg-gray-100 px-8 pt-6 pb-8 mb-4" phx-submit="submit" phx-change="change">
<input type="text" name="query" value={@query} readonly={if !@message, do: "readonly"} placeholder="Please enter keyword" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" />
<p>Query: <%= @query %></p>
<input type="submit" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline" value="search" onclick="blur()" />
</form>
<table class="w-full shadow-md rounded px-8 pt-6 pb-8 mb-4 items-center">
<tr>
<th class="border px-4 py-2 bg-gray-100">ID</th>
<th class="border px-4 py-2 bg-gray-100">タイトル</th>
<th class="border px-4 py-2 bg-gray-100">作成日</th>
</tr>
<%= for result <- @results do %>
<tr>
<td class="border px-4 py-2"><%= result[ "id" ] %></td>
<td class="border px-4 py-2"><%= result[ "title" ] %></td>
<td class="border px-4 py-2"><%= result[ "created_at" ] %></td>
</tr>
<% end %>
</table>
</div>
LiveViewハンドラを書く
ここは元コラムと同じ内容です
defmodule BasicWeb.RealtimeLive.Index do
use BasicWeb, :live_view
def mount(_params, _session, socket) do
{:ok, assign(socket, query: "", message: "", results: [])}
end
def handle_event("change", %{"query" => query}, socket) do
{ :noreply, assign( socket, query: query, message: "" ) }
end
def handle_event("submit", %{"query" => query}, socket) do
send(self(), {:submit, query})
{:noreply, assign(socket, query: query, message: "[Searching...]")}
end
def handle_info({:submit, query}, socket) do
results = Json.get("https://qiita.com", "/api/v2/items?query=#{query}")
{:noreply, assign(socket, query: query, message: "[Search completed!!]", results: results)}
end
end
LiveView+Tailwindの成果を確認する
出来上がりは、パッと見はmilligramと大差無いですが、Tailwindを使っているメリットは、スペースの取り方がキレイになってたり、ホバー時のデザインがキレイに出来たり、さほど意識せずともレスポンシブ対応できる点ですね
また、LiveViewのリアルタイム入力/描画との相性もイイ感じです
レスポンシブも、横幅に合わせた4段階の調整がバッチリ効いてます … ぜひ実際に動かして、お手元で見てみてください
最後に
ライブラリ「PhxGenTailwind」を使うと、いとも簡単にTailwindが利用可能となることがお分かりでしょうか?
これまでPhoenixでは、CSSライブラリの導入にあまりチカラが入っておらず、自前で手配する手間が結構ありましたが、本コラムの最小手順を使い、Tailwindを使ったプロダクション開発をしてみるのはいかがでしょう?
なお、最後にご紹介した例は、phx.gen.liveを使わないLiveView構築例でもあるため、LiveViewの基本を学ぶ上でも参考にしてください
あと、同じ例を使って、Phoenix創始者Chris MaccrodがTwitterで紹介してたTailwind導入方法についても、下記でコラム化しているので、併せてご覧ください
ElixirでTailwindは1行で使える②~Node.jsは不要~Phoenix創始者がツイートで紹介したTailwind導入(将来Phoenix標準搭載となる可能性?)
https://qiita.com/piacerex/items/c2e6b1763fbcc7679e67