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)
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: