Elixir Phoenix 1.7 breadcrumb component
Tags: elixir, phoenix, tailwindcss, breadcrumb
Published: 2023-08-12 19:39:15

I like the [breadcrumb] navigation in a web app so I wanted to make one what works in [Phoenix LiveView][Phoenix] apps.

[Phoenix]: https://www.phoenixframework.org/
[breadcrumb]: https://en.wikipedia.org/wiki/Breadcrumb_navigation
[Bootstrap breadcrumb]: https://getbootstrap.com/docs/5.3/components/breadcrumb

## what breadcrumb navigation is

I think I first learned about the [breadcrumb] from [Bootstrap][Bootstrap breadcrumb] a long time ago, but I do not know the exact definition of it. Let's wikipedia.

> A breadcrumb or breadcrumb trail is a graphical control element used as a navigational aid in user interfaces and on web pages. It allows users to keep track and maintain awareness of their locations within programs, documents, or websites.

https://en.wikipedia.org/wiki/Breadcrumb_navigation

## my custom breadcrumb component

I do not need anything smart. Simple is best. My breadcrumb component simply accepts a list of navigation items. It might be used like below.

```elixir
<.breadcrumb items={[
  %{text: "Home", navigate: ~p"/"},
  %{text: "Examples", navigate: ~p"/examples"},
  %{text: "Light"}
]} />
```

![liveview-breadcrumb 2023-08-11 at 22.40.14.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82804/f60fd652-e1b8-bee8-cd30-dfe512af071e.gif)

By the way, the LiveView pages in the screen recording above are from [Pragmatic Studio Phoenix LiveView Course]. The online course is amazing. I added my custom [breadcrumb] component onto them for my extra learning.

[Pragmatic Studio Phoenix LiveView Course]: https://pragmaticstudio.com/courses/phoenix-liveview

## HTML and Tailwind classes

[Phoenix] 1.7 comes with [Tailwind CSS] by default. [Tailwind CSS] just works without any configuration changes.

First of all, I did the Internet search for an example [HTML] snippet with [Tailwind CSS classes] because my primary focus is to enjoy programming with [Elixir] and [Phoenix], not [HTML], not [Tailwind CSS].

I started by using this [example snippet from Flowbite](https://flowbite.com/docs/components/breadcrumb).

https://flowbite.com/docs/components/breadcrumb


[Elixir]: https://elixir-lang.org
[HTML]: https://developer.mozilla.org/en-US/docs/Learn/Getting_started_with_the_web/HTML_basics
[Tailwind CSS]: https://tailwindcss.com
[Tailwind CSS classes]: https://tailwind.build/classes
[Using Tailwind CSS in Phoenix 1.7]: https://pragmaticstudio.com/tutorials/using-tailwind-css-in-phoenix

## Phoenix.Component.link/1

For links, I took advangate of [Phoenix.Component.link/1] and its `:navaigate` attribute so I can navigate between LiveView pages smoothely.

https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#link/1

[Phoenix.Component.link/1]: https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#link/1

## icons

[Phoenix] 1.7 ships with [SVG] icons from [heroicons] and those [SVG] icons can be used easily via [MyAppWeb.CoreComponents.icon] component.

Since all the functions in `MyAppWeb.CoreComponents` module are [imported by default](
https://github.com/phoenixframework/phoenix/blob/e095f23ff62efb2f8c1205b0cc67bfacd80b11ed/installer/templates/phx_single/lib/app_name_web.ex#L87), the module name can be omitted when we use the icon component.

Thanks to the icon component, we no longer need to wrap our head around in terms of setting up icons, as long as we are happy with what [heroicons] library provides. It is very nice.

[SVG]: https://en.wikipedia.org/wiki/SVG
[heroicons]: https://heroicons.com
[MyAppWeb.CoreComponents.icon]: https://github.com/phoenixframework/phoenix/blob/e095f23ff62efb2f8c1205b0cc67bfacd80b11ed/installer/templates/phx_web/components/core_components.ex#L569-L594

## building breadcrumb component

I created a new module named `MyAppWeb.Breadcrumb` that is dedicated for the breadcrumb [function component][Phoenix.Component]. Alternatively it could have been in `MyAppWeb.CoreComponents` along with the icon component but I chose to create a new module so that I can focus on the breadcrumb concern. I might change my mind in the future.

[Phoenix.Component]: https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html

Here is the entirety of the module I created.

```elixir
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
```

### only one public function

The module has only one public function called `breadcrumb`, which is a function component. For readability, I factored out a sub-component `breadcrumb_item` that maps provided info to the breadcrumb item markup according to the breadcrumb item type.

### three types of breadcrumb items

Typically the breadcrumb navigation has three types of items and they have different looks and behaviors.

1. the leftmost
    - the home
    - link enabled
    - can look special with home icon
2. the rightmost
    - the current
    - link disabled
3. the in-between
    - all other paths between the home and the current
    - link enabled

For each type, the sub-component `breadcrumb_item` renders differently. We can easily determine the appropriate breadcrumb item type by the index and length of the list. The private function `index_to_item_type` does the job.

We need one preparation before rendering the component. The length of the list varies so we need to find out the list length beforehand.

[Enum.with_index/2] gives an index to each element of the list. With that index and the pre-calculated list length, we can determine the item type.

[Enum.with_index/2]: https://hexdocs.pm/elixir/Enum.html#with_index/2