6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ElixirAdvent Calendar 2024

Day 19

Phoenix LiveView アプリケーションに Flowbite の UI コンポーネントを導入する

Last updated at Posted at 2024-12-25

はじめに

Phoenix LiveView は Elixir の Web フレームワークです

JavaScript をほぼ書くことなく、 Elixir でフロントエンドもバックエンドも実装できます

Flowbite は Tailwind CSS を使った UI コンポーネントのライブラリです

簡単に導入でき、様々な Web フレームワークと一緒に利用できます

Phoenix LiveView アプリケーションに Flowbite を導入する手順について、 Flowbite のドキュメントに掲載されています

しかし、情報が古い(Phoenix 1.6 時点)ので、2024年12月現在(Phoenix 1.7 時点)での手順を残しておきます

実装したアプリケーションはこちら

実行環境(前提条件)

Erlang と Elixir 、 Node.js をインストールしているものとします

各言語のバージョン

  • Erlang 27.2
  • Elixir 1.17.3
  • Node.js 22.12.0

macOS であれば miseasdf を使ってインストールするのがおすすめです

mise の場合

mise use -g erlang@27.2
mise use -g elixir@1.17.3
mise use -g node@22.12.0

asdf の場合

asdf plugin add erlang
asdf install erlang 27.2
asdf global erlang 27.2

asdf plugin add elixir
asdf install elixir 1.17.3
asdf global elixir 1.17.3

asdf plugin add nodejs
asdf install nodejs 22.12.0
asdf global nodejs 22.12.0

elixir のパッケージマネージャー Hex をインストールします

mix local.hex

Phoenix プロジェクトを生成するための phx_new をインストールします

mix archive.install hex phx_new

Phoenix プロジェクトの作成

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

今回は UI だけ見たいので DB 操作をしないため --no-ecto のオプションを付けています

mix phx.new phoenix_flowbite_sample --no-ecto

Fetch and install dependencies? [Yn] と質問されるので、そのまま Enter を押下(デフォルトの yes で回答)します

phoenix_flowbite_sample ディレクトリーが作成されるので、中に移動します

cd phoenix_flowbite_sample

Phoenix アプリケーションを起動します

mix phx.server

http://localhost:4000 にアクセスすると、以下のような画面が表示されます

スクリーンショット 2024-12-25 10.35.08.png

一旦、 Ctrl + C でアプリケーションを停止します

Flowbite の導入

Flowbite のドキュメントでは Tailwind CSS 導入の手順を書いていますが、 Phoenix 1.7 ではデフォルトで Tailwind CSS インストール済になっています

Tailwind CSS 導入の手順を飛ばして、 Flowbite を導入します

assets ディレクトリーに移動します

cd assets

以下の内容で assets/package.json ファイルを作成します

{
  "name": "phoenix_flowbite_sample",
  "version": "1.0.0",
  "main": "js/app.jp",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "flowbite": "^2.5.2"
  }
}

Flowbite をインストールします

npm install

上のディレクトリーに戻っておきます

cd ..

assets/tailwind.config.js を以下のように編集します

