live view stream insert behavior is inconsistent with docs
Environment
- Elixir version (elixir -v): 1.17.3
- Phoenix version (mix deps): 1.7.21
- Phoenix LiveView version (mix deps): 1.0.9
- Operating system: MacOS
- Browsers you attempted to reproduce this bug on (the more the merrier): brave
- Does the problem persist after removing "assets/node_modules" and trying again? Yes/no: N/A
Actual behavior
in the docs here: https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#stream/4-handling-the-empty-case
this example code doesn't actually render the way the template is defined which could mean unintended side effects.
this template:
<table>
<tbody id="songs" phx-update="stream">
<tr id="songs-empty" class="only:block hidden">
<td colspan="2">No songs found</td>
</tr>
<tr
:for={{dom_id, song} <- @streams.songs}
id={dom_id}
>
<td>{song.title}</td>
<td>{song.duration}</td>
</tr>
</tbody>
</table>
will actually render like this:
<table>
<tbody id="songs" phx-update="stream">
<tr
:for={{dom_id, song} <- @streams.songs}
id={dom_id}
>
<td>{song.title}</td>
<td>{song.duration}</td>
</tr>
<tr id="songs-empty" class="only:block hidden">
<td colspan="2">No songs found</td>
</tr>
</tbody>
</table>
Expected behavior
ideally, there should be some way to specify the insert behavior or it should just work as defined in the template.
It does render with songs-empty as first child (you can inspect the HTML source of the disconnected render), but when the stream children are later patched, they are inserted before non stream children. Currently, this cannot be customized, but you can add a custom hook:
export const StreamSortPrePostChildren = {
mounted() {
this.sort();
},
updated() {
this.sort();
},
sort() {
if (this.el.getAttribute("phx-update") !== "stream") return;
const preChildren = this.el.querySelectorAll("[data-pre-child]");
const postChildren = this.el.querySelectorAll("[data-post-child]");
if (preChildren.length == 0 && postChildren.length == 0) return;
const streamChildren = Array.from(this.el.children).filter((node) => node.hasAttribute("data-phx-stream"));
if (streamChildren.length > 0) {
const firstStreamChild = streamChildren[0];
const lastStreamChild = streamChildren[streamChildren.length - 1];
Array.from(preChildren).forEach(node => firstStreamChild.insertAdjacentElement("beforebegin", node));
Array.from(postChildren).forEach(node => lastStreamChild.insertAdjacentElement("afterend", node));
}
}
}
and then annotate the stream with the hook, and the elements with data-pre-child / data-post-child depending on where they should be.
@SteffenDE right, sorry, that was what I was talking about, I believe live view should provide an API to allow a user to determine what is the deterministic output. e.g. phx-stream-pre="songs-empty" phx-stream-post="songs-empty"
This is the first time this has come up, so I'm not convinced that we need an official API for it yet. I'm happy to change my mind if this comes up more frequently.
In the "empty" use case it doesn't really matter where the only child renders. Do you have a use case where it does?