Highlighting the current page link in the Navbar menu in Phoenix LiveView
A good user experience happens when an application is easy to use and with intuitive UI feedback. Nav
menu bar is one the most important element in web applications, it helps users to navigate to different pages. It's vital that users should be aware of which current tab on which user is. This can be achieved by highlighting the current tab user is. See the cover image of this blog to make sense.
NOTE: Jump to Highlighting the current menu (Dirty Solution) section if you already have a project with Navbar.
Let's start building
- First of all, we should have a LiveView project or will create a new LiveView project by running
mix phx.new navigation_demo --no-ecto --live
- We will then navigate to the project directory by running
cd navigation_demo
and runmix phx.server
. Once, the project starts running navigate tolocalhost:4000
to see theWelcome
phoenix screen.
Adding liveview pages
- To demonstrate the navbar highlighting logic in Elixir, we will first add three items to the navbar menu with minimal functionality.
- For this open
router.ex
and following routeslive "/", HomePageLive, :index live "/page1", Page1Live, :index live "/page2", Page2Live, :index
- Now go to live folder and add three files
home_page_live.ex
,page1_live.ex
, andpage2_live.ex
. Add
mount
andrender
callbacks in each of the following with minimal text. Refer following code for reference// home_page_live.ex defmodule NavigationDemoWeb.HomePageLive do use NavigationDemoWeb, :live_view def mount(_params, _session, socket) do {:ok, socket} end def render(assigns) do ~H""" Home page """ end end // page1_live.ex defmodule NavigationDemoWeb.Page1Live do use NavigationDemoWeb, :live_view def mount(_params, _session, socket) do {:ok, socket} end def render(assigns) do ~H""" Page 1 """ end end // page2_live.ex defmodule NavigationDemoWeb.Page2Live do use NavigationDemoWeb, :live_view def mount(_params, _session, socket) do {:ok, socket} end def render(assigns) do ~H""" Page 2 """ end end
- Adding of LiveView pages is done, now we will add a navigation menu with minimal styling.
Add Navbar menu
- Before adding the Navbar menu we will do some cleanup in our existing code.
- Open
templates/layout/root.html.heex
and remove all code in the body tag and just add the following code<body> <%= @inner_content %> </body>
- Open the
live.html.heex
file and add the following code<nav> <h3>NavBar Test</h3> <ul> <li><%= live_patch "Home", to: Routes.home_page_path(socket, :index)%></li> <li><%= live_patch "Page 1", to: Routes.page1_path(socket, :index)%></li> <li><%= live_patch "Page 2", to: Routes.page2_page_path(socket, :index)%></li> </ul> </nav>
Open
app.css
and add the following stylingnav { display: flex; justify-content:space-between; align-items: center; padding: 1rem 2rem; background: #cfd8dc; } nav ul { display: flex; list-style: none; } nav li { padding-left: 1rem; } nav a { text-decoration: none; color: lightblue } .active { background: red; }
- The resulting home page will be as follows
- But, now here the problem starts if you will notice the
home
page in the navbar does not have any kind of indication that it is the currently selected menu.
Highlighting the current menu (Dirty Solution)
- The easiest solution can be using the
handle_params
callback in each LiveView file and assigning some variable saycurrent_path
to the socket struct which will be accessible in thelive.html.heex
def handle_params(_params, url, socket) {:noreply, socket |> assign(current_path, URI.parse(url).path)} end
- In the
live.html.heex
, add the following logic<ul> <li class={"#{if @current_path == "/", do: "active"}"}> <%= live_patch "Home", to: Routes.home_page_path(socket, :index)%> </li> <li class={"#{if @current_path == "/page1", do: "active"}"}> <%= live_patch "Page 1", to: Routes.page1_path(socket, :index)%> </li> <li class={"#{if @current_path == "/page2", do: "active"}"}> <%= live_patch "Page 2", to: Routes.page2_page_path(socket, :index)%> </li> </ul>
- This will work but it will become very cumbersome when there will be more tabs also, this approach is not DRY.
Highlighting the current menu (Improved Solution)
- The above solution is not very DRY. We can improve this code using the concept of
on_mount
andattach_hook
. - We want that every time when the user changes the menu in our app it should trigger
handle_params
. Also, we don't wanthandle_params
to be placed in everyLiveView
. - We can add custom behavior to other callbacks using
attach_hook
. - So, we'll register this callback i.e
handle_params
to the socket struct, something like this.socket |> attach_hook(:set_menu_path, :handle_params, &manage_active_tabs/3)
- In the above code,
:set_menu_path
is the hook name:handle_params
is the callbackmanage_active_tabs
is the function body ofhandle_params
- This hook will be assigned to
socket
struct and this will be done in theon_hook
function. - We'll attach these hooks via Phoenix.LiveView.Router.live_session.
- We'll add a file
live/route_assigns.ex
, and we'll mount theRouteAssigns
module usingon_mount
in therouter.ex
as followslive_session :default, `on_mount: NavigationDemoWeb.RouteAssigns` do scope "/", NavigationDemoWeb do pipe_through :browser live "/", HomePageLive, :index live "/page1", Page1Live, :index live "/page2", Page2Live, :index end end
- In
on_mount
you have access to socket struct. We will assign amenus
key with value as a list of tuples ofmenu name
and theroute
of the menu. Refer to the following code
defmodule NavigationDemoWeb.RouteAssigns do import Phoenix.LiveView alias NavigationDemoWeb.Router.Helpers, as: Routes def on_mount(:default, _params, _session, socket) do socket = assign(socket, menus: [ {"Home", Routes.home_page_path(socket, :index)}, {"Page 1", Routes.page1_path(socket, :index)}, {"Page 2", Routes.page2_path(socket, :index)} ] ) {:cont, socket |> attach_hook(:set_menu_path, :handle_params, &manage_active_tabs/3) } end defp manage_active_tabs(_params, url, socket) do {:cont, assign(socket, current_path: URI.parse(url).path)} end end
- Notice
cont
in the return tuple ofon_mount
andmanage_active_tabs
functions. The hook has the option to either halt or continue the process. - In our
handle_params
callback function i.emanage_active_tabs
we will assigncurrent_path
field tosocket
struct. This field will help us to select the current menu. - Because of assigning
menus
to the socket variable it will be accessible in thelive.html.heex
file. - We can iterate over it i.e
menus
, with the first element of the tuple as thelink name
and the second element as thepath
. Refer to the code below// live.html.heex <ul> <%= for {menu_name, path} <- @menus do %> <li class={"#{if path == @current_path, do: "active"}"}> <%= live_patch menu_name, to: path %> </li> <% end %> </ul>
- In the above code, you can notice the
if
condition which appliesactive
if@current_path
is equal to the path of the menu. - The
active
class applied a background ofred
color to highlight the selected menu.
I know the blog was very conceptual with lots of information. I hope I did justice with the explanation and you like this blog. If you have any questions then please comment below. Thanks for reading ๐.
Subscribe to my newsletter
Read articles from AbulAsar S. directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
AbulAsar S.
AbulAsar S.
I am a Software Engineer from Mumbai, India. In love with Functional programming ( precisely Elixir). Love to share the knowledge that I learn while developing things.