Liveview Navbar, component in layout receives socket?

I’ve been struggling with Phoenix LiveView and its outer layout setup.

I’d like to just have a very vanilla setup that would let me create a sidebar navigation panel showing the currently selected page for an admin tool. This seems like it would be a rather cookie-cutter thing to develop.

Most of the comments I’ve found note the LiveDashboard example, but it’s using a macro and PageBuilder module and isn’t quite the “simple” barebones thing I was hoping to find.

I found that I could inject a live_component call in the template layout for live.html.leex alongside the @inner_content for the page, but it appears that its render function receives the socket struct instead of the assigns map? This made me worry something weird is going on…

I feel like this kind of thing should be dead simple, and it’s driving me crazy that I can’t find a simple implementation anywhere.

Any advice or links would be appreciated, thanks!

What do you mean by this: “receives the socket struct instead of the assigns map”? You need to pass the assigns you’ll need in the component yourself. For a static admin sidebar something like this could work in live.html.leex:

<%= Phoenix.LiveView.Helpers.live_component(@socket, 
     AdminSidebarMenu, 
     id: :admin_sidebar_menu, 
     uri: @uri) %>

You would use the @uri struct (assign it in all of your LiveViews…this can be done by default in a helper), so that you can figure the current active page.

Recently, I am implementing navbar, too.

I am using the method provided by bytepack. You can read its source code, and improve it with your own idea.

1 Like

Thanks for your suggestion – I just haven’t thus far been able to find an example or documentation showing how to pass a uri into the root layout liveview. I can’t seem to intercept mount – and the socket structs path is nil (not matching what appears in the browser location…)

Any assign assigned in the root liveview will be available in live.html.leex. In the handle_params of your liveview, you can do:

def handle_params(params, uri, socket) do
  ...
  socket
  |> assign(:uri, uri)
  # or have this handled by a helper
  |> LiveHelpers.handle_params_defaults(uri)
end

I guess I cant figure out how to embed handlers in the leex file or associate an elixir file for the layout part?

The example code posted above will make @uri available in live.html.leex.

Ahh, ok, so this needs to be added to every page that uses the live layout? I was hoping it was possible to handle something at the layout level I guess. Thanks!

Not necessarily. For example, if you use assigns[:uri] instead of @uri in live.html.leex, it will return nil if it wasn’t assigned.

But, yes, if you wanna depend on the assign explicitly (eg: @uri), you’ll need to assign it in every liveview. That’s why I was suggesting to move such default machinery in a helper, similar to how LiveView docs suggest to handle assigning the current user into the socket.

2 Likes

That format cannot be change tracked by liveview, which is the reason it’s not a useful pattern to use in that context.

1 Like

Thanks! That page of documentation has the information I was really looking for! I never looked through it since it’s labeled “security considerations” – but it has the lifecycle explanation that I wasn’t following.

Many thanks!!!

Here is how I approached this problem:

  1. I set an on_mount hook in my lib/my_app_web/router.ex file:
scope "/", MyAppWeb do
  pipe_through([:browser, :require_authenticated_user])

    live_session :quiz_maker, on_mount: MyAppWeb.LeftMenu do
      live "/widgets", WidgetLive.Index, :index
      live "/gadgets/new", GadgetLive.Index, :index
      live "/sprockets/:id/edit", SprocketLive.Index, :index
      live "/fasteners/:id", FastenerLive.Index, :index
  end
end
  1. I created a lib/my_app_web/live/left_menu.ex file – the mount function is the on_mount hook that is run. When this hook is run, it sets another hook in turn – on the handle_params method:
defmodule MyAppWeb.LeftMenu do
  use MyAppWeb, :live_view

  def mount(params, session, socket) do
    socket =
      assign(socket,
        left_menu: [
          {"Widgets", Routes.widgets_index_path(socket, :index)},
          {"Sprockets", Routes.sprockets_index_path(socket, :index)},
          {"Gadgets", Routes.gadgets_question_index_path(socket, :index)},
          {"Fasteners", Routes.fasteners_index_path(socket, :index)}
        ]
      )

    socket =
      attach_hook(socket, :set_left_menu_active_path, :handle_params, fn
        _params, url, socket ->
          {:cont, assign(socket, left_menu_active_path: URI.parse(url).path)}
      end)

    socket = {:cont, socket}
  end
end
  1. in my lib/my_app_web/templates/layout/live.html.heex file I set up my left menu as follows:
<div id="left-menu">
    <%= for {menu_txt, path} <- @left_menu do %>
        <div class={"left-menu-item #{if path == @left_menu_active_path, do: "active"}"}>
            <a class="menu-txt" href={path}> <%= menu_txt %> </a>
        </div>
    <% end %>
</div>
5 Likes

Where is this Routes.widgets_index_path come from?
I do not have Routes module nor the function in my Phoenix app.

It’s an alias for MyAppWeb.Router.Helper (see my_app_web.ex), which is a module generated by your router automatically.

Thx for Routes.
the functions Routes.widgets_index_path(socket, :index)}…how do they look like? Do they return the path like “/my-url”?

Hey, I tried this approach but the drawback seems to be that either the :handle_params hook on the socket doesn’t seem to fire when navigating between pages using live_redirect/2 or my navbar never updates since it’s defined in the root layout which doesn’t change when navigating pages. So, the active navbar item never changes when I navigate to different pages unless I perform a full refresh. Do you have suggestions to better approach the problem?