...
module.exports = {
  content: [
    "./js/**/*.js",
+   "./node_modules/flowbite/**/*.js",
    "../lib/phoenix_flowbite_sample_web.ex",
    "../lib/phoenix_flowbite_sample_web/**/*.*ex"
  ],
  ...
  plugins: [
+   require('flowbite/plugin'),
    require("@tailwindcss/forms"),
  ...
}

assets/js/app.js を以下のように編集します

...
import topbar from "../vendor/topbar"
+import "flowbite/dist/flowbite.phoenix.js"

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
...

Flowbite コンポーネントの配置

lib/phoenix_flowbite_sample_web/controllers/page_html/home.html.heex を以下の内容に書き換えます

<.flash_group flash={@flash} />
<div class="px-20 py-10 sm:px-20 sm:py-28 lg:px-20 xl:px-60 xl:py-32">
  <div id="alerts">
    <div
      class="flex items-center p-4 mb-4 text-sm text-blue-800 rounded-lg bg-blue-50 dark:bg-gray-800 dark:text-blue-400"
      role="alert"
    >
      <svg
        class="flex-shrink-0 inline w-4 h-4 me-3"
        aria-hidden="true"
        xmlns="http://www.w3.org/2000/svg"
        fill="currentColor"
        viewBox="0 0 20 20"
      >
        <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z" />
      </svg>
      <span class="sr-only">Info</span>
      <div>
        <span class="font-medium">Info alert!</span>
        Change a few things up and try submitting again.
      </div>
    </div>
    <div
      class="flex items-center p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400"
      role="alert"
    >
      <svg
        class="flex-shrink-0 inline w-4 h-4 me-3"
        aria-hidden="true"
        xmlns="http://www.w3.org/2000/svg"
        fill="currentColor"
        viewBox="0 0 20 20"
      >
        <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z" />
      </svg>
      <span class="sr-only">Info</span>
      <div>
        <span class="font-medium">Danger alert!</span>
        Change a few things up and try submitting again.
      </div>
    </div>
    <div
      class="flex items-center p-4 mb-4 text-sm text-green-800 rounded-lg bg-green-50 dark:bg-gray-800 dark:text-green-400"
      role="alert"
    >
      <svg
        class="flex-shrink-0 inline w-4 h-4 me-3"
        aria-hidden="true"
        xmlns="http://www.w3.org/2000/svg"
        fill="currentColor"
        viewBox="0 0 20 20"
      >
        <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z" />
      </svg>
      <span class="sr-only">Info</span>
      <div>
        <span class="font-medium">Success alert!</span>
        Change a few things up and try submitting again.
      </div>
    </div>
    <div
      class="flex items-center p-4 mb-4 text-sm text-yellow-800 rounded-lg bg-yellow-50 dark:bg-gray-800 dark:text-yellow-300"
      role="alert"
    >
      <svg
        class="flex-shrink-0 inline w-4 h-4 me-3"
        aria-hidden="true"
        xmlns="http://www.w3.org/2000/svg"
        fill="currentColor"
        viewBox="0 0 20 20"
      >
        <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z" />
      </svg>
      <span class="sr-only">Info</span>
      <div>
        <span class="font-medium">Warning alert!</span>
        Change a few things up and try submitting again.
      </div>
    </div>
    <div
      class="flex items-center p-4 text-sm text-gray-800 rounded-lg bg-gray-50 dark:bg-gray-800 dark:text-gray-300"
      role="alert"
    >
      <svg
        class="flex-shrink-0 inline w-4 h-4 me-3"
        aria-hidden="true"
        xmlns="http://www.w3.org/2000/svg"
        fill="currentColor"
        viewBox="0 0 20 20"
      >
        <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z" />
      </svg>
      <span class="sr-only">Info</span>
      <div>
        <span class="font-medium">Dark alert!</span>
        Change a few things up and try submitting again.
      </div>
    </div>
  </div>
</div>

<div id="alerts">...</div> 内部については Flowbite のドキュメントからコピーしたものです(貼り付けて保存後、 mix format を実行して整形しています)

フロントエンドだけで動作が完結するものに関してはコピー&ペーストだけで実装終了です

この状態でアプリケーションを起動します

mix phx.server

http://localhost:4000 にアクセスすると、以下の画面が表示されます(ライトモードの表示)

スクリーンショット 2024-12-25 11.23.39.png

その他、アコーディオンなど、様々なコンポーネントが Flowbite 公式ドキュメントに掲載されています

コンポーネント化

このままだと扱いにくいので、 Phoenix LiveView のコンポーネントにしてしまいましょう

以下の内容で lib/phoenix_flowbite_sample_web/components/flowbite_components.ex を作成します

defmodule PhoenixFlowbiteSampleWeb.FlowbiteComponents do
  use Phoenix.Component
  use Gettext, backend: PhoenixFlowbiteSampleWeb.Gettext

  attr :id, :string, required: true
  attr :type, :atom, default: :info, values: [:info, :success, :warning, :danger, :dark]
  slot :inner_block, required: true

  def alert(assigns) do
    ~H"""
    <div
      id={@id}
      class={[
        "flex items-center p-4 mb-4 text-sm rounded-lg dark:bg-gray-800",
        case @type do
          :info -> "text-blue-800 bg-blue-50 dark:text-blue-400"
          :success -> "text-green-800 bg-green-50 dark:text-green-400"
          :warning -> "text-yellow-800 bg-yellow-50 dark:text-yellow-400"
          :danger -> "text-red-800 bg-red-50 dark:text-red-400"
          :dark -> "text-gray-800 bg-gray-50 dark:text-gray-400"
        end
      ]}
      role="alert"
    >
      {render_slot(@inner_block)}
    </div>
    """
  end

  def info_icon(assigns) do
    ~H"""
    <svg
      class="flex-shrink-0 inline w-4 h-4 me-3"
      aria-hidden="true"
      xmlns="http://www.w3.org/2000/svg"
      fill="currentColor"
      viewBox="0 0 20 20"
    >
      <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z" />
    </svg>
    """
  end
end

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

defmodule PhoenixFlowbiteSampleWeb do
  ...
  defp html_helpers do
    quote do
      # Translation
      use Gettext, backend: PhoenixFlowbiteSampleWeb.Gettext

      # HTML escaping functionality
      import Phoenix.HTML
      # Core UI components
      import PhoenixFlowbiteSampleWeb.CoreComponents
+     import PhoenixFlowbiteSampleWeb.FlowbiteComponents

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

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

これにより、 lib/phoenix_flowbite_sample_web/controllers/page_html/home.html.heex は以下のように書き換えられます

<.flash_group flash={@flash} />
<div class="px-20 py-10 sm:px-20 sm:py-28 lg:px-20 xl:px-60 xl:py-32">
  <div id="alerts">
    <.alert id="info-alert" type={:info}>
      <.info_icon />
      <span class="sr-only">Info</span>
      <div>
        <span class="font-medium">Info alert!</span>
        Change a few things up and try submitting again.
      </div>
    </.alert>
    <.alert id="danger-alert" type={:danger}>
      <.info_icon />
      <span class="sr-only">Danger</span>
      <div>
        <span class="font-medium">Danger alert!</span>
        Change a few things up and try submitting again.
      </div>
    </.alert>
    <.alert id="success-alert" type={:success}>
      <.info_icon />
      <span class="sr-only">Success</span>
      <div>
        <span class="font-medium">Success alert!</span>
        Change a few things up and try submitting again.
      </div>
    </.alert>
    <.alert id="yellow-alert" type={:warning}>
      <.info_icon />
      <span class="sr-only">Warning</span>
      <div>
        <span class="font-medium">Warning alert!</span>
        Change a few things up and try submitting again.
      </div>
    </.alert>
    <.alert id="gray-alert" type={:dark}>
      <.info_icon />
      <span class="sr-only">Dark</span>
      <div>
        <span class="font-medium">Dark alert!</span>
        Change a few things up and try submitting again.
      </div>
    </.alert>
  </div>
</div>

かなりスッキリしましたね

コンポーネント化する場合、 CoreComponents で定義されているコンポーネントとの衝突(同じ名前の関数)に気をつけましょう

Flowbite Datepicker の導入

Datepicker は JavaScript 実装の都合上、フックを利用する必要があります

assets/js/app.js を以下のように編集します

...
import Datepicker from "flowbite-datepicker/Datepicker"

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")

Hooks = {}

Hooks.Datepicker = {
    mounted() {
        const datepickerEl = this.el;
        new Datepicker(datepickerEl, {
            // options
        });
    },
    updated() {
        this.mounted();
    }
}

let liveSocket = new LiveSocket("/live", Socket, {
  longPollFallbackMs: 2500,
  params: {_csrf_token: csrfToken},
  hooks: Hooks
})
...

Datepicker をインポートし、 Datepicker のフックを持つ DOM 要素が追加されたときに Datepicker が動くようにしています

lib/phoenix_flowbite_sample_web/controllers/page_html/home.html.heex について、以下のように phx-hook="Datepicker" を持つ input 要素を追加してみます

<.flash_group flash={@flash} />
<div class="px-20 py-10 sm:px-20 sm:py-28 lg:px-20 xl:px-60 xl:py-32">
  <input
    phx-hook="Datepicker"
    id="myInput"
    type="text"
    class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full pl-10 p-2.5 mb-4 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
    placeholder="Select date"
  />
  ...

この状態でアプリケーションを起動すると、 DatePicker で日付選択ができます

datepicker.gif

まとめ

Flowbite を Phoenix LiveView アプリケーションに導入できました

Pines や SaladUI など、コンポーネントライブラリーは色々あるので、デザインが良いもの、便利なものなどを選びましょう

6
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?