ElixirでTailwindは1行で使える①～Node.jsは不要～React的リアルタイムフロントをサーバサイドで書くLiveViewにTailwindCSS導入

下記Elixirコミュニティ運営／所属の [piacere](https://twitter.com/piacere_ex) です、ご覧いただいてありがとございます :bow:

[**Elixir実装の芽を愛でるコミュニティ「Elixir Digitalization Implementors」
福岡Elixirコミュニティ「fukuoka.ex」
小倉Elixirコミュニティ「kokura.ex」**](https://fukuokaex.connpass.com)

https://fukuokaex.connpass.com?1

[**ElixirでリアルタイムUIを作るLiveView／Livebookで盛り上がるコミュニティ「LiveView JP」**](https://liveviewjp.connpass.com)

https://liveviewjp.connpass.com

LiveViewが標準搭載されたPhoenix 1.6に、deps1行でライトにTailwind CSSを導入し、Tailwindを気軽に使ってみようと思います

Tailwind CSSは最近人気のCSSライブラリで、2021年12月にバージョン3.0がリリースされ、よりパワーアップされた元気の良いライブラリです

なお、このコラムはPhoenix 1.6から導入されたesbuildを使うことで、Node.js／npm無しでTailwind CSSが利用できる手順となっています

<font color="red">【2021/2/1追記】<br>Node.jsが10系（たとえばUbuntu 20.04のaptで入る最新版は10系）だと、Phoenix起動後のWatherでnpxがTailwindのビルドエラー（Object.fromEntries is not a function）を出すため、[「ElixirでTailwindは1行で使える②」](https://qiita.com/piacerex/items/c2e6b1763fbcc7679e67)の方の手順をお試しください</font>

内容が、面白かったり、役に立ったら、「LGTM」よろしくお願いします :wink:

# 本コラムの検証環境

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

- Windows 10
- Elixir 1.13.0 on WSL2 Ubuntu 18.04　※<b>[最新版のインストール手順はコチラ](https://qiita.com/piacerex/items/6b64dfe70f48c89d8385)</b>
- Phoenix 1.6.2、1.6.6　　　　　　　 ※<b>[最新版のインストール手順はコチラ](https://qiita.com/piacere_ex/items/b7787580fce5f148242f#phoenix%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)</b>
- Node.js 14.17.3
 
# ライブラリ「PhxGenTailwind」の導入

kokura.exオーガナイザ @im_miolab さんが書いた、下記コラムをベースにしつつ、ライブラリ「PhxGenTailwind」を使った手順で構築します

<b>Tailwind CSSでトランジションとメディアクエリをサクッとPhoenixフレームワークに導入・実装する</b>
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をライブラリ追加します

```elixir:mix.exs
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が無効化されてる感じです
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/2d44246d-a7b6-1fbe-54fc-76f515fd0e52.png)

## 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/

次に、レイアウトを下記に差し替えます

```erb:lib/basic_web/templates/layout/app.html.heex
<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コード解釈されません）

```erb:lib/basic_web/templates/page/index.html.heex
<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で構成されたページが、イイ感じに表示されました
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/ffb8ec29-cde9-76bb-9a90-ada2c7433b0d.png)

レスポンシブも、ちゃんと働いています
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/a87d3f7c-589d-3286-4a5d-428f4ccfebc1.png)

# LiveViewっぽいリアルタイムフロントUIでTailwindを使う

せっかくなので、もっとLiveViewっぽいUIでTailwindを使ってみましょう

下記LiveViewコラムを、Phoenix 1.6＋Tailwindバージョンで再構築してみます

<b>LiveViewでSPAを作る②: API無しQiita検索SPAをフォームsubmitスタイルに換装</b>
https://qiita.com/piacerex/items/21b0e308a36e486d8b25

まず、元コラム同様、Smallexをライブラリ追加します

```elixir:mix.exs
…
  defp deps do
    [
      {:smallex, "~> 0.2.3"},  # <- add here
…
```

インストールします

```
mix deps.get
```

レイアウトを元に戻します

```erb:lib/basic_web/templates/layout/app.html.heex
<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を追加します

```elixir:lib/basic_web/router.ex
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指定をしていきます

```erb:lib/basic_web/live/realtime/index.html.heex
<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ハンドラを書く

ここは元コラムと同じ内容です

```elixir:lib/basic_web/live/realtime/index.ex
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のリアルタイム入力／描画との相性もイイ感じです
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/ac033ad4-0d39-ac2e-f629-227f41a1823d.png)

レスポンシブも、横幅に合わせた4段階の調整がバッチリ効いてます … ぜひ実際に動かして、お手元で見てみてください
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/c4409f98-e586-55a4-4582-acd18d686c79.png)

# 最後に

ライブラリ「PhxGenTailwind」を使うと、いとも簡単にTailwindが利用可能となることがお分かりでしょうか？

これまでPhoenixでは、CSSライブラリの導入にあまりチカラが入っておらず、自前で手配する手間が結構ありましたが、本コラムの最小手順を使い、Tailwindを使ったプロダクション開発をしてみるのはいかがでしょう？

なお、最後にご紹介した例は、phx.gen.liveを使わないLiveView構築例でもあるため、LiveViewの基本を学ぶ上でも参考にしてください

あと、同じ例を使って、Phoenix創始者Chris MaccrodがTwitterで紹介してたTailwind導入方法についても、下記でコラム化しているので、併せてご覧ください :information_desk_person:

<b>ElixirでTailwindは1行で使える②～Node.jsは不要～Phoenix創始者がツイートで紹介したTailwind導入（将来Phoenix標準搭載となる可能性？）</b>
https://qiita.com/piacerex/items/c2e6b1763fbcc7679e67

# 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