SaladUI を mix phx.gen.live で生成した画面に組み込む

## はじめに

前回、とりあえず Phoenix のトップ画面にコンポーネントを配置してみました

https://qiita.com/RyoWakabayashi/items/c3dc2ec8bb67adb6dc3e

しかし、それだと実用的ではないので `mix phx.gen.live` で生成した画面に組み込んでみます

## 基本となるプロジェクトの準備

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

新しいプロジェクトを作成し、ディレクトリー配下に移動します

```shell
mix archive.install hex phx_new
mix phx.new salad_ui_sample
cd salad_ui_sample
```

### データベースの用意

今回はデータベースを使用するため、 Docker で起動します（ローカルで起動している場合などはそのまま使って問題ありません）

"docker-compose.yml" をカレントディレクトリーに以下の内容で作成します

`POSTGRES_USER` などの値は作成したプロジェクトの設定に従っています

```yml
---

services:
  db:
    container_name: postgres
    image: postgres:15.7-bullseye
    ports:
      - "5432:5432"
    volumes:
      - postgres:/var/lib/postgresql/data
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=salad_ui_sample_dev

volumes:
  postgres:
```

PostgreSQL 　のコンテナを起動します

`--detach` でコンテナを裏で起動したままにしています

```shell
docker compose up --detach
```

依存モジュールをインストールし、データベースを作成します

```shell
mix setup
```

### 画面の生成

以下のコマンドでユーザー一覧、ユーザー詳細、ユーザー作成/更新などの画面を一括生成します

```shell
mix phx.gen.live Accounts User users name:string age:integer
```

"lib/salad_ui_sample_web/router.ex" に各画面のルートを追記します

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

    get "/", PageController, :home

+   live "/users", UserLive.Index, :index
+   live "/users/new", UserLive.Index, :new
+   live "/users/:id/edit", UserLive.Index, :edit
+
+   live "/users/:id", UserLive.Show, :show
+   live "/users/:id/show/edit", UserLive.Show, :edit
  end
...
```

ユーザーテーブルをデータベース上に作成します

```shell
mix ecto.migrate
```

### ベースとなる画面の確認

Phoenix を起動します

```shell
mix phx.server
```

`http://localhost:4000/users` にアクセスすると、ユーザー一覧画面が確認できます

![スクリーンショット 2024-07-13 11.20.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/6652bc21-99fe-8eaa-a467-d3ba1fb20c53.png)

ユーザーを一人作成してみましょう

![スクリーンショット 2024-07-13 11.20.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/0aef1703-5f71-aa11-a1a5-d3fd2c3eb4c8.png)

![スクリーンショット 2024-07-13 11.21.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/27f5426e-716e-5b6e-a819-5a2c1df04773.png)

## SaladUI の導入

プロジェクトに SaladUI を導入します

前回の手順とほぼ同じです

### SaladUI モジュールの追加

`{:salad_ui, "~> 0.5"}` を "mix.exs" 内の依存モジュールに追加します

```diff_elixir
...
  defp deps do
    [
      {:phoenix, "~> 1.7.14"},
      {:phoenix_html, "~> 4.1"},
      {:phoenix_live_reload, "~> 1.2", only: :dev},
      # TODO bump on release to {:phoenix_live_view, "~> 1.0.0"},
      {:phoenix_live_view, "~> 1.0.0-rc.1", override: true},
      ...
-     {:bandit, "~> 1.5"}
+     {:bandit, "~> 1.5"},
+     {:salad_ui, "~> 0.5"}
    ]
  end
...
```

依存モジュールをインストールします

```shell
mix deps.get
```

### カスタムカラーの追加

shadcn/ui のテーマ提供ページを開きます

https://ui.shadcn.com/themes

:::note info
前回から少し画面が変わりました
:::

![スクリーンショット 2024-07-13 11.23.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/1a528a1c-34ea-d2d1-de79-b3515a2102e5.png)

"Customize" のボタンをクリックして好きな色を選択します

![スクリーンショット 2024-07-13 11.24.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/26423b38-c9d3-3b00-88c3-68de51f870fc.png)

