How can I obtain the current url to pass to a functional component from the app.html.heex layout when rendering liveviews?

I have a sidebar as a function component that takes the current route as well as a list of items to display:

[%{
  title: "some title",
  route: ~p"/some/route"
}, ...]

The sidebar displays all of the items vertically, and if any of their routes matches the current route, then it adds a little highlight to display the currently selected page.

But now I would like to embed this into my layout, so that all dead and LiveViews can use this. So I put this in my app layout, since I would like the highlighted item to update if i live_redirect. But here is where I run into my issue.
If my app is rendering a dead view, I can access the @conn variable here, and obtain the current route to pass to my sidebar component. But if it is rendering a LiveView @conn is not available, and I must use @socket, where the current route is not available. Is there a way I can obtain the current route from the socket, or is there any other solution for obtaining the current route form the layout regardless if the page being rendered is a dead view/LiveView, and also one that updates across live_redirects etc.

You can make use of handle_params

Second parameter as you can see in the documentation is the current uri, so removing the base uri you can get the current path.

Something that you could use is

  @base_path "http://localhost:4000"



  def handle_params(_, @base_path <> path, socket) do
    IO.inspect(path)
    {:noreply, socket}
  end

I made a hook that I attach in a live session so the current path is always available

def on_mount(:current_path, _params, _session, socket) do
    socket =
      attach_hook(socket, :current_path, :handle_params, fn _params, uri, socket ->
        %{path: path} = URI.parse(uri)
        {:cont, Phoenix.Component.assign(socket, current_path: path)}
      end)

    {:cont, Phoenix.Component.assign(socket, current_path: "")}
  end

I thought this too, but this does not work in the app.html.heex file. I need the sidebar in this is the file since it is the layout that embeds the <.sidebar current_url={...} items={...}/> component, in all views (both dead views and liveviews).

I think the file is just a template, that I’m assuming compiles down to a function body in the layouts.ex file, similar to how other .heex files get converted into function bodies in the respective controller etc. Since the layout.ex file isn’t a LiveView, putting handle_params in that file doesn’t work. Even attaching a hook doesn’t work since the socket.assigns themselves are unavailable at the time that app.html.heex is evaluated. If I try to print them, even with a hook attached, the value of the assigns is Phoenix.LiveView.Socket.AssignsNotInSocket<>.

Can you make the sidebar a live view? then you can use handle_params

and in app.html.heex call it with <%= live_render(@conn, Sidebar) %>

I tried this as well, but this does not work for another reason. If you use live_render then handle_params isn’t invoked, and the app doesn’t even compile, and throws an error. handle_params is only ever invoked for live routes in the router.

Actually @brady131313 @gpopides, I think I have got it working with a hook. I think I was checking the wrong assigns.socket property and hence I was seeing Phoenix.LiveView.Socket.AssignsNotInSocket<>. When I just check the assigns in the app.html.heex, I can see the hook’s url output in there.

I’m still confused about the order of how the assigns works though, since handle_params is invoked after the mount of a liveview. So at what point do things get put in the assigns of the layout?

You can assign @current_path in both a plug and a LiveView hook (not to be confused with a js hook).

Plug:

defmodule MyAppWeb.Plugs do
  import Plug.Conn
  import Phoenix.Controller

  def assign_current_path(conn, _) do
    assign(conn, :current_path, current_path(conn))
  end
end

…and add to your browser pipeline in the router:

MyAppWeb.Router do
  # ...
  import MyAppWeb.Plugs

  pipeline :browser do
    # ...
    plug :assign_current_path
  end
end

Live Hook:

defmodule MyAppWeb.LiveHooks do
  import Phoenix.Component, only: [assign: 3]
  import Phoenix.LiveView, only: [attach_hook: 4]

  def on_mount(:global, _params, _session, socket) do
    socket =
      attach_hook(socket, :assign_current_path, :handle_params, &assign_current_path/3)

    {:cont, socket}
  end

  defp assign_current_path(_params, uri, socket) do
    uri = URI.parse(uri)

    {:cont, assign(socket, :current_path, uri.path)}
  end
end

…and attach the hook in your router:

live_session :some_name, on_mount: [{MyAppWeb.LiveHooks, :global}] do
  # ...
end

(:global can be something more descriptive if you like)

Now @current_path is always available in the layout whether rendered by a live or dead view.

You can use these methods to stuff other data into assigns as well.

EDIT: I forgot the router portion for the plug… see above.

2ND EDIT: Sorry, @brady131313, I blew past your message that offered the same hook advice.

5 Likes

Don’t be sorry, Soda. I am a complete beginner, and your example has been incredibly helpful.

1 Like