ウエブアプリのパンくずリストが好きなので、Elixir Phoenixアプリでも使えるようにコンポーネントを作ってみました。
目標
あまり複雑にしたくないので、あまりスマートなことはやらずに明示的にリストを渡す方針にしました。というかシンプルなコードしか思いつきません。
以下のようなインターフェイスを目指します。
<.breadcrumb items={[
  %{text: "Home", navigate: ~p"/"},
  %{text: "Examples", navigate: ~p"/examples"},
  %{text: "Light"}
]} />
画像のサンプルアプリはPragmatic Studio Phoenix LiveView Courseについてきたものです。それにパンくずリストをつけて遊びました。
HTMLとTailwind
Phoenix 1.7にはデフォルトでTailwind CSSが付属しています。 特に設定を変更しなくてもTailwind CSSが機能するようになっています。
まず最初に、Tailwind CSSを使用したHTMLの例をインターネットで検索しました。私の焦点は、HTMLやTailwind CSSではなく、ElixirとPhoenixを使用してプログラミングを楽しむことですので、あえてTailwind CSSの深掘りはしません。
このサイトの例をベースにすることにしました。
Phoenix.Component.link/1
リンクについては、Phoenix.Component.link/1コンポーネントとその:naviigate属性を利用して、LiveViewページ間をスムーズに移動できるようにしました。
アイコン
アイコンはパンくずリストとは直接関係ないのですが、場合により使用したくなるかもしれません。Phoenix1.7にはheroiconsのSVGが同梱されており、これらのSVGアイコンはMyAppWeb.CoreComponents.iconコンポーネントとして簡単に利用できます。
MyAppWeb.CoreComponentsモジュール内のすべてのコンポーネントはPhoenix 1.7の初期設定でインポートされていますので、コンポーネントのモジュール名は省略可能です。
.iconコンポーネントのおかげでheroiconsライブラリが提供するものに満足している限り、アイコンの設定について頭を悩ませる必要はなくなりました。
パンくずリストの関数コンポーネント
パンくずリストの関数コンポーネント専用の MyAppWeb.Breadcrumbという名前の新しいモジュールを作成しました。
MyAppWeb.CoreComponentsモジュールに追加する手もありますが、ここではパンくずリストの問題に集中できるように、新しいモジュールを作成することにしました。
これが私が作成したモジュールの全体像です。
defmodule MyAppWeb.Breadcrumb do
  use Phoenix.Component
  import MyAppWeb.CoreComponents
  attr :items, :list, required: true
  def breadcrumb(assigns) do
    assigns = assign(assigns, :size, length(assigns.items))
    ~H"""
    <nav class="flex" aria-label="breadcrumb">
      <ol class="inline-flex items-center space-x-1 md:space-x-3">
        <.breadcrumb_item
          :for={{item, index} <- Enum.with_index(@items)}
          type={index_to_item_type(index, @size)}
          navigate={item[:navigate]}
          text={item[:text]}
        />
      </ol>
    </nav>
    """
  end
  defp index_to_item_type(0, _size), do: "first"
  defp index_to_item_type(index, size) when index == size - 1, do: "last"
  defp index_to_item_type(_index, _size), do: "middle"
  attr :type, :string, default: "middle"
  attr :navigate, :string, default: "/"
  attr :text, :string, required: true
  defp breadcrumb_item(assigns) when assigns.type == "first" do
    ~H"""
    <li class="inline-flex items-center">
      <.link navigate={@navigate} class="inline-flex items-center text-sm font-medium">
        <.icon name="hero-home" class="h-4 w-4" />
      </.link>
    </li>
    """
  end
  defp breadcrumb_item(assigns) when assigns.type == "last" do
    ~H"""
    <li aria-current="page">
      <div class="flex items-center">
        <.icon name="hero-chevron-right" class="h-4 w-4" />
        <span class="ml-1 text-sm font-medium md:ml-2">
          <%= @text %>
        </span>
      </div>
    </li>
    """
  end
  defp breadcrumb_item(assigns) do
    ~H"""
    <li>
      <div class="flex items-center">
        <.icon name="hero-chevron-right" class="h-4 w-4" />
        <.link navigate={@navigate} class="ml-1 text-sm font-medium md:ml-2 ">
          <%= @text %>
        </.link>
      </div>
    </li>
    """
  end
end
breadcrumb/1
breadcrumb/1関数を唯一のパブリック関数としました。 これはパンくずリスト全体を取りまとめるコンポーネントです。コード読みやすくするために、パンくずリストアイテムは、breadcrumb_item/1という別のプライベートなコンポーネントに分割しました。breadcrumb_item/1はアサインされたアイテムタイプ(assigns.type)に応じて挙動を切り替えるようにしています。
breadcrumb_item/1
単純化すると、パンくずリストアイテムには3種類ある考えられ、それぞれ外観と動作が異なるようにする必要があります。
- 一番左(起点)
- ホームのパス
- リンクしたい
 
- 一番右(終点)
- 現在のパス
- リンク不要
 
- 間にある項目
- 通過点のパス
- リンクしたい
 
タイプごとにレンダリングするマークアップを切り替えます。
index_to_item_type/2
各アイテムのタイプについては、リストのインデックスと長さによって簡単に決定できます。リストの長さはさまざまなので、事前にリストの長さを調べておく必要があります。
Enum.with_index/2
Enum.with_index/2は、リストの各要素にインデックスを与えます。 項目タイプを決定する時にそのインデックスと事前に計算されたリストの長さを利用します。

