Tailwind CSSはElixirならNode.js不要のPhoenix準公式ライブラリ「Tailwind」ですぐに使える

下記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を気軽に使う方法が、Phoenix創始者Chris Mccordから紹介されています

https://twitter.com/chris_mccord/status/1473730002151743488

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

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

内容が、面白かったり、役に立ったら、「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>

<font color="red">【2022/2/1追記】<br>Node.jsは不要です</font>

# ライブラリ「Tailwind」の導入

kokura.exオーガナイザ @im_miolab さんが書いた、下記コラムをベースにしつつ、上記Chrisのツイートで紹介しているFly.ioのコラムで使われているライブラリ「Tailwind」を使った手順で構築します

<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
```

## Tailwindのインストール（Fly.ioコラム通りの手順）

まず、下記Fly.ioコラム通りの手順で進めます

https://fly.io/phoenix-files/tailwind-standalone/

Tailwindをライブラリ追加します

```elixir:mix.exs
defmodule Basic.MixProject do
  use Mix.Project
…
  defp deps do
    [
      {:tailwind, "~> 0.1"},
      {:phoenix, "~> 1.6.2"},
…
```

configにTailwindの利用バージョンを書きます

```elixir:config/config.exs
# This file is responsible for configuring your application
# and its dependencies with the aid of the Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.

# General application configuration
import Config
…
config :phoenix, :json_library, Jason

# v-- add start
config :tailwind,
  version: "3.0.7",
  default: [
    args: ~w(
      --config=tailwind.config.js
      --input=css/app.css
      --output=../priv/static/assets/app.css
    ),
    cd: Path.expand("../assets", __DIR__)
  ]
# ^-- add end
…
```

Tailwind用のビルドウォッチャーを設定します

```elixir:config/dev.exs
# For development, we disable any cache and enable
# debugging and code reloading.
#
# The watchers configuration can be used to run external
# watchers to your application. For example, we use it
# with esbuild to bundle .js and .css sources.
config :basic, BasicWeb.Endpoint,
…
  watchers: [
    tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]},  # <- add here
…
```

この後、Phoenixを起動すればTailwindが使える手順となっていましたが、Tailwindライブラリのインストールやtailwind.config.jsの生成など、色々と手順が抜けてて、このままではTailwindが有効にならないため、不足手順を下記でカバーします

## Tailwindのインストール（Fly.ioコラムの不足をカバー）

Tailwindに欠かせないベーススタイル／コンポーネント／ユーティリティをimportします

```js:assets/css/app.css
/* This file is for your main application CSS */
@import "./phoenix.css";

@import "tailwindcss/base";  /* <- add here */
@import "tailwindcss/components";  /* <- add here */
@import "tailwindcss/utilities";  /* <- add here */
…
```

ライブラリをインストールし、Phoenixを起動します

```
mix deps.get
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)

# 最後に

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

「PhxGenTailwind」よりは一手間多い現状ですが、「Tailwind」はPhoenixチームが開発を行っていることから、恐らくコチラが将来のPhoenixに標準搭載されるかと思われます

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

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

# 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