How to get current URL in app.html.heex

I have live_component (sidebar component) in app.html.heex in i need to hide and show based on the the current URL as every time the URL changes, any solution/suggestion please

Heres how I do it.

  1. Define a liveview mount hook.
defmodule MyAppWebWeb.Hooks.DefaultHooks do
  import Phoenix.Component
  import Phoenix.LiveView

  def on_mount(:default, _params, session, socket) do
    socket = attach_hook(socket, :current_path_hook, :handle_params, &put_uri_hook/3)
    {:cont, socket}
  end

  def put_uri_hook(_params, uri, socket), do: {:cont, assign(socket, uri: uri)}
end
  1. Call this in hook in your routes, then the @uri will be available in assigns in your liveview, you can pass it down your component as attribute.
5 Likes

Thanks kamaroly

1 Like

Is there nothing as a built in option for such a standard thing?

The current URI is available in the handle_params/3 callback of every LiveView, and there are mechanisms available in the framework to reduce repeated application logic. What’s suggested here is a good example of using those available mechanisms to set the URI in the assigns of every LiveView (without much code, IMO).

What were you hoping the “built in option” would work?

I think for people who are coming from all-batteries-and-your-grandmother-included frameworks are confused when not everything is built into the framework.

Something like “host_uri” from the socket, but this only gives me the domain, not the path.

I’ve got a global menu and in it I want to highlight buttons of the menu if they are selected. When clicked, the button takes you to a route and the logic would be “if the current_path is /example then highlight button “example”

My menu is in the “app.html.heex” and it only got “socket”, no “conn”

Thanks @jswanner . I tried to modify the handle-params in one of the live views by adding the url, but got different errors later in heex saying that I can’t use handle_params in a child LiveView . And even if I could, I would need to add this option manually to every liveview that is connected to a menu button.

Anyway, after trial and error, huffing and puffing, I wrote a JavaScript Hook. Now it all works, but it feels extremely cumbersome for something so small and basic.

PS: the JS hook does not identify the path, it just highlights any button that was clicked

1 Like

Yes @Stefano1990 , we kind of do. The understanding and the default assumption is that Phoenix is indeed a battery included framework. If it’s got Ecto and database Integration, it’s got authorisation nipped in the bud, It’s got email sending built-in, it’s got live view and pub subs. so yeah, I kind of expect it would be easy to get the current page from the heex template

1 Like

There’s multiple things problematic with an approach like this:

  • The path may change throughout the lifecycle of a LV process, hence it needs to be tracked for changes so templates update when it does.
  • LV doesn’t have any built in means of computed derived values. So if you have stuff in your assigns, which are derived from the url that needs to be manually computed in a callback triggered when the url changed (handle_params). If the socket would change without a callback you’d have nowhere to do such computations.
  • LV is stateful, so any information forcefully retained by LV does make your memory footprint worse. Hence LV retains the minimal amount of information it can leaving it up to the user to retain any additional information only if needed.

Given those constraints a callback like handle_params and letting all the change tracking work like anywhere else for any other retained custom data makes sense.

5 Likes

To add some context, the reason the current URL isn’t just available to views is that it needs to be stored on the socket (as mentioned above) which isn’t free. Not every application needs it and many applications only need pieces of it (for example perhaps they don’t care about query params or perhaps just the first slash segment) so it’s left up to the developer to decide what to store.

1 Like

Thanks @sodapopcan @LostKobrakai - I understand the listed reasons, but I still can’t justify them. I don’t know the how, but - if JS can do it, I’m sure Elixir/Phonenix can too and the memory footprint - I don’t think a couple of strings with slashes would clog the socket. I think that if Svelte (as compiled as Phoenix) can be fast and light, Elixir/Phoenix can do too. My take is that the reason for this is more historical and cultural than technical - and it’s fine by me.

I’m not going to win this battle. I’m just looking at it with the eyes of a newcomer and notice what in my opinion is something that is taken for granted elsewhere while that elsewhere is much more primitive (Svelte, Next.js) than Phoenix.