カスタムカラー用の CSS コードがモーダルで開くので、右上 "Copy" をクリックしてコードをクリップボードにコピーしておきます

![スクリーンショット 2024-07-13 11.28.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/dc7b6911-39bb-87fd-a713-95c2200119f1.png)

プロジェクト内の "assets/css/app.css" にコピーしたコードを追加します

```diff_css
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

/* This file is for your main application CSS */

+@layer base {
+  :root {
+    --background: 0 0% 100%;
+    --foreground: 222.2 84% 4.9%;
+    --card: 0 0% 100%;
...
+   --border: 217.2 32.6% 17.5%;
+   --input: 217.2 32.6% 17.5%;
+   --ring: 224.3 76.3% 48%;
+ }
+}
```

"assets/tailwind.colors.json" を新規作成し、以下のコードを貼り付けます

```json
{
  "accent": {
    "DEFAULT": "hsl(var(--accent))",
    "foreground": "hsl(var(--accent-foreground))"
  },
  "background": "hsl(var(--background))",
  "border": "hsl(var(--border))",
  "card": {
    "DEFAULT": "hsl(var(--card))",
    "foreground": "hsl(var(--card-foreground))"
  },
  "destructive": {
    "DEFAULT": "hsl(var(--destructive))",
    "foreground": "hsl(var(--destructive-foreground))"
  },
  "foreground": "hsl(var(--foreground))",
  "input": "hsl(var(--input))",
  "muted": {
    "DEFAULT": "hsl(var(--muted))",
    "foreground": "hsl(var(--muted-foreground))"
  },
  "popover": {
    "DEFAULT": "hsl(var(--popover))",
    "foreground": "hsl(var(--popover-foreground))"
  },
  "primary": {
    "DEFAULT": "hsl(var(--primary))",
    "foreground": "hsl(var(--primary-foreground))"
  },
  "ring": "hsl(var(--ring))",
  "secondary": {
    "DEFAULT": "hsl(var(--secondary))",
    "foreground": "hsl(var(--secondary-foreground))"
  }
}
```

### TailwindCSS の設定

"assets/tailwind.config.js" を開いて編集します

```diff_javascript
...
module.exports = {
  content: [
    "./js/**/*.js",
+   "../deps/salad_ui/lib/**/*.ex",
    "../lib/salad_ui_sample_web.ex",
    "../lib/salad_ui_sample_web/**/*.*ex"
  ],
  theme: {
    extend: {
-     colors: {
-       brand: "#FD4F00",
-     }
+     colors: require("./tailwind.colors.json")
    },
  },
  plugins: [
    require("@tailwindcss/forms"),
+   require("@tailwindcss/typography"),
+   require("tailwindcss-animate"),
...
```

### tailwindcss-animate の追加

TailwindCSS のアニメーション用プラグイン tailwindcss-animate を追加します

https://github.com/jamiebuilds/tailwindcss-animate

```shell
cd assets
yarn add -D tailwindcss-animate
cd ..
```

### tails の設定

TailwindCSS の Elixir 用ユーティリティーである tails を設定します

https://github.com/zachdaniel/tails

"config/config.exs" に以下のコードを追加します

```diff_elixir
config :tails, colors_file: Path.join(File.cwd!(), "assets/tailwind.colors.json")
```

## SaladUI コンポーネントの適用、追加

ベースとなる画面に SaladUI を組み込んでいきます

### 別名の設定

CoreComponents と共存させるため、使用する各コンポーネントに別名をつけておきます

"lib/salad_ui_sample_web.ex"

```diff_elixir
...
  defp html_helpers do
    quote do
      # HTML escaping functionality
      import Phoenix.HTML
      # Core UI components and translation
      import SaladUiSampleWeb.CoreComponents
      import SaladUiSampleWeb.Gettext

      # Shortcut for generating JS commands
      alias Phoenix.LiveView.JS

+     alias SaladUI.Button, as: SaladButton
+     alias SaladUI.Input, as: SaladInput
+     alias SaladUI.Tooltip, as: SaladTooltip

      # Routes generation with the ~p sigil
      unquote(verified_routes())
    end
  end
...
```

