はじめに
前回の記事で Pines を紹介しました
Pines はコピー&ペーストだけでアコーディオンやバナーなどの UI コンポーネントを実装できるライブラリーです
See the Pen Pines Accordion by Ryo Wakabayashi (@ryowakabayashi) on CodePen.
See the Pen Untitled by Ryo Wakabayashi (@ryowakabayashi) on CodePen.
See the Pen Untitled by Ryo Wakabayashi (@ryowakabayashi) on CodePen.
See the Pen Pines Copy to Clipboard by Ryo Wakabayashi (@ryowakabayashi) on CodePen.
Alpine.js や Tailwind CSS を利用しているため、 JS や CSS をほとんど書くことなく綺麗に動く UI が作れてしまいます
Alpine.js と Tailwind CSS でピンと来た方もいると思いますが、 Pines は Elixir の Web フレームワークである Phoeix と非常に相性が良いです
この記事では Phoenix への Pines コンポーネント導入方法を紹介します
なお、 Phoenix のバージョンは 1.7 を想定しています
実装したコードの全量はこちら
実行環境
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 プロジェクトを準備します
公式ドキュメントに従って作業していきます
Hex のインストール
Elixir のパッケージマネージャーである Hex をインストールします
mix local.hex
Phoenix のインストール
Phoenix をインストールします
mix archive.install hex phx_new
プロジェクトの作成
新しい Phoenix プロジェクトを作成します
mix phx.new pines_sample --no-ecto
今回の例ではデータベースを使わないので --no-ecto
を指定しています
実際のプロジェクトの場合は必要に応じて変更してください
しばらくすると Fetch and install dependencies? [Yn]
と尋ねられるのでそのまま Enter キーを押してください
Phoenix プロジェクトの雛形が作成され、デフォルトで必要なライブラリーが取得されます
作成したプロジェクトのディレトリー内に移動します
cd pines_sample
Alpine.js の導入
Phoenix 1.7 では Tailwind CSS がデフォルトで導入されているため、 Alpine.js だけ手動で導入します
まず、プロジェクト内の assets ディレクトリーに移動します
cd assets
そして、 JavaScript の依存ライブラリーとして Alpine.js を追加します
ついでにアニメーションを実装するための Collapse Plugin も追加しておきましょう
yarn add alpinejs @alpinejs/collapse
コマンドを実行すると "assets/package.json" が追加され、 "assets/node_modules" 内に依存ライブラリーがインストールされます
"assets/package.json" の中身は以下のようになります
{
"dependencies": {
"@alpinejs/collapse": "^3.12.3",
"alpinejs": "^3.12.3"
}
}
続いて "assets/js/app.js" に Alpine.js の読み込み処理を追加します
// 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)
+ }
+ }
+ }
+ })
...
作業が済んだらプロジェクトのルートに戻っておきましょう
cd ..
コンポーネントの追加
まずカードコンポーネントを作ってみましょう
コンポーネントファイルの作成
"lib/pines_sample_web/components/card.ex" に以下の内容のファイルを作ります
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 のテンプレートです
コンポーネント表示用画面の作成
コンポーネントを表示するため、 Phoenix LiveView による画面を作成しましょう
Phoenix LiveView では、xxx.ex (動作の定義)と同じ階層に xxx.html.heex (外観のテンプレート)を作成することで画面を実装できます
まず "lib/pines_sample_web/live" ディレクトリーを作成します
"lib/pines_sample_web/live/showcase.ex" を以下の内容で作成します
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" を以下の内容で作成します
<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" を以下のように編集します
...
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 を起動します
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 を開きます
枠の中の "Card" の部分がコンポーネントです
このコンポーネントを改造していきます
Pines のコピー&ペースト
Pines 公式ドキュメントの Card コンポーネントのページを開きます
コピーボタン(下画像の赤枠部分)をクリックし、コードをコピーします
"lib/pines_sample_web/components/card.ex" の H シギル内にペーストします
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 公式ドキュメントと全く同じカードが実装できています
引数の追加
このままだと固定値を表示するだけなので、変えたい部分を引数にします
更に Phoenix LiveView によるボタンクリック時のイベント phx-click
を追加します
また、単純に貼り付けたためにインデントなどが狂っているので修正します
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 は以下のような表示になります
"lib/pines_sample_web/live/showcase.html.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 は以下のような表示になります
また、ボタンをクリックすると 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" を以下の内容で作成します
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 公式ドキュメントからアコーディオンのコードをコピー&ペーストします
ただし、コードに含まれる :class=
の文言がエラーになるため、 :class=
を x-bind:class=
に一括置換します
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" を以下のように編集します
<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 にアコーディオンが追加できました
アコーディオンへの引数追加
アコーディオンも引数によって内容を制御できるようにしましょう
"lib/pines_sample_web/components/accordion.ex" を以下の内容に変更します
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" を以下のように編集します
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" を以下のように編集します
<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 を開くと、アコーディオンの内容が変化しました
ボタンの実装
ここまでと同じ容量でボタンコンポーネントを作りましょう
Pines 公式ドキュメントでボタンコンポーネントの上から2番目の色付きボタンを実装します
"lib/pines_sample_web/components/button.ex" を以下の内容で作成します
今回は最終的な引数追加まで含めた結果だけ以下に示しています
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
に応じて、背景色、ボーダー色、ホバー時の背景色、フォーカス時に出る枠線の色、文字色を切り替えるように実装しています
{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 による変更が反映されません)
assigns =
assigns
|> assign(:bg_color, bg_color)
|> assign(:border_color, border_color)
...
クラス名(Tailwind CSS によるスタイル)をソケット内の値で置き換える場合、以下のように class={"..."}
としてダブルクォーテーションの外を {}
で囲い、ソケットの各キーを #{}
で囲います
class={"... #{@color} ... #{@bg_color} ..."}
ボタンを画面に表示しましょう
"lib/pines_sample_web/live/showcase.ex" を以下のように編集します
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" を以下のように編集します
<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 に以下のようにボタンが追加されます
まとめ
Pines を使うことで、 Phoenix LiveView にも簡単にコンポーネントを追加できました
Pines 公式ドキュメントには他にも様々なコンポーネントが紹介されているので、そこからどんどんコピー&ペーストしましょう