Anyway, if you can’t beat them - join them


A question about handle_params - I can’t use this approach for all of my menu buttons because one of them leads to a search modal based on a LiveView and my app.html.heex is also backed by a LiveView (no @conn) and If I use handle_params , I get an error
handle_params/3 is not allowed on child LiveViews, only at the root.

What could I do in this case? Thanks

P.S. Here is my repo

The ‘Territory’ menu button is the only one that can’t accept handle-params. The rest of them I adapted to your @LostKobrakai approach. Thanks

current_url

There are two ways to avoid the duplication of attaching the suggested LV hook (which is an alternative of having the handle_params callback of each LiveView assign the current url, which indeed would become very verbose):

  • attach on the level of a live_session (look for the :on_mount option)
  • attach using the Phoenix.LiveView.on_mount/1 macro. You can use this method in combination with the __using__ macro that is scaffolded in your typical MyAppWeb module, so that it automatically gets added to each LiveView that has use MyAppWeb, :live_view. This approach will also minimise repetition.

If you’re using the suggested LV hook, then you shouldn’t need the handle_params hook in any child LiveView (which indeed is not called since it’s not mounted in the router, hence the error). The url will be available as an assign on the socket of the child LiveView (assuming you’re using live_render/3).

I’d be surprised that the LV hook method is more involved than the JavaScript approach. But I must admit that the LV hooks have a steep learning curve. See you on the other side :slight_smile:

3 Likes

URLs can get very large depending on the application, especially when you take query parameters into account. That said, I do agree it could be made (even) simpler to opt into it, but that would require someone proposing a solution and doing the work. I doubt it’s a priority for the Phoenix team as most people are satisfied with status quo once they learn about it (I’ve answered this question a times on this forum).

Conversely, many people express concern over how much stuff is in the socket (a little more than they should be by me). So I do agree with the decision not to include it by default as many people would end up not using it. And again, people who do need it don’t always need the whole URL, so it would just open up other types of complaints.

This is great @linusdm ! Thanks a bunch! I used the #1 . Now I don’t need to go into each index.ex and show.ex for each route and it even works with my LV within LV case.

1 Like

it makes sense @sodapopcan. Thanks for your help!

1 Like

We’ll if we’re comparing client-side and server-side applications, then those are very different beasts. It’s not the client-side frameworks ultimately providing you with the current URL but the JavaScript runtime that’s making that value always available (window.location). It’s a little bit like how you can always get the PID of any BEAM process (with self()).

In my experience, using the current URL/path for marking navigation items as active/inactive starts off great, but then pretty quickly becomes inadequate. When sub navigation gets added we’ll switch from “equals” to “starts with,” then it always happens where multiple starting paths are under the same root navigation item and now we’ve got to rethink the strategy.

1 Like

This is the only way I’ve ever done it—what’s the alternative?

In cases where I only need to worry about one level of navigation, what I use is pretty similar to what’s described here, except I use the value of socket.assigns.live_action as the fallback value for what the assign the article calls active_tab instead of nil.

In cases where I have to worry about multiple levels of navigation, I prefer to be even more explicit and will put something like the following in all the LiveViews:

def mount(_, _, socket) do
  assign(socket, nav_root: :account, nav_sub: :preferences)
end

That way when I get a request along the lines of “I know when want everything under /account to market the ‘Account’ link as active, except when they are on /account/xyz then we want ‘Foo’ active.” I open up that one LiveView file, add assign(socket, :nav_root, :foo) and move on.

If using the current URL works for you in marking your nav items as active/inactive then that’s great. I personally wouldn’t be passing the URL/path to the templates do the active/inactive check down at that level. Instead, I would use a handle_params hook that sets an assign based on the URL, that way in those exceptional cases the value can be easily overridden in the respective LiveView.

4 Likes

Oh right, that all makes sense. I’ve just been doing it based off URL for so long that I never thought to it any other way. But ya, that makes sense. Thanks!