New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Client-side js crashes when component with phx-remove
set to JS.hide
navigates to another LiveView
#3139
Comments
PS. The soonest I'll have the time to make a repro app will be several weeks from now. Hopefully, someone figures it out in the meantime. |
Thank you for the report. I have added a label that we are waiting for a reproduction. |
Here's the min repro app. Btw, if the "page" of data is "loaded" synchronously (as opposed to async as in this example), the bug no longer manifests. Same if 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.4"},
{:gettext, "~> 0.23"},
{:phoenix, "~> 1.7.11"},
{:phoenix_ecto, "~> 4.4"},
{:phoenix_html, "~> 3.3"},
{:phoenix_live_view, "0.20.14", override: true},
{ :extructure, "~> 1.0"}
])
defmodule Repro3071.Gettext do
use Gettext, otp_app: :sample
end
defmodule Example.ErrorView do
def render(template, _), do: Phoenix.Controller.status_message_from_template(template)
end
defmodule Async do
defmacro async( do: code) do
quote do
Task.Supervisor.async( Example.TaskSupervisor, fn ->
unquote( code)
end)
end
end
end
defmodule PickerComponent do
use Phoenix.LiveComponent
import Extructure
def handle_event( event, _params, socket) do
socket =
case event do
"change_selection" ->
[ update_event] <~ socket.assigns
{ module, assigns} = update_event
send_update( module, assigns)
socket
end
{ :noreply, socket}
end
def render( assigns) do
~H"""
<div id={@id} class="p-[32px]">
<button
type="button" class="bg-yellow-200"
phx-click="change_selection" phx-target={@myself}
>
Second
</button>
</div>
"""
end
end
defmodule ItemComponent do
use Phoenix.LiveComponent
def render( assigns) do
~H"""
<div id={@id} class="flex flex-col gap-[24px]">
<%= @text %>
</div>
"""
end
end
defmodule MainComponent do
use Phoenix.LiveComponent
import Extructure
import Async
alias Phoenix.LiveView.JS
def mount( socket) do
socket =
socket
|> stream_configure( :items, dom_id: & &1.id)
|> stream_init_page()
|> assign_picker_open( false)
|> assign_ensuring( :connected)
{ :ok, socket}
end
def update( %{ event: event} = new_assigns, socket) do
socket =
case event do
:init_page ->
display_init_page( new_assigns.page, socket)
|> assign_ensuring( nil)
:something_changed ->
socket
|> assign_picker_open( false)
|> reload_items()
end
{ :ok, socket}
end
def update( new_assigns, socket) do
socket =
socket
|> assign( new_assigns)
|> ensure_assigns_available()
{ :ok, socket}
end
def handle_event( event, _params, socket) do
socket =
case event do
"open_picker" ->
assign_picker_open( socket, true)
"close_picker" ->
assign_picker_open( socket, false)
end
{ :noreply, socket}
end
defp ensure_assigns_available( socket)
defp ensure_assigns_available( %{ assigns: %{ ensuring: :connected}} = socket) do
if connected?( socket) do
socket
|> load_init_page()
|> assign_ensuring( :load_init)
else
socket
end
end
defp ensure_assigns_available( socket) do
socket
end
defp reload_items( socket) do
socket
|> stream( :items, [], reset: true)
|> stream_init_page()
|> load_init_page()
|> assign_ensuring( :reload_init)
end
defp load_init_page( socket) do
[ id] <~ socket.assigns
lv_pid = self()
async do
page = Enum.map( 1..10, &%{ id: "item-#{ &1}"})
send_update( lv_pid, __MODULE__, %{ id: id, event: :init_page, page: page})
end
socket
end
defp display_init_page( page, socket) do
stream_page( page, socket)
end
defp stream_init_page( socket) do
stream_page( [], socket)
end
def stream_page( page, socket) do
stream( socket, :items, page)
end
defp assign_picker_open( socket, picker_open?) do
assign( socket, :picker_open?, picker_open?)
end
defp assign_ensuring( socket, ensuring) do
assign( socket, :ensuring, ensuring)
end
def render( assigns) do
~H"""
<div id={@id} class="relative flex flex-col outline-none">
<!-- if div below is removed or the phx-remove in the nested element is removed, the bug no longer manifests -->
<div class="pt-[40px] flex flex-col items-center">
<button type="button" class="bg-blue-200" phx-click="open_picker" phx-target={@myself}>
First
</button>
<div
:if={@picker_open?}
class="flex flex-col items-center"
phx-remove={
JS.hide(
transition: { "transition ease-in-out duration-150", "opacity-100 scale-100", "opacity-0 scale-[85%]"}
)
}
>
<.live_component
module={PickerComponent}
id={"PickerLive-#{@id}"}
update_event={{ __MODULE__, id: @id, event: :something_changed}}
/>
</div>
</div>
<button
type="button" class="bg-red-200"
phx-click={JS.navigate( "/")}
>
Third
</button>
<div id={"Stream-#{@id}"} class="w-[954px] flex flex-col gap-[24px]" phx-update="stream">
<.live_component
:for={{ dom_id, _item} <- @streams.items}
module={ItemComponent}
id={dom_id}
text="some text"
/>
</div>
</div>
"""
end
end
defmodule Example.HomeLive do
use Phoenix.LiveView, layout: {__MODULE__, :live}
defp phx_vsn, do: Application.spec(:phoenix, :vsn)
def handle_params( _params, _url, socket) do
{ :noreply, socket}
end
def handle_info( _msg, socket) do
{ :noreply, socket}
end
def render("live.html", assigns) do
~H"""
<script src={"https://cdn.jsdelivr.net/npm/phoenix@#{phx_vsn()}/priv/static/phoenix.min.js"}></script>
<script src={"https://cdn.jsdelivr.net/gh/phoenixframework/phoenix_live_view@main/priv/static/phoenix_live_view.min.js"}></script>
<script src="https://cdn.tailwindcss.com"></script>
<script>
let liveSocket = new window.LiveView.LiveSocket("/live", window.Phoenix.Socket)
liveSocket.connect()
</script>
<style>
* { font-size: 16px; }
</style>
<%= @inner_content %>
"""
end
def render( assigns) do
~H"""
<.live_component
module={MainComponent}
id="main"
/>
"""
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, :default)
end
end
defmodule Example.Endpoint do
use Phoenix.Endpoint, otp_app: :sample
socket("/live", Phoenix.LiveView.Socket)
plug(Example.Router)
end
children = [
Example.Endpoint,
{ Task.Supervisor, name: Example.TaskSupervisor}
]
{ :ok, _} = Supervisor.start_link( children, strategy: :one_for_one, name: Example.Supervisor)
Process.sleep( :infinity) |
Ah, yes, the reproduction sequence:
|
@josevalim Can you please take the |
Btw, I know we had it settled it wasn't your concern, but for curiosity sake, in the meantime I've discovered that virtually all issues arising from the LiveView/Alpine integration are also related to the A workaround for all this is essentially removing the |
Environment
Actual behavior
Setup
LiveView1 has a function component that conditionally renders an element with a child LiveComponent1.1 that switches to LiveView2 on a button click. The conditionally rendered element has the
phx-remove
set to aJS.hide
with a transition.Bug reproducing interaction
phx-remove
that contains LiveComponent1.1Notes:
phx-remove
has an id or not.Without the
phx-remove
, it all works.Chrome web console crash message
Firefox web console crash message
The text was updated successfully, but these errors were encountered: