Highlighting the current page link in the Navbar menu in Phoenix LiveView

AbulAsar S.AbulAsar S.
5 min read

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 run mix phx.server. Once, the project starts running navigate to localhost:4000 to see the Welcome 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 routes
    live "/", 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, and page2_live.ex.
  • Add mount and render 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 styling

    nav {
      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 Screenshot 2022-06-11 at 12.34.14 PM.png
  • 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 say current_path to the socket struct which will be accessible in the live.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 and attach_hook.
  • We want that every time when the user changes the menu in our app it should trigger handle_params. Also, we don't want handle_params to be placed in every LiveView.
  • 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 callback
    • manage_active_tabs is the function body of handle_params
  • This hook will be assigned to socket struct and this will be done in the on_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 the RouteAssigns module using on_mount in the router.ex as follows
    live_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 a menus key with value as a list of tuples of menu name and the route 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 of on_mount and manage_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 assign current_path field to socket struct. This field will help us to select the current menu.
  • Because of assigning menus to the socket variable it will be accessible in the live.html.heex file.
  • We can iterate over it i.e menus, with the first element of the tuple as the link name and the second element as the path. 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 applies active if @current_path is equal to the path of the menu.
  • The active class applied a background of red 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 ๐Ÿ˜Š.

5
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.