How to write a component with Routes for LiveView and DeadView?

How to write a component that has a Route inside, where the component can be rendered in both LiveView and DeadView?
For example,

defmodule DemoWeb.Components.Nav do
  use DemoWeb, :component

  def nav(assigns) do
    ~H"""
     <%= live_redirect to: Routes.user_session_path(@socket, :new) do %>
        Login
     <% end %>
    """
  end
end

If I am rendering this <.nav />, in a DeadView, I am getting an error that @socket is not available. If I change it to @conn, in a LiveView, I am getting an error that @conn is not available.
How to write such components?

Shouldn’t something like Map.get(assigns, :socket, assigns.conn) work?
Creating a function and pattern match the assigns would probably be cleaner though.

def socket_or_conn(%{socket: socket}), do: socket
def socket_or_conn(%{conn: conn}), do: conn
1 Like

Hmm… sorry, could not get it. What would be the statement inside the component?
<%= live_redirect to: Routes.user_session_path(socket_or_conn, :new) do %> ?
Not working.

For the first case:

<%= live_redirect to: Routes.user_session_path(Map.get(assigns, :socket, assigns.conn), :new) do %>

This would try to get the value for the key :socket out of your assigns map and if that isn’t in the map, it returns socket.conn as the default value.

For the second case:
Define the socket_or_conn/1 function somewhere and import it into your controller and live view and then:

<%= live_redirect to: Routes.user_session_path(socket_or_conn(assigns), :new) do %>

should work.

1 Like

But, the component will not know whether it is first case or second case na?

The first and second case in my example depends on if you want to use Map.get with a default or define your own function.

(You could also just use your endpoint as first arg in your Route.Helpers functions)

I used the 2nd case. It worked. Thanks.

In a DeadView, no function clause matching in DemoWeb.LiveHelpers.socket_or_conn/1 is the error message I am getting. In a LiveView it is working fine.

How do you call socket_or_conn in your DeadView?

I imported the module in view_helpers

You can pass the link as an assign parameter.

Tried

 <.nav current_user={@current_user}
      @user_setting_path={Routes.user_settings_path(@socket, :edit)}
      @user_logout_path={Routes.user_session_path(socket_or_conn(assigns), :delete), method: :delete} />

gave an issue. For the logout path, method can not be given like that. So, another assign parameter for method has to be given.
I thought I am doing it wrong - and - there must be a simpler way.

Yes, the assigns aren’t passed down by default. You could use <.nav {assigns} /> but I wouldn’t do that.
Passing the route (as @kokolegorille mentioned) is probably the cleanest solution.

Something like:

<.nav current_user={@current_user}
      user_setting_path={Routes.user_settings_path(@socket, :edit)}
      user_logout_path={Routes.user_session_path(@socket, :delete), method: :delete} />

in your LiveView and

<.nav current_user={@current_user}
      user_setting_path={Routes.user_settings_path(@conn, :edit)}
      user_logout_path={Routes.user_session_path(@conn, :delete), method: :delete} />

in your DeadView should work.

Then just use @user_setting_path and @user_logout_path in your Component.

1 Like

Tried this method as the first. As mentioned to @kokolegorille , I am getting an error user_logout_path={Routes.user_session_path(@socket, :delete), method: :delete} - unexpected symbol ,.

Remove the , method: :delete.
That is not part of the route. You probably copy-pasted it from the delete link phoenix generated.

2 Likes

protocol Phoenix.HTML.Safe not implemented for {"/users/log_in"} of type Tuple is the error.

<.nav current_user={@current_user}
          user_setting_path={Routes.user_settings_path(@socket, :edit)}
          user_logout_path={Routes.user_session_path(@socket, :delete)}
          user_signin_path={Routes.user_session_path(@socket, :new)} />
<DemoWeb.Components.Nav.nav current_user={@current_user} user_setting_path={Routes.user_settings_path(@conn,
      :edit)} user_logout_path={Routes.user_session_path(@conn, :delete)}
      user_signin_path={Routes.user_session_path(@conn, :new)} />

are the calls in two cases.
<%= live_redirect "Settings", to: {@user_setting_path} %> etc. are the calls in the component definition.

You don’t need {} there.

1 Like

Thanks a lot! That was sharp eye.
Finally, passing the Routes as assign params is what worked.

I am still not fully convinced that this is the most elegant solution - because, the component does have access to Routes - and - passing them as arguments again is not making all that sense.
Let us see, if there are other ways.
Thanks, for staying with me on a holiday.

No problem.
As mentioned you could always use your endpoint as first parameter in your route helpers (Routes.user_settings_path(DemoWeb.Endpoint, :edit)) if that would be more elegant for you.

1 Like

I tried this. This is almost working - and - I felt this is an elegant method also.
Small trivia -
For the logout method Routes.user_session_path(@socket, :delete), method: :delete is necessary. The logout method is a HTTP DELETE method. live_redirect works only for get request, I think. So, I had to use
<%= link to: Routes.user_session_path(DemoWeb.Endpoint, :delete), method: :delete do %>
for the logout link.
This is for information and for anyone who is following the thread.
Thanks again.

1 Like