Skip to content

Commit 68b70e0

Browse files
authored
Force remove stream elements on join patch (#3730)
* force remove stream elements on join patch * add implicit test * check owner before removing elements
1 parent e232cc5 commit 68b70e0

File tree

4 files changed

+48
-16
lines changed

4 files changed

+48
-16
lines changed

assets/js/phoenix_live_view/dom_patch.js

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -298,11 +298,19 @@ export default class DOMPatch {
298298

299299
// clear stream items from the dead render if they are not inserted again
300300
if(isJoinPatch){
301-
DOM.all(this.container, `[${phxUpdate}=${PHX_STREAM}]`, el => {
302-
Array.from(el.children).forEach(child => {
303-
this.removeStreamChildElement(child)
301+
DOM.all(this.container, `[${phxUpdate}=${PHX_STREAM}]`)
302+
// it is important to filter the element before removing them, as
303+
// it may happen that streams are nested and the owner check fails if
304+
// a parent is removed before a child
305+
.filter(el => this.view.ownsElement(el))
306+
.forEach(el => {
307+
Array.from(el.children).forEach(child => {
308+
// we already performed the owner check, each child is guaranteed to be owned
309+
// by the view. To prevent the nested owner check from failing in case of nested
310+
// streams where the parent is removed before the child, we force the removal
311+
this.removeStreamChildElement(child, true)
312+
})
304313
})
305-
})
306314
}
307315

308316
morph.call(this, targetContainer, html)
@@ -356,11 +364,11 @@ export default class DOMPatch {
356364
}
357365
}
358366

359-
removeStreamChildElement(child){
367+
removeStreamChildElement(child, force=false){
360368
// make sure to only remove elements owned by the current view
361369
// see https://github.com/phoenixframework/phoenix_live_view/issues/3047
362370
// and https://github.com/phoenixframework/phoenix_live_view/issues/3681
363-
if(!this.view.ownsElement(child)){ return }
371+
if(!force && !this.view.ownsElement(child)){ return }
364372

365373
// we need to store the node if it is actually re-added in the same patch
366374
// we do NOT want to execute phx-remove, we do NOT want to call onNodeDiscarded

lib/phoenix_live_view/test/client_proxy.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ defmodule Phoenix.LiveViewTest.ClientProxy do
9191
# because the live_module assign was set.
9292
root_html = DOM.parse(response_html, fn msg -> send(self(), {:test_error, msg}) end)
9393

94+
# clear stream elements from static render
95+
root_html = DOM.remove_stream_children(root_html)
96+
9497
{id, session_token, static_token, redirect_url} =
9598
case Map.fetch(opts, :live_redirect) do
9699
{:ok, {id, session_token, static_token}} ->

lib/phoenix_live_view/test/dom.ex

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,19 @@ defmodule Phoenix.LiveViewTest.DOM do
255255
|> elem(1)
256256
end
257257

258+
@doc """
259+
Removes stream children from the given HTML tree.
260+
"""
261+
def remove_stream_children(html_tree) do
262+
walk(html_tree, fn {tag, attrs, children} = node ->
263+
if attribute(node, "phx-update") == "stream" do
264+
{tag, attrs, []}
265+
else
266+
{tag, attrs, children}
267+
end
268+
end)
269+
end
270+
258271
# Diff merging
259272

260273
def merge_diff(rendered, diff) do

test/support/live_views/streams.ex

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -712,18 +712,26 @@ defmodule Phoenix.LiveViewTest.Support.StreamNestedComponentResetLive do
712712

713713
# first mount
714714
def update(assigns, socket) do
715+
items =
716+
if connected?(socket) do
717+
[
718+
%{id: assigns.id <> "-a", name: "N-A"},
719+
%{id: assigns.id <> "-b", name: "N-B"},
720+
%{id: assigns.id <> "-c", name: "N-C"},
721+
%{id: assigns.id <> "-d", name: "N-D"}
722+
]
723+
else
724+
[
725+
%{id: assigns.id <> "-e", name: "N-E"},
726+
%{id: assigns.id <> "-f", name: "N-F"},
727+
%{id: assigns.id <> "-g", name: "N-G"},
728+
%{id: assigns.id <> "-h", name: "N-H"}
729+
]
730+
end
731+
715732
socket
716733
|> assign(assigns)
717-
|> stream(
718-
:nested,
719-
[
720-
%{id: assigns.id <> "-a", name: "N-A"},
721-
%{id: assigns.id <> "-b", name: "N-B"},
722-
%{id: assigns.id <> "-c", name: "N-C"},
723-
%{id: assigns.id <> "-d", name: "N-D"}
724-
],
725-
reset: true
726-
)
734+
|> stream(:nested, items, reset: true)
727735
|> then(&{:ok, &1})
728736
end
729737

0 commit comments

Comments
 (0)