One of the most important challenges when we are 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 we as developers are faced with the challenge of showing the user in each interaction with the website the place where they are in a clear and intuitive way.
Today I’ll share with you a recipe on how we can handle navigation within our Liveview applications and how we can offer the user a navigation bar that shows the active tab in which the user is located.
Defining the sidebar markup
First we’ll define the markup of the application sidebar within our live layout in live.html.heex
, in this way it will be part of the life cycle 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
(in a few moments I’ll describe in detail where they were assigned) and, on the other hand, we have a function component called sidebar_nav_links
, to which we are passing as parameters the current_user
and active_tab
assigns and is 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 that contains two links. The first with the text My songs and another with the text Settings, which would look as follows:
In addition, we have a css class in each of the links that we defined, depending on the content of the assign @active_tab
, the css class bg-gray-200
or the hover:bg-gray-50
class will be applied:
"#{if @active_tab == :profile, do: "bg-gray-200", else: "hover:bg-gray-50"}"
In this way, the user will be able to perceive in which part of the navigation options he is (in this example, within My songs page):
Setting the @active_tab assign
So far we have defined a list of active_tabs
to which a conditional css class will be assigned in such a way that the location of the user within our application is perceived, however, we don’t know how the active_tab
and current_user
assigns were assigned. We also need this assign to be available in all LiveViews since it is rendered in the live layout. Fortunately, 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 can see that a live_session
is defined as :authenticated
, that session will include a list of routes that will go through a user validation process and will show content if the user is valid and authenticated in our application, otherwise user will be redirected to the log-in page.
Later (lines 2 and 3), we attached a couple of hooks to the “mount” life cycle of each of the LiveViews defined in the session. The live_session
macro allows us to define a group of routes with shared life-cycle hooks, and live navigate between them.
The first will be in charge of performing the user validation process by invoking the on_mount
callback of the LiveBeatsWeb.UserAuth
module with the parameter :ensure_authenticated
.
Lets see the callback definition in LiveBeatsWeb.UserAuth
:
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
As we can see, in line 4 we are assigning the value of current_user
to the socket assigns in case of getting the current user of the session and get a result from the database, otherwise, we redirect the user to the log-in view.
The second hook is in charge of setting the assigns we need in order to show the active tab in the sidebar.
Let’s see the content of the LiveBeatsWeb.Nav
module:
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
First, in line 4 we are defining the on_mount
callback that will be used as :default
if no other option is sent in the invocation of the hook within router.ex
The most 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
life cycle and we passed our set_active_tab/3
function to be invoked for this stage.
Within set_active_tab/3
, we implemented logic to set the @active_tab
based on the params, LiveView module, and live action from the router. When viewing a user’s profile, we only set the active tab to :profile
if the current user is viewing their own profile. Likewise, we set the active tab to :settings
if routed to the SettingsLive
LiveView. Otherwise, @active_tab
is set to nil.
Now when the user navigations across LiveViews, the live layout will reactively update as the URL changes, and our tabs will be highlighted appropriately.
Let’s see it in action!