はじめに
この記事はElixirアドベントカレンダー2025のシリーズ2、4日目の記事です
本記事ではPhoenixが1.7系から1.8系に変わる際にコンポーネント周りに大きな変更が入ったためそれを解説したいと思います
UIコンポーネントライブラリについて
コンポーネントを作成する前にUIコンポーネントのライブラリについて解説します。PhoenixではデフォルトでCSSフレームワークにTailwind、1.8からはさらにUIフレームワークにDaisyUIが採用されています。
DaisyUIはTailwind CSSだけで実装された、豊富なコンポーネントとテーマを提供するUIライブラリです。
DaisyUIはデフォルトとして設定されていますが、各種ジェネレータには依存しておらず他のものを使用したい場合は、app.cssからpluginの記述を書き換えることで簡単に変更できます。
ですが他のUIコンポーネントの選定は気をつけることがあって、「Tailwind UIコンポーネント」で検索するとReactやVue、SveltなどJavaScriptのフレームワークの上で動かすものが多くヒットします。
LiveViewで使う場合、Tailwind単体で動いて、UI制御にJavaScriptを使用していないもの、またはLiveViewに対応している物を選ぶ必要があります。UI制御にJavaScriptを使用していないものを選ぶのは、LiveViewでUI制御をするときに干渉してしまうためです。
Tailwind単体で動いて、JavaScriptを使用していないものは2種類あります。
- Tailwindのユーティリティクラスだけで作成した実装サンプル集
- applyでcssのクラス的にまとめたコンポーネント集
大半は1になります。このUIを実装するにはどうするのかがわかるので、参考にするのにも大変良いです。
例として次のようなものがあります。
-
Sailboat UI{{ footnote 'https://sailboatui.com' }} -
HyperUI{{ footnote 'https://www.hyperui.dev' }} -
Tailwind Elements{{ footnote 'https://tw-elements.com/' }}
CoreComponentの解説
phx.gen.liveによって生成された画面で使用されている、CoreComponentについて解説します。
CoreComponentはPhoenixにビルドインされたTailwindとDaisyUI、 LiveView Phoniex Componentで実装されたデフォルトコンポーネント群は以下のようなコンポーネントがあります。
-
flash:保存の成功、エラー発生の際右上に表示されるFlash -
button:各丸ボタン -
input:各種各丸input、正規化やエラーメッセージ表示も対応 -
error:input等で使うエラーメッセージ -
header:タイトル、サブタイトル、アクション含むデザインヘッダー -
table:LiveStream対応したデザイン調整されたテーブル -
list:構造体やMapの情報を一覧する -
icon:Heroiconを表示するコンポーネント
こちらも1.8になってmodalやsimple_form,backと大きく削られ、デザイン部分もDaisyUIを使用してコード量と複雑さを極端に減らしています。
attr と slot
CoreComponentの元になっているPhoenix.Componentはattrとslotという値を定義できます。
attrはパラメーターとして渡せる値を定義でき、slotはcomponentのタグで挟んだ内容をどの場所で展開するかを定義できます。
実際にbuttonコンポーネントの実装を見てみましょう。
@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に使い方が載っています。attrのvariantにprimaryを指定し、slotに Send!が指定されています。
attrのvariantはbuttonタグのclassにbtn-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>
こんな感じで動きます
最後に
1.8になってデフォルトのUIコンポーネントがDaisyUIになったり、CoreComponentもスッキリしたり、Layoutコンポーネントができたりで画面の構築がすごく楽になりました。
コンポーネント以外にも1.8で便利になったことがたくさんあるのでそちらも紹介できたらなと思います
本記事は以上になりますありがとうございました
