Router navigation to different Liveviews based on dynamic url path

I want to navigate to different LiveViews based on the url path.

Let’s say I have this in my router:

scope “/”, FooWeb do
  pipe_through :browser
  live "/:slug", SlugLive, :index
end

Based on the lookup result of the slug I want to either render ALive, BLive or CLive.
So I have some lookup module that returns either :a, :b or :c and then I know which LiveView to render. But I’m not sure what’s the best way to do this?

So as an example, if I have a link “/123”, I use 123 to find the type, let’s say the result is :a and then I render ALive with param 123.
If it was “/129”, I might find :b and render BLive.

The solution I have found is to introduce a controller and use Phoenix.LiveView.Controller.live_render to render the correct LiveView. Not sure if this is the best solution but it works

I believe you can also do I with a single plug. Do your lookup in it and rewrite the path of the connection.

Do you want to be able to live navigate from ALive, etc, within the same live session (navigation over the existing WebSocket connection)?

Ignoring the HEEx template, how different are the target (in terms of their behavior, callbacks, …)?

Do you need to serve those different targets all on the same URL? Note that if you do, a refresh/reconnection has to go through the dispatch process again, whereas an explicit URL could go directly to the right LV.

It’s not that simple, because for now websockets do not go through the plug pipeline and LV does need to match the websocket connection to a router route. If the paths do not match up that doesn’t work.

Are you sure you want to do that? Is there a way to move logic and make it composable so you can still have just one liveview?

I’m asking because my experience has shown that it usually pays off to think twice before “going against the flow”.

1 Like

I’m migrating an existing app and want to keep the urls the same. I could do an url rewrite and go to a different path based on different types but don’t want to do that.

They are really different pages. To be fair, for some of them I probably don’t need a live_view at all and could do with a static page, some of them do need a live_view.

Did you consider then having a single LiveView (as in your original router snippet) and then simply delegate each callback to the appropriate implementation in another module? (They could also be LVs not attached to the router, only used via the “proxy”).

You have 3 options.

  1. Use a controller with live_render as you say.

  2. Use a single LiveView with live_render. This is what I would do. I don’t know the business requirements for slug -> live_view_module mapping but if these can change in real-time then you might want live-updates.

  3. Get all weird and freaky. Fork the Phoenix lib and the PhoenixLiveView lib and start writing weird stuff like this (do not do this, you will be sad):

defmodule MyAppWeb.Hacks do
  @moduledoc false

  defmacro painful_hackery(path, live_view) do
    live_view = Macro.expand_literals(live_view, %{__CALLER__ | function: {:live, 4}})

    quote bind_quoted: binding() do
      {action, router_options} =
        Phoenix.LiveView.Router.__live__(__MODULE__, live_view, nil, [])

      Phoenix.Router.get(path, MyAppWeb.Hacks, action, router_options)
    end
  end

  def init(_opts), do: nil

  def call(%Plug.Conn{} = conn, _opts) do
    live_view =
      case 1..2 |> Enum.random() do
        1 -> MyAppWeb.LiveView1
        2 -> MyAppWeb.LiveView2
      end

    conn
    |> Map.update!(:private, fn private ->
      Map.update!(private, :phoenix_live_view, fn {_live_view, opts, live_session} ->
        {live_view, opts, live_session}
      end)
    end)
    |> Phoenix.LiveView.Plug.call([])
  end
end

with routes like

  require MyAppWeb.Hacks

      MyApWeb.Hacks.painful_hackery "/:slug", Hacks

Note that this won’t work. Because of the metadata used at compile time when defining routes, something will crash. I used Enum.random so if you actually run this you will see the page refresh on every crash and switch randomly between page 1 and page 2.

1 Like

Before doing that I’d much rather suggest trying to move Phoenix.Router based socket routing by LostKobrakai · Pull Request #6142 · phoenixframework/phoenix · GitHub forward. That would allow a plug rewriting paths to just work.

1 Like

I am not sure what that PR is doing. I expected to see some changes to __before_compile__ in Phoenix.Router to account for the problem that underpins OP’s issue (one-to-many compile-time mapping between LiveView and route).

Can you please help me understand your PR? I don’t see an example of what kind of code it would allow us to write in the router.

Earlier it was suggested just to rewrite the request path with a plug, e.g. to /a/:slug => MyAppWeb.ALive and /b/:slug => MyAppWeb.BLive. So the router can match those separately with the live macro as it exists today not needing to care about the slug => LV page mapping.

That doesn’t work though, because LV requires the websocket request and the initial http request to agree on a path – as matched by the router. So if the router matches /a/:slug the websocket connection also needs to state it’s of path /a/:slug for LV to find the route on the router. But to the client it looks like it’s on /:slug – no additional prefix. Given the websocket request is handled completely separately to the plug pipeline by the socket macro, the path rewriting plug cannot apply the path customization for those websocket requests.

The separate handling of websocket connections has historical reasons, but is no longer technically necessary. Since phoenix 1.7 websocket requests can flow through the plug pipeline just fine. The socket macro doesn’t yet make use of that though. The PR I linked is about removing that separation. Once that’s gone a plug could customize the http request and the websocket request as needed to map both to a different path letting LV be none the wiser that the client is actually seeing a different path.

(Where I mention websockets I technically also mean long polling sockets, given that’s also done by the socket macro.)