2
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?

はじめに

この記事はElixirアドベントカレンダー2025のシリーズ2、4日目の記事です

本記事ではPhoenixが1.7系から1.8系に変わる際にコンポーネント周りに大きな変更が入ったためそれを解説したいと思います

UIコンポーネントライブラリについて

コンポーネントを作成する前にUIコンポーネントのライブラリについて解説します。PhoenixではデフォルトでCSSフレームワークにTailwind、1.8からはさらにUIフレームワークにDaisyUIが採用されています。

DaisyUITailwind CSSだけで実装された、豊富なコンポーネントとテーマを提供するUIライブラリです。

DaisyUIはデフォルトとして設定されていますが、各種ジェネレータには依存しておらず他のものを使用したい場合は、app.cssからpluginの記述を書き換えることで簡単に変更できます。

ですが他のUIコンポーネントの選定は気をつけることがあって、「Tailwind UIコンポーネント」で検索するとReactVueSveltなどJavaScriptのフレームワークの上で動かすものが多くヒットします。

LiveViewで使う場合、Tailwind単体で動いて、UI制御にJavaScriptを使用していないもの、またはLiveViewに対応している物を選ぶ必要があります。UI制御にJavaScriptを使用していないものを選ぶのは、LiveViewでUI制御をするときに干渉してしまうためです。

Tailwind単体で動いて、JavaScriptを使用していないものは2種類あります。

  1. Tailwindのユーティリティクラスだけで作成した実装サンプル集
  2. applyでcssのクラス的にまとめたコンポーネント集

大半は1になります。このUIを実装するにはどうするのかがわかるので、参考にするのにも大変良いです。

例として次のようなものがあります。

CoreComponentの解説

phx.gen.liveによって生成された画面で使用されている、CoreComponentについて解説します。

CoreComponentPhoenixにビルドインされたTailwindDaisyUILiveView Phoniex Componentで実装されたデフォルトコンポーネント群は以下のようなコンポーネントがあります。

  • flash:保存の成功、エラー発生の際右上に表示されるFlash
  • button:各丸ボタン
  • input:各種各丸input、正規化やエラーメッセージ表示も対応
  • errorinput等で使うエラーメッセージ
  • header:タイトル、サブタイトル、アクション含むデザインヘッダー
  • tableLiveStream対応したデザイン調整されたテーブル
  • list:構造体やMapの情報を一覧する
  • iconHeroiconを表示するコンポーネント

こちらも1.8になってmodalやsimple_form,backと大きく削られ、デザイン部分もDaisyUIを使用してコード量と複雑さを極端に減らしています。

attr と slot

CoreComponentの元になっているPhoenix.Componentattrslotという値を定義できます。

attrはパラメーターとして渡せる値を定義でき、slotcomponentのタグで挟んだ内容をどの場所で展開するかを定義できます。

実際にbuttonコンポーネントの実装を見てみましょう。

basic_web/components/core_components.ex:L82
@doc """
  Renders a button with navigation support.

  ## Examples

      <.button>Send!</.button>
      <.button phx-click="go" variant="primary">Send!</.button>
      <.button navigate={~p"/"}>Home</.button>
  """
  attr :rest, :global, include: ~w(href navigate patch method download name value disabled)
  attr :class, :string
  attr :variant, :string, values: ~w(primary)
  slot :inner_block, required: true

  def button(%{rest: rest} = assigns) do
    variants = %{"primary" => "btn-primary", nil => "btn-primary btn-soft"}

    assigns =
      assign_new(assigns, :class, fn ->
        ["btn", Map.fetch!(variants, assigns[:variant])]
      end)

    if rest[:href] || rest[:navigate] || rest[:patch] do
      ~H"""
      <.link class={@class} {@rest}>
        {render_slot(@inner_block)}
      </.link>
      """
    else
      ~H"""
      <button class={@class} {@rest}>
        {render_slot(@inner_block)}
      </button>
      """
    end
  end

Examplesに使い方が載っています。attrvariantprimaryを指定し、slotSend!が指定されています。

attrvariantbuttonタグのclassbtn-primaryとして展開されています。またslotの中身を展開するときはrender_slotを使用します。

restで色々はhtml属性をセットできてページ推移に関するものだったらbuttonでは無く、linkコンポーネントにするなど細かい気配りもしてくれています。

core_component カスタム

カスタムする場合はcore_components.exに全てあるので、対象のコンポーネントを検索してclassを値を変更することで簡単に調整ができます。

buttonのvariantにprimaryだけではなく、secondary, success, errorも設定する場合は以下のようになります

  def button(%{rest: rest} = assigns) do
    variants = %{
        "primary" => "btn-primary", 
        "secondary" => "btn-secondary",
        "success" => "btn-success",
        "error" => "btn-error",
        nil => ""
    }

    assigns =
      assign_new(assigns, :class, fn ->
        ["btn", Map.fetch!(variants, assigns[:variant])]
      end)

    if rest[:href] || rest[:navigate] || rest[:patch] do
      ~H"""
      <.link class={@class} {@rest}>
        {render_slot(@inner_block)}
      </.link>
      """
    else
      ~H"""
      <button class={@class} {@rest}>
        {render_slot(@inner_block)}
      </button>
      """
    end
  end

レイアウトコンポーネント

1.7まではページのレイアウトhtmlファイルがあり、用途に応じてput_layoutをする必要があり、その際もroot_layoutをfalseにして使わないようにしてなど色々面倒なことが多かったのですが、1.8からはレイアウトもコンポーネント化されたので

以下のようにLayouts.appコンポーネントで囲うことでhtmlやhead、script、metaだけ共通のrootとして読み込んで、body以下のheader,fotterや画面サイズ等を柔軟に切り替えれるようになりました

<Layouts.app>
  <h1>Hello Phoenix</h1>
</Layouts.app>

モーダル

core_componentからmodal消えちゃったどうしよう?!と焦るかもしれませんが
そもそもモーダルを多用するなモードレスにしろと言う風潮が最近は強いです

それもで削除とか取り返しのつかないことは、まぁ使うことはあります

その場合はDiasyUIのdaialogをそのまま使えばよいです

data-confirmのところを以下を参考に書き換えます

<:action :let={{id, shelf}}>
  <.link
    phx-click={JS.push("delete", value: %{id: shelf.id}) |> hide("##{id}")}
    data-confirm="Are you sure?"
  >
    Delete
  </.link>
</:action>
<:action :let={{id, shelf}}>
  <.button onclick={"s#{shelf.id}.showModal()"}>
    Delete
  </.button>
  <dialog id={"s#{shelf.id}"} class="modal">
    <div class="modal-box">
      <p class="py-4">Are you sure?</p>
      <div class="modal-action">
        <.button
          phx-click={JS.push("delete", value: %{id: shelf.id}) |> hide("##{id}")}
          class="btn btn-error text-white"
        >
          delete
        </.button>
        <form method="dialog">
          <button class="btn">cancel</button>
        </form>
      </div>
    </div>
  </dialog>
</:action>

こんな感じで動きます

6d2d7a8361cdecd21c09d9c9baaa0bca.gif

最後に

1.8になってデフォルトのUIコンポーネントがDaisyUIになったり、CoreComponentもスッキリしたり、Layoutコンポーネントができたりで画面の構築がすごく楽になりました。

コンポーネント以外にも1.8で便利になったことがたくさんあるのでそちらも紹介できたらなと思います

本記事は以上になりますありがとうございました

2
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
2
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?