### ボタンの変更

画面内のボタンに SaladButton の別名を付けるだけで変更可能です

選択したテーマの色が適用されます

"lib/salad_ui_sample_web/live/user_live/index.html.heex"

```diff_elixir
<.header>
  Listing Users
  <:actions>
    <.link patch={~p"/users/new"}>
-     <.button>New User</.button>
+     <SaladButton.button>New User</SaladButton.button>
    </.link>
  </:actions>
</.header>
...
```

![スクリーンショット 2024-07-13 12.09.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/4e2eb3a6-87b2-7c25-3ad1-f8ef48a33c00.png)

lib/salad_ui_sample_web/live/user_live/show.html.heex

```diff_elixir
<.header>
  User <%= @user.id %>
  <:subtitle>This is a user record from your database.</:subtitle>
  <:actions>
    <.link patch={~p"/users/#{@user}/show/edit"} phx-click={JS.push_focus()}>
-     <.button>Edit user</.button>
+     <SaladButton.button>Edit user</SaladButtonbutton>
    </.link>
  </:actions>
</.header>
```

![スクリーンショット 2024-07-13 12.09.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/7ea90e77-9d90-f3c3-2926-bcb089966041.png)

lib/salad_ui_sample_web/live/user_live/form_component.ex

```diff_elixir
...
        <:actions>
-         <.button phx-disable-with="Saving...">Save User</.button>
+         <SaladButton.button phx-disable-with="Saving...">Save User</SaladButton.button>
        </:actions>
...
```

![スクリーンショット 2024-07-13 12.09.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/bbbd9365-25b4-6dfa-76c3-7d4cd34be81a.png)

### Input の変更

Input の場合、単純に入れ替えるだけではいけません

外観は `SaladInput` を付けただけですぐに変わりますが、値を変更したりするとエラーが出力されます

`name` 属性に `user[name]` のように `<テーブル名>[<列名>]` となるように値を設定しましょう

```diff_elixir
...
      <.simple_form
        for={@form}
        id="user-form"
        phx-target={@myself}
        phx-change="validate"
        phx-submit="save"
      >
-       <.input field={@form[:name]} type="text" label="Name" />
+       <SaladInput.input
+         field={@form[:name]}
+         type="text"
+         placeholder="Name"
+         name="user[name]"
+         id="user_name"
+       />
-       <.input field={@form[:age]} type="number" label="Age" />
+       <SaladInput.input
+         field={@form[:age]}
+         type="number"
+         placeholder="Age"
+         name="user[age]"
+         id="user_id"
+       />
        <:actions>
          <SaladButton.button phx-disable-with="Saving...">Save User</SaladButton.button>
        </:actions>
      </.simple_form>
...
```

![スクリーンショット 2024-07-13 12.13.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/ecae3316-ffab-4c0e-fe6e-288407b9d326.png)

### Tooltip を追加する

マウスオーバーでツールチップを表示させたい場合、 `SaladTooltip.tooltip` タグでマウスオーバーの対象を囲み、 `SaladTooltip.tooltip_content` 内にツールチップの内容を記載します

```diff_elixir
<.header>
  Listing Users
  <:actions>
-   <.link patch={~p"/users/new"}>
-     <SaladButton.button>New User</SaladButton.button>
-   </.link>
+   <SaladTooltip.tooltip>
+     <.link patch={~p"/users/new"}>
+       <SaladButton.button>New User</SaladButton.button>
+     </.link>
+     <SaladTooltip.tooltip_content>
+         <p>Open Modal.</p>
+     </SaladTooltip.tooltip_content>
+   </SaladTooltip.tooltip>
  </:actions>
</.header>
```

![tooltip.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/77b987f2-41c6-60a0-423e-ee538fb52718.gif)

## まとめ

SaladUI のコンポーネントに別名を付けることで CoreComponents と共存させました

外見だけなら別名を頭に付けるだけで既存のコンポーネントを変換可能ですが、特に入力が絡むようなものはそのままではエラーになるため、内部処理を理解して書き換えましょう

ちなみに、 Table は大掛かりな変更になりそうなので今回は対象外としました