phoenixframework / phoenix_live_view

Rich, real-time user experiences with server-rendered HTML
https://hex.pm/packages/phoenix_live_view
MIT License
5.99k stars 902 forks source link

Clear redirected when creating component socket #3334

Open SteffenDE opened 4 days ago

SteffenDE commented 4 days ago

Under some circumstances it could happen that we copy the redirected value from the LV socket to a component socket, call update directly after and then raise an error, even if the component did not cause the redirect.

To reproduce:

Application.put_env(:sample, Example.Endpoint,
  http: [ip: {127, 0, 0, 1}, port: 5001],
  server: true,
  live_view: [signing_salt: "aaaaaaaa"],
  secret_key_base: String.duplicate("a", 64)
)

Mix.install([
  {:plug_cowboy, "~> 2.5"},
  {:jason, "~> 1.0"},
  {:phoenix, "~> 1.7"},
  # please test your issue using the latest version of LV from GitHub!
  {:phoenix_live_view,
   github: "phoenixframework/phoenix_live_view", branch: "main", override: true}
])

# build the LiveView JavaScript assets (this needs mix and npm available in your path!)
path = Phoenix.LiveView.__info__(:compile)[:source] |> Path.dirname() |> Path.join("../")
System.cmd("mix", ["deps.get"], cd: path, into: IO.binstream())
System.cmd("npm", ["install"], cd: Path.join(path, "./assets"), into: IO.binstream())
System.cmd("mix", ["assets.build"], cd: path, into: IO.binstream())

defmodule Example.ErrorView do
  def render(template, _), do: Phoenix.Controller.status_message_from_template(template)
end

defmodule Example.HomeLive do
  use Phoenix.LiveView, layout: {__MODULE__, :live}

  def mount(_params, _session, socket) do
    {:ok, socket}
  end

  def handle_params(params, _url, socket) do
    socket
    |> assign(:params, params)
    |> notify_child({:params, params})
    |> then(&{:noreply, &1})
  end

  defp notify_child(socket, msg) do
    if pid = socket.assigns[:child_pid] do
      send(pid, msg)
    end

    socket
  end

  def handle_info({:child_pid, pid}, socket) do
    {:noreply, assign(socket, :child_pid, pid)}
  end

  def render("live.html", assigns) do
    ~H"""
    <script src="/assets/phoenix/phoenix.js"></script>
    <script src="/assets/phoenix_live_view/phoenix_live_view.js"></script>
    <%!-- uncomment to use enable tailwind --%>
    <%!-- <script src="https://cdn.tailwindcss.com"></script> --%>
    <script>
      let liveSocket = new window.LiveView.LiveSocket("/live", window.Phoenix.Socket)
      liveSocket.connect()
    </script>
    <style>
      * { font-size: 1.1rem; }
    </style>
    <%= @inner_content %>
    """
  end

  def render(assigns) do
    ~H"""
    <%= live_render(@socket, Example.NestedLive, id: "mynested", session: %{"params" => @params}) %>
    """
  end
end

defmodule Example.Hook do
  import Phoenix.Component
  import Phoenix.LiveView

  def on_mount(:default, _params, session, socket) do
    %{"params" => params} = session

    send(socket.parent_pid, {:child_pid, self()})

    socket
    |> assign(:params, params)
    |> attach_hook(:handle_params, :handle_info, &handle_params/2)
    |> then(&{:cont, &1})
  end

  defp handle_params({:params, params}, socket) do
    if socket.assigns.params != params do
      socket =
        if function_exported?(socket.view, :handle_param_change, 2) do
          socket.view.handle_param_change(socket, params)
        else
          socket
        end

      {:halt, socket}
    else
      {:halt, socket}
    end
  end

  defp handle_params(_, socket), do: {:cont, socket}
end

defmodule Example.NestedLive do
  use Phoenix.LiveView
  use Phoenix.VerifiedRoutes, router: Example.Router, endpoint: Example.Endpoint

  on_mount({Example.Hook, :default})

  def mount(_params, session, socket) do
    %{"params" => params} = session

    {:ok, handle_param_change(socket, params)}
  end

  def handle_param_change(socket, params) do
    assign(socket, :params, params)
  end

  def handle_info({:patch, to, {flash_type, message}}, socket) do
    send_update(Example.LiveComponent, id: "mycomponent",
          updated: true
        )

    socket
    |> put_flash(flash_type, message)
    |> push_patch(to: to)
    |> then(&{:noreply, &1})
  end

  def render(assigns) do
    ~H"""
    <.link patch={~p"/?component=1"}>Open component</.link>

    <% x = @params %>
    <.live_component
      :if={@params["component"]}
      module={Example.LiveComponent}
      id="mycomponent"
      params={x}
      on_click={fn message ->
        send(self(), {:patch, ~p"/", {:success, message}})
      end}
    />
    """
  end
end

defmodule Example.LiveComponent do
  use Phoenix.LiveComponent
  use Phoenix.VerifiedRoutes, router: Example.Router, endpoint: Example.Endpoint

  def update(assigns, socket) do
    {:ok, assign(socket, assigns)}
  end

  def handle_event("do-something", _params, socket) do
    socket.assigns.on_click.("Clicked!")

    {:noreply, socket}
  end

  def render(assigns) do
    ~H"""
    <div>
      <button phx-click="do-something" phx-target={@myself}>Click me</button>
    </div>
    """
  end
end

defmodule Example.Router do
  use Phoenix.Router
  import Phoenix.LiveView.Router

  pipeline :browser do
    plug(:accepts, ["html"])
  end

  scope "/", Example do
    pipe_through(:browser)

    live("/", HomeLive, :index)
  end
end

defmodule Example.Endpoint do
  use Phoenix.Endpoint, otp_app: :sample
  socket("/live", Phoenix.LiveView.Socket)

  plug(Plug.Static, from: {:phoenix, "priv/static"}, at: "/assets/phoenix")
  plug(Plug.Static, from: {:phoenix_live_view, "priv/static"}, at: "/assets/phoenix_live_view")

  plug(Example.Router)
end

{:ok, _} = Supervisor.start_link([Example.Endpoint], strategy: :one_for_one)
Process.sleep(:infinity)