How to build breadcrumbs for your Phoenix app

Illustration by Lena Volkova (https://www.instagram.com/_lena_volkova_)

Breadcrumbs is very useful element which allows users to keep track of their locations within site.

That’s not so hard to implement it. So let’s do it together!

Firstly we need to define new view helper under the web/views/ folder and import some useful stuff.


defmodule Spark.Web.BreadcrumbsHelper do
use Phoenix.HTML
import Spark.Web.Gettext
import SparkWeb.Router.Helpers
end

Because we need to build html, we should import Phoenix.HTML module. In case if you are wondering what’s the difference between use and import, you could read my previous article. Spark.Web.Gettext is needed only in case you need internalization. Because we are planning to build links we also need to import SparkWeb.Router.Helpers module.

Now I am planning to implement public function called breadcrumbs. And then call it from the template like so:

<%= breadcrumbs [conn, :users] %>
<%= breadcrumbs [conn, :collections, param1] %>
<%= breadcrumbs [conn, :edit_collection, param1, param2] %>

Firstly we need to import our helper inside the def view section in the web/web.ex file because we want our breadcrumbs available in all views.

defmodule Spark.Web do
...
  def view do
quote do
...
      import Spark.Web.BreadcrumbsHelpers
end
end
end

Now let’s start with implementation!

def breadcrumbs(args) do
crumbs(args)
|> render
end

The key idea is to define crumb functions. Each of them should return list with two-element tuples: {breadcrumb_name, link}
Imagine our app has users which many collections. For the new collection page breadcrumbs should look like this: Home / Users / Mike / Collections / New Collection.

Now let’s implement crumbs functions starting with Home.

def crumbs(conn, :root) do 
[{gettext("Home"), page_path(conn, :index)}]
end

Okay, first one is completed! Now we will proceed with some functions breadcrumbs for user navigation. I will create two of them: first one for user’s index page and second one for show.

def crumbs(conn, :users) do
[{gettext("Users"), user_path(conn, :index)} | crumbs(conn, :root)]
end
def crumbs(conn, :user, %User{} = user) do
[{user.name, user_path(conn, :show, user)} | crumbs(conn, :users)]
end

crumbs(conn, :users) returns new list with two tuples. First element is data for current breadcrumb, second is a result of function call for parent breadcrumbs. Because prepending an element to a list is faster than appending.

Next step is to add functions for collections.

def crumbs(conn, :collections, %User{} = user) do
[{gettext("Collections"), user_collection_path(conn, :index, user)} | crumbs(conn, :user, user)]
end

def crumbs(conn, :collection, %User{} = user, %Collection{} = collection) do
[{collection.name, user_collection_path(conn, :show, user, collection)} | crumbs(conn, :collections, user)]
end
def crumbs(conn, :new_collection, %User{} = user) do
[{gettext("New Collection"), user_collection_path(conn, :new, user)} | crumbs(conn, :collections, user)]
end
def crumbs(conn, :edit_collection, %User{} = user, %Collection{} = collection) do
[{gettext("Edit"), user_collection_path(conn, :edit, user, collection)} | crumbs(conn, :collection, user, collection)]
end

crumbs(conn, :edit_collection, user, collection) will transform into:

[{"Edit", "/users/1/collections/1/edit"}, {"Sunny spark", "/users/1/collections/1"}, {"Collections", "/users/1/collections"}, {"Mike", "/users/1"}, {"Users", "/users"}, {"Home", "/"}]

Now we have all functions, that returns necessary data for rendering our navigation helper. I propose move forward and implement rendering mechanism.

defp render([current | tail]) do
([render_crumb(current, :current)] ++ Enum.map(tail, &render_crumb&1))
|> Enum.reverse
end
defp render_crumb({text, _path}, :current) do
content_tag :li, do: text
end
defp render_crumb({text, path}) do
content_tag :li, do: link(text, to: path)
end

render returns reversed list of links to parent pages and text list item for current page.

Now if we would try to call our helper <%= breadcrumbs [conn, :users] %> from the template, we would get an error. That’s because crumbs accepts some args, but not one arg with list type.

def breadcrumbs(args) do
apply(__MODULE__, :crumbs, args)
|> render
end

apply invokes :crums function from the current module with args. Now this would work, but we also need to add some markup to finish this feature.

def breadcrumbs(args) do
content_tag :div, class: "row columns" do
content_tag :nav, role: "navigation" do
content_tag :ul, class: "breadcrumbs" do
apply(__MODULE__, :crumbs, args)
|> render
end
end
end
end

We are done! We can use our helpers from templates:

<%= breadcrumbs [@conn, :collections, @user] %>
<%= breadcrumbs [@conn, :collection, @user, @collection] %>
<%= breadcrumbs [@conn, :new_collection, @user] %>

Here is full breadcrumbs helper file:

defmodule Spark.Web.BreadcrumbsHelpers do
use Phoenix.HTML
import Spark.Web.Gettext
import SparkWeb.Router.Helpers
  alias Spark.Accounts.User
alias Spark.Gallery.Collection
  def breadcrumbs(args) do
content_tag :div, class: "row columns" do
content_tag :nav, role: "navigation" do
content_tag :ul, class: "breadcrumbs" do
apply(__MODULE__, :crumbs, args)
|> render
end
end
end
end
  defp render([current | tail]) do
([render_crumb(current, :current)] ++ Enum.map(tail, &render_crumb&1))
|> Enum.reverse
end
  defp render_crumb({text, _path}, :current) do
content_tag :li, do: text
end
defp render_crumb({text, path}) do
content_tag :li, do: link(text, to: path)
end
  def crumbs(conn, :root) do 
[{gettext("Home"), page_path(conn, :index)}]
end
  def crumbs(conn, :users) do
[{gettext("Brands"), user_path(conn, :index)} | crumbs(conn, :root)]
end
def crumbs(conn, :user, %User{} = user) do
[{user.name, user_path(conn, :show, user)} | crumbs(conn, :users)]
end
  def crumbs(conn, :collections, %User{} = user) do
[{gettext("Collections"), user_collection_path(conn, :index, user)} | crumbs(conn, :user, user)]
end
def crumbs(conn, :collection, %User{} = user, %Collection{} = collection) do
[{collection.name, user_collection_path(conn, :show, user, collection)} | crumbs(conn, :collections, user)]
end
def crumbs(conn, :new_collection, %User{} = user) do
[{gettext("New Collection"), user_collection_path(conn, :new, user)} | crumbs(conn, :collections, user)]
end
def crumbs(conn, :edit_collection, %User{} = user, %Collection{} = collection) do
[{gettext("Edit"), user_collection_path(conn, :edit, user, collection)} | crumbs(conn, :collection, user, collection)]
end
end

That’s all for today! 
Thank you for reading my post!