Active nav with LiveView · The Phoenix Files
Berenice Medel
One of the most important challenges when developing a new website is to give the user a great navigation experience. The user must know where they are and what navigation options they have at their disposal within the website. For this, we use navigation components such as navbars or sidebars, and developers face the challenge of showing the user their current position in a clear and intuitive way.
Today I’ll share a recipe for handling navigation within our Liveview applications, and how we can offer a navigation bar that shows the active tab the user is currently on.
Defining the sidebar markup
First, we’ll define the markup of the application sidebar within our live layout in live.html.heex, which will be part of the lifecycle of our LiveView components.
<%= if @current_user do %>
<.sidebar_nav current_user={@current_user} active_tab={@active_tab}/>
<% end %>
Let’s take a closer look at the previous code; there are two assigns defined, @active_tab and @current_user. We are passing these as parameters to a function component called sidebar_nav_links, defined in the LiveBeatsWeb.LayoutView module as follows:
def sidebar_nav(assigns) do
~H"""
<nav class="px-3 mt-6 space-y-1">
<%= if @current_user do %>
<.link
navigate={profile_path(@current_user)}
class={"#{if @active_tab == :profile, do: "bg-gray-200", else: "hover:bg-gray-50"}"}>
<.icon name={:music_note} outlined />
My Songs
</.link>
<.link
navigate={Routes.settings_path(Endpoint, :edit)}
class={"#{if @active_tab == :settings, do: "bg-gray-200", else: "hover:bg-gray-50"}"}>
<.icon name={:adjustments} outlined/>
Settings
</.link>
<% end %>
</nav>
"""
end
We are using the sigil_H that returns a rendered structure containing two links: one for My Songs and another for Settings. Each link has a CSS class defined depending on the value of @active_tab.
"#{if @active_tab == :profile, do: "bg-gray-200", else: "hover:bg-gray-50"}"
This way, the user perceives their current location within our navigation options.
Setting the @active_tab assign
We have defined a list of active_tabs to assign a conditional CSS class to indicate the user's location within our application. Now we need to see how the active_tab and current_user assigns are determined. We also need this assign available in all LiveViews since it is rendered in the live layout. Luckily, LiveView lifecycle hooks make this easy.
Let’s take a look at the following code inside router.ex:
live_session :authenticated, on_mount: [
{LiveBeatsWeb.UserAuth, :ensure_authenticated},
LiveBeatsWeb.Nav
] do
live "/:profile_username/songs/new", ProfileLive, :new
live "/:profile_username", ProfileLive, :index
live "/profile/settings", SettingsLive, :edit
end
We see that a live_session is defined as :authenticated, which includes a user validation process. If the user is valid, the content will be shown; otherwise, they will be redirected to the login page.
The first hook handles user validation through the on_mount callback of the LiveBeatsWeb.UserAuth module:
def on_mount(:ensure_authenticated, _params, session, socket) do
case session do
%{"user_id" => user_id} ->
new_socket = LiveView.assign_new(socket, :current_user, fn ->
Accounts.get_user!(user_id)
end)
{:cont, new_socket}
%{} ->
{:halt, redirect_require_login(socket)}
end
rescue
Ecto.NoResultsError -> {:halt, redirect_require_login(socket)}
end
In line 4, current_user is assigned to the socket assigns if the current user is found. If not, they are redirected to the log-in view.
The second hook sets the assigns needed to show the active tab in the sidebar.
defmodule LiveBeatsWeb.Nav do
import Phoenix.LiveView
def on_mount(:default, _params, _session, socket) do
{:cont,
socket
|> attach_hook(:active_tab, :handle_params, &set_active_tab/3)}
end
defp set_active_tab(params, _url, socket) do
active_tab =
case {socket.view, socket.assigns.live_action} do
{ProfileLive, _} ->
if params["profile_username"] == current_user(socket) do
:profile
end
{SettingsLive, _} ->
:settings
{_, _} ->
nil
end
{:cont, assign(socket, active_tab: active_tab)}
end
defp current_user(socket) do
socket.assigns.current_user[:username]
end
end
In line 4, we define the on_mount callback that will be used as :default. The important part of this callback can be seen in line 7, where we call attach_hook/4. We named our hook :active_tab and attached it to the handle_params stage of the socket lifecycle, invoking our set_active_tab/3 function.
In set_active_tab/3, logic is implemented to set @active_tab based on parameters, the LiveView module, and live action from the router. This allows the user to navigate through LiveViews, with the tabs highlighting appropriately as the URL changes.