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.
- 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
- 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.
Thanks kamaroly
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
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
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.
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.
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
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 typicalMyAppWeb
module, so that it automatically gets added to each LiveView that hasuse 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
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.
it makes sense @sodapopcan. Thanks for your help!
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.
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.
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!