Phoenix LiveView に超簡単 UI コンポーネント Pines を導入する

## はじめに

前回の記事で Pines を紹介しました

https://qiita.com/RyoWakabayashi/private/c2d5db339e3bc4811deb

https://devdojo.com/pines

Pines はコピー＆ペーストだけでアコーディオンやバナーなどの UI コンポーネントを実装できるライブラリーです

<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="NWEZzmW" data-user="ryowakabayashi" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/ryowakabayashi/pen/NWEZzmW">
  Pines Accordion</a> by Ryo Wakabayashi (<a href="https://codepen.io/ryowakabayashi">@ryowakabayashi</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="bGQXPzp" data-user="ryowakabayashi" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/ryowakabayashi/pen/bGQXPzp">
  Untitled</a> by Ryo Wakabayashi (<a href="https://codepen.io/ryowakabayashi">@ryowakabayashi</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="LYXwowO" data-user="ryowakabayashi" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/ryowakabayashi/pen/LYXwowO">
  Untitled</a> by Ryo Wakabayashi (<a href="https://codepen.io/ryowakabayashi">@ryowakabayashi</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="ZEmdgER" data-user="ryowakabayashi" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/ryowakabayashi/pen/ZEmdgER">
  Pines Copy to Clipboard</a> by Ryo Wakabayashi (<a href="https://codepen.io/ryowakabayashi">@ryowakabayashi</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

Alpine.js や Tailwind CSS を利用しているため、 JS や CSS をほとんど書くことなく綺麗に動く UI が作れてしまいます

Alpine.js と Tailwind CSS でピンと来た方もいると思いますが、 Pines は Elixir の Web フレームワークである Phoeix と非常に相性が良いです

https://www.phoenixframework.org/

この記事では Phoenix への Pines コンポーネント導入方法を紹介します

なお、 Phoenix のバージョンは 1.7 を想定しています

実装したコードの全量はこちら

https://github.com/RyoWakabayashi/pines_sample

## 実行環境

M2 Mac 上に asdf でインストールした環境を使いました

他の環境でも特に問題ないと思いますが、 Elixir は 1.12 以降（Phoenix LiveView の要件）を使いましょう

- macOS Ventura 13.5
- Elixir 1.15.4
- Erlang 26.0.2
- Node.js 20.5.0
- Yarn 1.22.19

## Phoenix プロジェクトの準備

Pines を導入する Phoenix プロジェクトを準備します

公式ドキュメントに従って作業していきます

https://hexdocs.pm/phoenix/installation.html

### Hex のインストール

Elixir のパッケージマネージャーである Hex をインストールします

```bash
mix local.hex
```

### Phoenix のインストール

Phoenix をインストールします

```bash
mix archive.install hex phx_new
```

### プロジェクトの作成

新しい Phoenix プロジェクトを作成します

```bash
mix phx.new pines_sample --no-ecto
```

今回の例ではデータベースを使わないので `--no-ecto` を指定しています

実際のプロジェクトの場合は必要に応じて変更してください

しばらくすると `Fetch and install dependencies? [Yn]` と尋ねられるのでそのまま Enter キーを押してください

Phoenix プロジェクトの雛形が作成され、デフォルトで必要なライブラリーが取得されます

作成したプロジェクトのディレトリー内に移動します

```bash
cd pines_sample
```

## Alpine.js の導入

Phoenix 1.7 では Tailwind CSS がデフォルトで導入されているため、 Alpine.js だけ手動で導入します

まず、プロジェクト内の assets ディレクトリーに移動します

```bash
cd assets
```

そして、 JavaScript の依存ライブラリーとして Alpine.js を追加します

ついでにアニメーションを実装するための Collapse Plugin も追加しておきましょう

```bash
yarn add alpinejs @alpinejs/collapse
```

コマンドを実行すると "assets/package.json" が追加され、 "assets/node_modules" 内に依存ライブラリーがインストールされます

"assets/package.json" の中身は以下のようになります

```json
{
  "dependencies": {
    "@alpinejs/collapse": "^3.12.3",
    "alpinejs": "^3.12.3"
  }
}
```

続いて "assets/js/app.js" に Alpine.js の読み込み処理を追加します

```diff_javascript
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html"
// Establish Phoenix Socket and LiveView configuration.
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import topbar from "../vendor/topbar"

+ import Alpine from "alpinejs"
+ import collapse from '@alpinejs/collapse'

+ Alpine.plugin(collapse)
+ window.Alpine = Alpine
+ Alpine.start()

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
- let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}})
+ let liveSocket = new LiveSocket("/live", Socket, {
+     params: {_csrf_token: csrfToken},
+     dom: {
+       onBeforeElUpdated(from, to) {
+         if (from._x_dataStack) {
+           window.Alpine.clone(from, to)
+         }
+       }
+     }
+ })
...
```

作業が済んだらプロジェクトのルートに戻っておきましょう

```bash
cd ..
```

## コンポーネントの追加

まずカードコンポーネントを作ってみましょう

### コンポーネントファイルの作成

"lib/pines_sample_web/components/card.ex" に以下の内容のファイルを作ります

```elixir
defmodule PinesSampleWeb.Components.Card do
  @moduledoc """
  Card.

  # Example
  <PinesSampleWeb.Components.Card.render />
  """
  use PinesSampleWeb, :live_component

  def render(assigns) do
    ~H"""
    Card
    """
  end
end
```

これは Phoenix でコンポーネントを作る場合の雛形になっています

H シギル内（`H"""` から `"""` の間）が画面に表示される HTML のテンプレートです

https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html

### コンポーネント表示用画面の作成

コンポーネントを表示するため、 Phoenix LiveView による画面を作成しましょう

Phoenix LiveView では、xxx.ex （動作の定義）と同じ階層に xxx.html.heex （外観のテンプレート）を作成することで画面を実装できます

https://github.com/phoenixframework/phoenix_live_view

まず "lib/pines_sample_web/live" ディレクトリーを作成します

"lib/pines_sample_web/live/showcase.ex" を以下の内容で作成します

```elixir
defmodule PinesSampleWeb.Showcase do
  use PinesSampleWeb, :live_view

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

  def handle_event("card_button_clicked", _, socket) do
    IO.inspect("card_button_clicked")
    {:noreply, socket}
  end
end
```

また、"lib/pines_sample_web/live/showcase.html.heex" を以下の内容で作成します

```heex
<div class="w-full">
  <div class="m-4">
    <p class="text-xl font-bold mb-4">Card</p>
    <div class="flex items-center justify-center p-8 border border-nuetral-300 rounded-md">
      <PinesSampleWeb.Components.Card.render />
    </div>
  </div>
</div>
```

### Router の設定

"lib/pines_sample_web/router.ex" を以下のように編集します

```diff_elixir
...
  scope "/", PinesSampleWeb do
    pipe_through :browser

    get "/", PageController, :home
  end
+
+  scope "/" do
+   pipe_through :browser
+
+   live "/live", PinesSampleWeb.Showcase
+ end
...
```

これにより、 <http://localhost:4000/live> にアクセスすることで `PinesSampleWeb.Showcase` の画面が表示されるようになります

### Phoeix の起動

以下のコマンドを実行し、 Phoenix を起動します

```bash
mix phx.server
```

以下のような結果が表示されます

```
Compiling 14 files (.ex)
Generated pines_sample app
[info] Running PinesSampleWeb.Endpoint with cowboy 2.10.0 at 127.0.0.1:4000 (http)
[info] Access PinesSampleWeb.Endpoint at http://localhost:4000
[watch] build finished, watching for changes...

Rebuilding...

Done in 144ms.
```

ブラウザで <http://localhost:4000/live> を開きます

![スクリーンショット 2023-08-14 17.14.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/2a7f3585-fcff-fe9a-e9fc-496c813b7975.png)

枠の中の "Card" の部分がコンポーネントです

このコンポーネントを改造していきます

### Pines のコピー＆ペースト

Pines 公式ドキュメントの Card コンポーネントのページを開きます

https://devdojo.com/pines/docs/card

コピーボタン（下画像の赤枠部分）をクリックし、コードをコピーします

![スクリーンショット 2023-08-14 17.17.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/9c844433-bb6e-8b56-8d13-f863863bd9cc.png)

"lib/pines_sample_web/components/card.ex" の H シギル内にペーストします

```elixir
defmodule PinesSampleWeb.Components.Card do
  @moduledoc """
  Card.

  # Example
  <PinesSampleWeb.Components.Card.render />
  """
  use PinesSampleWeb, :live_component

  def render(assigns) do
    ~H"""
    <div class="rounded-lg overflow-hidden border border-neutral-200/60 bg-white text-neutral-700 shadow-sm w-[380px]">
<div class="relative">
<img src="https://images.unsplash.com/photo-1542291026-7eec264c27ff?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2370&q=80" class="w-full h-auto" />
</div>
<div class="p-7">
<h2 class="mb-2 text-lg font-bold leading-none tracking-tight">Product Name</h2>
<p class="mb-5 text-neutral-500">This card element can be used to display a product, post, or any other type of data.</p>
<button class="inline-flex items-center justify-center w-full h-10 px-4 py-2 text-sm font-medium text-white transition-colors rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none bg-neutral-950 hover:bg-neutral-950/90">View Product</button>
</div>
</div>
    """
  end
end
```

ファイルを上書き保存して <http://localhost:4000/live> を見ると、下画像のように Pines 公式ドキュメントと全く同じカードが実装できています

![スクリーンショット 2023-08-14 17.21.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/5dce895d-a152-7f74-c1b5-886cf04d7378.png)

### 引数の追加

このままだと固定値を表示するだけなので、変えたい部分を引数にします

更に Phoenix LiveView によるボタンクリック時のイベント `phx-click` を追加します

また、単純に貼り付けたためにインデントなどが狂っているので修正します

```elixir
defmodule PinesSampleWeb.Components.Card do
  @moduledoc """
  Card.

  # Example
  <PinesSampleWeb.Components.Card.render
    img_src="https://www.phoenixframework.org/images/blog/1.7-released-e6dc45801b961cb0bb04e6e2a907fbc4.png?vsn=d"
    title="Card"
    message="Card message"
    button_text="Button"
    button_event="card_button_clicked"
  />
  """
  use PinesSampleWeb, :live_component

  attr :img_src, :string, default: ""
  attr :title, :string, default: ""
  attr :message, :string, default: ""
  attr :button_text, :string, default: "Click"
  attr :button_event, :string, default: "card_button_clicked"

  def render(assigns) do
    ~H"""
    <div class="rounded-lg overflow-hidden border border-neutral-200/60 bg-white text-neutral-700 shadow-sm w-[380px]">
      <div class="relative">
        <img src={@img_src} class="w-full h-auto" />
      </div>
      <div class="p-7">
        <h2 class="mb-2 text-lg font-bold leading-none tracking-tight">
          <%= @title %>
        </h2>
        <p class="mb-5 text-neutral-500">
          <%= @message %>
        </p>
        <button
          class="inline-flex items-center justify-center w-full h-10 px-4 py-2 text-sm font-medium text-white transition-colors rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none bg-neutral-950 hover:bg-neutral-950/90"
          phx-click={@button_event}
        >
          <%= @button_text %>
        </button>
      </div>
    </div>
    """
  end
end
```

`attr` から始まっている行が引数です

以下の4つを引数として追加しています

- img_src: 画像 URL
- title: カードのタイトル
- message: カードの本文
- button_text: カードのボタン文言
- button_event: カードのボタンクリックイベント

`<img src={@img_src} class="w-full h-auto" />` のように HTML のタグ内で `<属性>={@<引数名>}` とすることで引数の値をタグの属性に渡せます

また、 `<%= @<引数名> %>` で画面上に引数の値を表示できます

`phx-click={@button_event}` により、コンポーネントを使う画面の `handle_event` 関数でイベントを処理できるようになります

各引数はデフォルト値を設定しているため、この時点で <http://localhost:4000/live> は以下のような表示になります

![スクリーンショット 2023-08-14 17.31.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/556b01b3-dd7c-2fdf-80c6-42277a3160ea.png)

"lib/pines_sample_web/live/showcase.html.heex" を以下の内容に編集しましょう

```heex
<div class="w-full">
  <div class="m-4">
    <p class="text-xl font-bold mb-4">Card</p>
    <div class="flex items-center justify-center p-8 border border-nuetral-300 rounded-md">
      <PinesSampleWeb.Components.Card.render
        img_src="https://www.phoenixframework.org/images/blog/1.7-released-e6dc45801b961cb0bb04e6e2a907fbc4.png?vsn=d"
        title="Card"
        message="Card message"
        button_text="Button"
        button_event="card_button_clicked"
      />
    </div>
  </div>
</div>
```

コンポーネントに引数の値が渡され、 <http://localhost:4000/live> は以下のような表示になります

![スクリーンショット 2023-08-14 17.34.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/2316316e-fc3f-f4f7-27e7-dc854710808c.png)

また、ボタンをクリックすると Phoenix を起動したターミナルに以下のようなログが表示され、イベントを処理できていることが確認できます

```
"card_button_clicked"
[debug] HANDLE EVENT "card_button_clicked" in PinesSampleWeb.Showcase
  Parameters: %{"value" => ""}
[debug] Replied in 503µs
```

これで Phoenix LiveView に Pines のカードコンポーネントを追加できました

### アコーディオンの追加

カードと同様にアコーディオンを追加します

"lib/pines_sample_web/components/accordion.ex" を以下の内容で作成します

```elixir
defmodule PinesSampleWeb.Components.Accordion do
  @moduledoc """
  Accordion.

  # Example
  <PinesSampleWeb.Components.Accordion.render />
  """
  use PinesSampleWeb, :live_component

  def render(assigns) do
    ~H"""
    Accordion
    """
  end
end
```

H シギルの中に Pines 公式ドキュメントからアコーディオンのコードをコピー＆ペーストします

https://devdojo.com/pines/docs/accordion

ただし、**コードに含まれる `:class=` の文言がエラーになるため、 `:class=` を `x-bind:class=` に一括置換します**

```elixir
defmodule PinesSampleWeb.Components.Accordion do
  @moduledoc """
  Accordion.

  # Example
  <PinesSampleWeb.Components.Accordion.render />
  """
  use PinesSampleWeb, :live_component

  def render(assigns) do
    ~H"""
    <div x-data="{
      activeAccordion: '',
      setActiveAccordion(id) {
          this.activeAccordion = (this.activeAccordion == id) ? '' : id
      }
  }" class="relative w-full mx-auto overflow-hidden text-sm font-normal bg-white border border-gray-200 divide-y divide-gray-200 rounded-md">
  <div x-data="{ id: $id('accordion') }" class="cursor-pointer group">
      <button @click="setActiveAccordion(id)" class="flex items-center justify-between w-full p-4 text-left select-none group-hover:underline">
          <span>What is Pines?</span>
          <svg class="w-4 h-4 duration-200 ease-out" x-bind:class="{ 'rotate-180': activeAccordion==id }" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
      </button>
      <div x-show="activeAccordion==id" x-collapse x-cloak>
          <div class="p-4 pt-0 opacity-70">
              Pines is a UI library built for AlpineJS and TailwindCSS.
          </div>
      </div>
  </div>
  <div x-data="{ id: $id('accordion') }" class="cursor-pointer group">
      <button @click="setActiveAccordion(id)" class="flex items-center justify-between w-full p-4 text-left select-none group-hover:underline">
          <span>How do I install Pines?</span>
          <svg class="w-4 h-4 duration-200 ease-out" x-bind:class="{ 'rotate-180': activeAccordion==id }" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
      </button>
      <div x-show="activeAccordion==id" x-collapse x-cloak>
          <div class="p-4 pt-0 opacity-70">
              Add AlpineJS and TailwindCSS to your page and then copy and paste any of these elements into your project.
          </div>
      </div>
  </div>
  <div x-data="{ id: $id('accordion') }" class="cursor-pointer group">
      <button @click="setActiveAccordion(id)" class="flex items-center justify-between w-full p-4 text-left select-none group-hover:underline">
          <span>Can I use Pines with other libraries or frameworks?</span>
          <svg class="w-4 h-4 duration-200 ease-out" x-bind:class="{ 'rotate-180': activeAccordion==id }" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
      </button>
      <div x-show="activeAccordion==id" x-collapse x-cloak>
          <div class="p-4 pt-0 opacity-70">
              Absolutely! Pines works with any other library or framework. Pines works especially well with the TALL stack.
          </div>
      </div>
  </div>
</div>
    """
  end
end
```

アコーディオンを画面上に追加するため、 "lib/pines_sample_web/live/showcase.html.heex" を以下のように編集します

```heex
<div class="w-full">
+ <div class="m-4">
+   <p class="text-xl font-bold mb-4">Accordion</p>
+   <div class="p-8 border border-nuetral-300 rounded-md">
+     <PinesSampleWeb.Components.Accordion.render />
+   </div>
+ </div>
  <div class="m-4">
...
</div>
```

これにより、 <http://localhost:4000/live> にアコーディオンが追加できました

![スクリーンショット 2023-08-15 2.28.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/601f1a99-8cda-07f0-4b0b-33ff852e6dea.png)

### アコーディオンへの引数追加

アコーディオンも引数によって内容を制御できるようにしましょう

"lib/pines_sample_web/components/accordion.ex" を以下の内容に変更します

```elixir
defmodule PinesSampleWeb.Components.Accordion do
  @moduledoc """
  Accordion.

  # Example
  <PinesSampleWeb.Components.Accordion.render accordions={@accordions} />
  """
  use PinesSampleWeb, :live_component

  attr :accordions, :list

  def render(assigns) do
    ~H"""
    <div x-data="{
      activeAccordion: '',
      setActiveAccordion(id) {
        this.activeAccordion = (this.activeAccordion == id) ? '' : id
      }
    }" class="relative w-full mx-auto overflow-hidden text-sm font-normal bg-white border border-gray-200 divide-y divide-gray-200 rounded-md">
      <%= for accordion <- @accordions do %>
        <div x-data="{ id: $id('accordion') }" class="cursor-pointer group">
          <button @click="setActiveAccordion(id)" class="flex items-center justify-between w-full p-4 text-left select-none group-hover:underline">
            <span><%= accordion.title %></span>
            <svg class="w-4 h-4 duration-200 ease-out" x-bind:class="{ 'rotate-180': activeAccordion==id }" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
          </button>
          <div x-show="activeAccordion==id" x-collapse x-cloak>
            <div class="p-4 pt-0 opacity-70">
              <%= accordion.message %>
            </div>
          </div>
        </div>
      <% end %>
    </div>
    """
  end
end
```

アコーディオンの中に表示するタイトルとメッセージを List で渡すようにしました

"lib/pines_sample_web/live/showcase.ex" を以下のように編集します

```elixir
defmodule PinesSampleWeb.Showcase do
  use PinesSampleWeb, :live_view

  def mount(_params, _session, socket) do
    socket =
      socket
      |> assign(:accordions, [
        %{
          title: "Accordion 1",
          message: "Message 1"
        },
        %{
          title: "Accordion 2",
          message: "Message 2"
        },
        %{
          title: "Accordion 3",
          message: "Message 3"
        }
      ])

    {:ok, socket}
  end

  def handle_event(event, _, socket) do
    IO.inspect(event)
    {:noreply, socket}
  end
end
```

また、 "lib/pines_sample_web/live/showcase.html.heex" を以下のように編集します

```heex
<div class="w-full">
  <div class="m-4">
    <p class="text-xl font-bold mb-4">Accordion</p>
    <div class="p-8 border border-nuetral-300 rounded-md">
      <PinesSampleWeb.Components.Accordion.render accordions={@accordions} />
    </div>
  </div>
...
```

これにより、 "showcase.ex" で定義したアコーディオンの内容をコンポーネントに渡すことができます

<http://localhost:4000/live> を開くと、アコーディオンの内容が変化しました

![スクリーンショット 2023-08-15 2.37.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/51182607-6484-bf2e-e2d2-efed72edf9a9.png)

### ボタンの実装

ここまでと同じ容量でボタンコンポーネントを作りましょう

Pines 公式ドキュメントでボタンコンポーネントの上から２番目の色付きボタンを実装します

https://devdojo.com/pines/docs/button

![スクリーンショット 2023-08-15 20.59.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/24ee39d0-572f-6128-9680-f3f8fa1d4428.png)

"lib/pines_sample_web/components/button.ex" を以下の内容で作成します

今回は最終的な引数追加まで含めた結果だけ以下に示しています

```elixir
defmodule PinesSampleWeb.Components.Button do
  @moduledoc """
  Button.

  # Example
  <PinesSampleWeb.Components.Button.render text="Click" bg_color="blue" event="button_clicked" />
  """
  use PinesSampleWeb, :live_component

  attr :text, :string
  attr :bg_color, :string, default: "white"
  attr :event, :string, default: "button_clicked"

  def render(assigns) do
    {bg_color, border_color, hover_bg_color, focus_ring_color, color} =
      case assigns.bg_color do
        "blue" -> {"bg-blue-600", "", "hover:bg-blue-700", "focus:ring-blue-700", "text-white"}
        "red" -> {"bg-red-600", "", "hover:bg-red-700", "focus:ring-red-700", "text-white"}
        "green" -> {"bg-green-600", "", "hover:bg-green-700", "focus:ring-green-700", "text-white"}
        "yellow" -> {"bg-yellow-500", "", "hover:bg-yellow-600", "focus:ring-yellow-600", "text-white"}
        _ -> {"bg-white", "border border-neutral-200/70", "hover:bg-neutral-100", "focus:ring-neutral-200/60", "text-neutral-500"}
      end

    assigns =
      assigns
      |> assign(:bg_color, bg_color)
      |> assign(:border_color, border_color)
      |> assign(:hover_bg_color, hover_bg_color)
      |> assign(:focus_ring_color, focus_ring_color)
      |> assign(:color, color)

    ~H"""
    <button
      type="button"
      class={"inline-flex items-center justify-center px-4 py-2 text-sm font-medium tracking-wide #{@color} transition-colors duration-200 #{@bg_color} rounded-md #{@border_color} #{@hover_bg_color} focus:ring-2 focus:ring-offset-2 #{@focus_ring_color} focus:shadow-outline focus:outline-none"}
      phx-click={@event}
    >
      <%= @text %>
    </button>
    """
  end
end
```

少しややこしくなっていますが、以下のコードで引数の `bg_color` に応じて、背景色、ボーダー色、ホバー時の背景色、フォーカス時に出る枠線の色、文字色を切り替えるように実装しています

```elixir
    {bg_color, border_color, hover_bg_color, focus_ring_color, color} =
      case assigns.bg_color do
        "blue" -> {"bg-blue-600", "", "hover:bg-blue-700", "focus:ring-blue-700", "text-white"}
        "red" -> {"bg-red-600", "", "hover:bg-red-700", "focus:ring-red-700", "text-white"}
        ...
```

ここで注意しなければならないのが、必ず `hover:bg-neutral-100` のようにクラス名全体を変数に入れることです

Tailwind CSS は実際に使われたクラス名にしかスタイルシートを生成しません

そのため、 `hover:` が共通だからといって変数に含めない場合、 Tailwind CSS が `hover:bg-neutral-100` のスタイルシートを生成せず、ホバーしても色が変化しません

関数内の変数をソケットにアサインしています

（変数を H シギル内に直接渡すと LiveView による変更が反映されません）

```elixir
    assigns =
      assigns
      |> assign(:bg_color, bg_color)
      |> assign(:border_color, border_color)
      ...
```

クラス名（Tailwind CSS によるスタイル）をソケット内の値で置き換える場合、以下のように `class={"..."}` としてダブルクォーテーションの外を `{}` で囲い、ソケットの各キーを `#{}` で囲います

```elixir
      class={"... #{@color} ... #{@bg_color} ..."}
```

ボタンを画面に表示しましょう

"lib/pines_sample_web/live/showcase.ex" を以下のように編集します

```elixir
defmodule PinesSampleWeb.Showcase do
  use PinesSampleWeb, :live_view

  def mount(_params, _session, socket) do
    socket =
      socket
      |> assign(:accordions, [
        %{
          title: "Accordion 1",
          message: "Message 1"
        },
        %{
          title: "Accordion 2",
          message: "Message 2"
        },
        %{
          title: "Accordion 3",
          message: "Message 3"
        }
      ])
      |> assign(:buttons, [
        %{
          text: "Button 1",
          bg_color: "white",
          event: "button_1_clicked"
        },
        %{
          text: "Button 2",
          bg_color: "blue",
          event: "button_2_clicked"
        },
        %{
          text: "Button 3",
          bg_color: "red",
          event: "button_3_clicked"
        },
        %{
          text: "Button 4",
          bg_color: "green",
          event: "button_4_clicked"
        },
        %{
          text: "Button 5",
          bg_color: "yellow",
          event: "button_5_clicked"
        },
      ])

    {:ok, socket}
  end

  def handle_event(event, _, socket) do
    IO.inspect(event)
    {:noreply, socket}
  end
end
```

"lib/pines_sample_web/live/showcase.html.heex" を以下のように編集します

```heex
<div class="w-full">
  <div class="m-4">
    <p class="text-xl font-bold mb-4">Button</p>
    <div class="flex items-center justify-center space-x-4 p-8 border border-nuetral-300 rounded-md">
      <%= for button <- @buttons do %>
        <PinesSampleWeb.Components.Button.render
          text={button.text}
          bg_color={button.bg_color}
          event={button.event}
        />
      <% end %>
    </div>
  </div>
...
```

<http://localhost:4000/live> に以下のようにボタンが追加されます

![スクリーンショット 2023-08-15 21.25.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/1378dfd8-3a1b-7650-335e-7fcb2197b3fc.png)

## まとめ

Pines を使うことで、 Phoenix LiveView にも簡単にコンポーネントを追加できました

Pines 公式ドキュメントには他にも様々なコンポーネントが紹介されているので、そこからどんどんコピー&ペーストしましょう