Closed nathanmalishev closed 7 months ago
This is not expected to work and will cause other problems on the client, even if it first looks like it’s working.
https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#stream/4
Note: Failing to place phx-update="stream" on the immediate parent for each stream will result in broken behavior.
Okay thanks @SteffenDE , I believe the note should read differently as in the examples i still have the stream directly under the immediate parent. The only change in the producing the error throw is how content of the slot is rendered.
With the wrapper component, but without Enum.with_index
def render(assigns) do
~H"""
<%= render_slot(@tab) %>
"""
end
end
Results in the following HTML & no test errors:
But with the code with the Enum.with_index
defmodule AsyncStreamTestWeb.Wrapper do
def render(assigns) do
~H"""
<div :for={{tab, _i} <- Enum.with_index(@tab)}>
<%= render_slot(tab) %>
</div>
"""
end
end
Results in the HTML (extra div) & test errors:
But if i were to just add the extra div, results in no test errors without enum'ing over the slots
defmodule AsyncStreamTestWeb.Wrapper do
def render(assigns) do
~H"""
<div>
<%= render_slot(t@ab) %>
</div>
"""
end
end
Sorry, I misunderstood the placement of the wrapper. This does look like it should work and not throw an error. I'll look into it.
I am seeing the same error in our tests for a slightly different but similar issue after upgrading to 0.20.9.
** (EXIT from #PID<0.1202.0>) an exception was raised:
** (ArgumentError) a container with phx-update="stream" must only contain stream children with the id set to the `dom_id` of the stream item. Got:
<div id="test-no-options" class="tw-p-2 tw-hidden last:tw-block">
No Options Available
</div>
(phoenix_live_view 0.20.9) lib/phoenix_live_view/test/dom.ex:525: Phoenix.LiveViewTest.DOM.verify_only_stream_children!/3
(phoenix_live_view 0.20.9) lib/phoenix_live_view/test/dom.ex:426: Phoenix.LiveViewTest.DOM.apply_phx_update/4
The code:
<div id={"#{@id}-options"} phx-update="stream">
<div id="#{@id}-no-options" class="tw-p-2 tw-hidden last:tw-block">
No Options Available
</div>
<div :for={{option_id, option} <- @stream.options} id={option_id}>...</div>
</div>
If I remove the first inner div
with the id of @id-no-options
then the error goes away. This was working in 0.20.3
@SteffenDE just to confirm, is 0.20.10
supposed to resolve this issue? If so, I am still seeing this particular issue in tests.
@barkerja I tested with the repo from @nathanmalishev and the PR fixed this problem. If you still see this please try to provide a minimal example to reproduce. You can use this as a staring point:
Application.put_env(:phoenix, 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.10"},
{:phoenix_live_view, "~> 0.20.6"},
{:floki, ">= 0.30.0"}
])
ExUnit.start()
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
socket
|> then(&{:ok, &1})
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>
<script>
let liveSocket = new window.LiveView.LiveSocket("/live", window.Phoenix.Socket)
liveSocket.connect()
</script>
<%= @inner_content %>
"""
end
def render(assigns) do
~H"""
<p>The LiveView content goes here</p>
"""
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: :phoenix
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
defmodule Example.HomeLiveTest do
use ExUnit.Case
import Phoenix.ConnTest
import Plug.Conn
import Phoenix.LiveViewTest
@endpoint Example.Endpoint
test "works properly" do
conn = Phoenix.ConnTest.build_conn()
{:ok, _view, html} = live(conn, "/")
assert html =~ "The LiveView content goes here"
end
end
{:ok, _} = Supervisor.start_link([Example.Endpoint], strategy: :one_for_one)
ExUnit.run()
Process.sleep(:infinity)
@SteffenDE After a lot of debugging and head scratching, I was finally able to isolate the issue to being a case of when the DOM is updated to show a hidden stream element. Here is an example:
Application.put_env(:phoenix, Example.Endpoint,
adapter: Bandit.PhoenixAdapter,
http: [ip: {127, 0, 0, 1}, port: 5001],
server: true,
live_view: [signing_salt: "aaaaaaaa"],
secret_key_base: String.duplicate("a", 64)
)
Mix.install([
{:bandit, "~> 1.0"},
{:jason, "~> 1.0"},
{:phoenix, "~> 1.7.10"},
{:phoenix_live_view, "~> 0.20.8"},
{:floki, ">= 0.30.0"}
])
ExUnit.start()
defmodule Example.ErrorView do
def render(template, _), do: Phoenix.Controller.status_message_from_template(template)
end
defmodule Example.LiveComponent do
use Phoenix.LiveComponent
def render(assigns) do
~H"""
<div id="example">
<button id="show-btn" phx-click="show" phx-target={@myself}>Show</button>
<div :if={@show?}>
<div id={"#{@id}-example"} phx-update="stream">
<div id={"#{@id}-example-a"} class="hidden only:block">
"Example"
</div>
<div :for={{example_id, example} <- @streams.stream_example} id={example_id}>
<%= example.value %>
</div>
</div>
</div>
</div>
"""
end
def handle_event("show", _params, socket) do
{:noreply, assign(socket, show?: !socket.assigns.show?)}
end
def mount(socket) do
socket =
socket
|> assign(:show?, false)
|> stream_configure(:stream_example, dom_id: &"options-#{&1.id}")
|> stream(:stream_example, [%{id: "one", value: "Example 1"}, %{id: "two", value: "Example 2"}])
{:ok, socket}
end
end
defmodule Example.HomeLive do
use Phoenix.LiveView, layout: {__MODULE__, :live}
def mount(_params, _session, socket) do
socket
|> then(&{:ok, &1})
end
def render("live.html", assigns) do
~H"""
<script src="https://cdn.tailwindcss.com"></script>
<script src="/assets/phoenix/phoenix.js"></script>
<script src="/assets/phoenix_live_view/phoenix_live_view.js"></script>
<script>
let liveSocket = new window.LiveView.LiveSocket("/live", window.Phoenix.Socket)
liveSocket.connect()
</script>
<%= @inner_content %>
"""
end
def render(assigns) do
~H"""
<div>
<.live_component module={Example.LiveComponent} example id="example" />
</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: :phoenix
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
defmodule Example.HomeLiveTest do
use ExUnit.Case
import Phoenix.ConnTest
import Plug.Conn
import Phoenix.LiveViewTest
@endpoint Example.Endpoint
test "works properly" do
conn = Phoenix.ConnTest.build_conn()
{:ok, lv, html} = live(conn, "/")
lv
|> element("#show-btn")
|> render_click()
assert true
end
end
{:ok, _} = Supervisor.start_link([Example.Endpoint], strategy: :one_for_one)
ExUnit.run()
Process.sleep(:infinity)
@barkerja you are indeed rendering a non stream item:
<div id={"#{@id}-example-a"} class="hidden only:block">
"Example"
</div>
That’s why the error is thrown. This is the same as https://github.com/phoenixframework/phoenix_live_view/issues/3129#issuecomment-1957745739. So maybe this was a misunderstanding. The specific issue was resolved, the one you are seeing with rendering a leading non stream element is currently still not supported, but planned to change. 0.20.10/0.20.11 does not allow this yet.
So maybe this was a misunderstanding. The specific issue was resolved, the one you are seeing with rendering a leading non stream element is currently still not supported, but planned to change. 0.20.10/0.20.11 does not allow this yet.
Ah my apologies! I misunderstood and thought the recent updates also "fixed" this.
Environment
Actual behavior
Related to https://github.com/phoenixframework/phoenix_live_view/issues/3098 & the change here https://github.com/phoenixframework/phoenix_live_view/pull/3078.
After version 0.20.3, introduced the 'child stream error' - I believe it's also incorrectly being displayed when when wrapping a stream in a slot that's enumerated over. For example see the introduction of a wrapper component: https://github.com/nathanmalishev/async_stream_test/blob/3754e4da9e02a2dc8145ac001aeb9bb6b5f50da3/lib/lv_async_stream_test_web/live/user_live/index.html.heex#L10
And the component: https://github.com/nathanmalishev/async_stream_test/blob/3754e4da9e02a2dc8145ac001aeb9bb6b5f50da3/lib/lv_async_stream_test_web/components/wrapper.ex#L12
Results in an error like
an exception was raised: ** (ArgumentError) a container with phx-update="stream" must only contain stream children with the id set to the
dom_idof the stream item. Got:
Replacing this code
<div :for={{tab, _i} <- Enum.with_index(@tab)}> <%= render_slot(tab) %> </div>
with<%= render_slot(@tab) %>
removes the error
Expected behavior
I don't believe this should throw an error. In my particular case, i'm using a wrapper component like this to render different tabs or visible content on the screen.
Thanks