Take the table generated with core_components.
Say I wanted to add checkboxes to the first column of my table, like the below
However my table is using a stream for the rows, and I only want these checkboxes to show conditionally (ie when entering an ‘edit mode’)
As phx-update="stream"
is set, the row will not change if I add any conditioal code inside it. For example,
<td class={[@selectable_rows? && "hidden"]}>
<.input type="checkbox> />
</td>
When @selectable_rows?
is toggled, nothing would happen (presumably because the content inside the phx-update="stream"
is now essentially clientside, so LiveView won’t send any more updates)
Im not realy sure on a good way to approach my issue however
- Is this possible to achieve without having to reset the stream?
- Do I have to send an
stream_insert
to every row to trigger an update?
- Do I have a seperate table to the left of my actual table with the checkboxes?
Would be intrested to hear some thoughts on this as none of my ideas feel very ‘good’ 
Hey @joshua-bouv,
you’re right that when you change your selectable_rows?
assign, nothing happens because on the server the stream is empty.
When you always render the column though - as you do - and you just need to toggle it from hidden to visible, you can push an event from the server. Something like this (I didn’t test the code!):
<td class={[@selectable_rows? && "hidden"]} data-toggle-row>
<.input type="checkbox> />
</td>
def handle_event("toggle_edit_mode", _params, socket) do
...
{:noreply, push_event(socket, "toggle_edit", %{"id" => "my-table"})}
end
# app.js
window.addEventListener("phx:toggle_edit", (e) => {
document.querySelector(`${e.detail.id} [data-toggle-row]`).forEach(node => {
if (node.classList.contains("hidden") {
node.classList.remove("hidden");
} else {
node.classList.add("hidden");
}
})
})
Note that if you toggle the edit_mode using a button, you can also use JS.toggle_class:
<button phx-click={JS.toggle_class("hidden", to: "#my-table [data-toggle-row]") |> JS.push("toggle_edit_mode")}>Toggle edit mode</button>
If you don’t need to do anything on the server, the extra JS.push isn’t needed.
2 Likes
Thank you @steffend !
My brain didn’t even think about using JS for this, but it does make total sense considering the stream lives on in the client!
Building on to top of your solution, I was thinking I could potentially skip the push_event
and instead attach the style to just above the stream. For example
thead.hidden-checkbox th:nth-child(1) {
display: none;
}
tbody.hidden-checkbox td:nth-child(1) {
display: none;
}
<tbody
id={@id}
phx-update={match?(%Phoenix.LiveView.LiveStream{}, @rows) && "stream"}
class={not @selectable_rows? && "hidden-checkbox"} # this
Would you say this could also be a reasonable approach or might it be questionable behaviour?
And I will be pinching the idea of using JS.toggle_class
for my edit button! Thank you!!
I just didn’t think about this. That’s a perfectly good solution, in my opinion much better than what I suggested. And you can also use toggle_class with it to toggle purely client-side!
2 Likes
I think this post is a good example of why we need to be more careful about recommending streams to new users. The way they break the declarative rendering model is very unintuitive and makes it more difficult to implement basic features like this. Obviously I have already written about this on here far too extensively, but again - this is a good example.
To the OP: if your real workload is similar to the example you gave, consider whether you really need to use streams for this case. Unless you have literally thousands of rows it’s probably not necessary. If you have, say, dozens to hundreds of rows, then a LiveComponent-per-row approach will perform similarly while preserving the declarative rendering model which you were expecting.
The CSS trick is a great solution for this particular case (I have done many similar things), but it will not scale to arbitrarily complex UI - you are going to run into problems like this again, eventually, and there may not be such an easy way out.
Obviously there are myriad cases where this is a valid approach, but you have to be very careful layering on the imperative complexity here. As an app grows, you are going to have to keep stacking more and more side-effects (events) on top of each other, and then you will have to keep stacking more and more conditional cases to handle how those events interact with each other. And with this approach, now all that work is also happening in JS instead of on the server - the horror! 
In my case, I do, and im doing a bunch of (necessary) preloading on each row too.
I agree it’s a ‘trick’, and nobody wants a codebase based on tricks! However my actual implementation was rather undesruptive so I am not too bothered by it.
I just create an extra <:col>
for my table which has my checkbox input, as you would any other column, and pass the thead
and tbody
hidden_checkbox
class into my <.table
to default the checkbox column as hidden.
As the classes aren’t actually changed in the <.table
component attribute (as it’s using JS.toggle_class
), there isn’t any unesccary re-rendering of that table either when I flip between edit and non-edit modes (I don’t think anyway?).
So the default core_components
table is almost unchaged, other than I add an optional attrbute for the thead
and thead
class.
But I will be careful in the future when I face my next inevitable problem. Thank you for your advice!
1 Like
Ah okay - I wasn’t going to get into this because I thought it wasn’t relevant but if you actually have that many rows you still don’t actually need to use streams.
There is no reason you can’t virtualize the table on the server instead and store the current “page” in-memory. You can load more rows on scroll up/down just like streams, but render them as LiveComponents instead so that you can perform operations on them (like the edit state change).
The weird thing is that this isn’t really more complicated than the stream approach - it’s actually much easier to reason about. Which is why I worry about streams being recommended for this in the docs.
I think the one case where streams would be a clear winner is if all of the rows somehow have to be on-screen at once. The only case where that really happens in practice is when rendering a large chart, and actually I seem to recall that being the example Chris referenced for how streams came about. I’m still not confident that is a problem LiveView really needs to solve, but that water is under the bridge.
I don’t mean to knock on the CSS trick, I actually quite like it. Solving this in CSS is great with LiveView because you get to avoid server latency.
What I was getting at is that there are variations of this problem, more complicated variations, that require actual “code” to solve. And with a streams approach solving them will get very difficult, because you don’t have the declarative rendering model to help you manage the exponentially-increasing state space of your UI.
There shouldn’t be, no. But since you’re using streams there will be no re-renders at all anyway in this case, because that’s how streams work.
1 Like