With LiveView.stream, how to update the actions on a table without fetching its related item?

When using the new LiveView stream feature, I want to disable the actions of an item (in a table) which is already being edited by another user.

For now, the only way I managed to do this is by calling stream_insert/4 even though this item didn’t really change yet. Otherwise, the actions won’t be updated because they are in the <tbody phx-update="stream">

Here is an example of the table component where Presence.is_entity_used(@presence_list, fruit.id) allows me to know if this item is being edited by another user, and if so, returns the user.

# fruit_live/index.html.heex

<.table id="fruits" rows={@streams.fruits}>
  <:col :let={{_id, fruit}} label="Name"><%= fruit.name %></:col>
  <:col :let={{_id, fruit}} label="Color"><%= fruit.color %></:col>
  <:col :let={{_id, fruit}} label="Quantity"><%= fruit.quantity %></:col>
  <:action :let={{_id, fruit}}>
    <%= if user = Presence.is_entity_used(@presence_list, fruit.id) -> %>
      <%= "This fruit is being edited by #{user.email}" %>
    <% else %>
      <.link patch={~p"/fruits/#{fruit}/edit"}>Edit</.link>
      <.link
        phx-click={JS.push("delete", value: %{id: fruit.id})}
        data-confirm="Are you sure?"
      >
        Delete
      </.link>
    <% end %>
  </:action>
</.table>

Here is a sample project implementing this feature.

My guess is I shouldn’t use streams in this case, what do you think?

Hey there, that’s an interesting scenario where only a very small portion of a streamed resource needs to be updated potentially frequently.

First thing that jumped out is that the logic checking for the resource “lock” is in the template itself, which means that every cell in the action column gets re-rendered whenever the @presence_list assign gets updated. Generally speaking, calling user/library functions and defining variables in HEEx templates is a known gotcha documented here under Pitfalls for Assigns and HEEx templates. A better place would be decorating the @streams.fruits streams to have that information outside/before rendering for example in mount.

And since you’re already making use of channels via Presence, I think it makes sense to more precisely stream_insert only the resources/entities that are being locked via subscribe, broadcast, and handle_info after the inital rendering. Basically, you could apply the event handling pattern for creating a new resource/entity with stream_insert to locking an existing resource/entity.

If you didn’t want to stream_insert, I suppose one option to explore would be using JS hooks and push_event to “re-hydrate” the action column.

p.s. Codeberg looks neat, but it seems to be missing a search bar

1 Like

Thanks for your answer @codeanpeace

First thing that jumped out is that the logic checking for the resource “lock” is in the template itself, which means that every cell in the action column gets re-rendered whenever the @presence_list assign gets updated.

Yes, but not in this case because streams are freed from socket as soon as they are rendered. The only way to trigger a re-render of a single row is by using stream_insert. But in other cases this is true.

A better place would be decorating the @streams.fruits streams to have that information outside/before rendering for example in mount.

Yes this is a good point and this is something I had in mind. I should probably add a virtual field “actions” to the fruit entity like so:

%Fruit{
...
actions: %{
    edit: :ok,
    delete: {locked: "Reason to be shown in the template"}
  }
}

But this wouldn’t solve my problem because on presence diff, I would still have to update fruits based on their ids.
And in some cases I can’t retrieve the desired struct of an item I want to update other than fetching it from database (which kind of defeats the purpose of streams) because streams doesn’t keep the state of what is displayed on the client as described in the doc:

Stream items are temporary and freed from socket state as soon as they are rendered

I think I miss a stream_update_by_dom_id which would allow to update some properties of an item without submitting the whole struct.

So my last option, as you said, would probably be to use JS hooks and update the table manually, but it seems too expensive to do, so I think I will just strop using streams for now. It is a nice to have but not essential in my case.

I will keep this post open in case I find something else.

Thanks for your help.

p.s. Codeberg looks neat, but it seems to be missing a search bar

They are looking for maintainers to work on the code search feature

1 Like

Hmm, could you describe those cases?

When a user clicks the edit button for a streamed item, that item has to be fetched from the database anyway to populate the edit form. It should be possible to then broadcast that item to other users subscribed to the topic and handle those messages by tagging the item as disabled before passing it into a stream_insert.

Anyways, looking forward to seeing what you come up with!

1 Like

When a user clicks the edit button for a streamed item, that item has to be fetched from the database anyway to populate the edit form. It should be possible to then broadcast that item to other users subscribed to the topic and handle those messages by tagging the item as disabled before passing it into a stream_insert.

The special case

I have a special case where only one item can be edited at a time because they all share a piece of data. I must lock the editing action for every entity in this case to avoid concurent editing.

So in this case I would need to stream_insert on every item, or fetch the whole list again every time a user start editing an item.

I think the solution here would probably be to pre render the disabled buttons hidden, and then use LiveView.JS.show and hide to enable/disable the actions when needed.

For every other lists where there is no such special case, I think I found my mistake

I have to handle the case where a user start editing an item and closes the modal or his browser tab without submitting the form.
I handled this by storing the initially fetched item in the Presence metas, so on Presence join and leave events, I could use the stored item to stream_insert.
But when the user submits the form, I only had an outdated item in the Presence metas.

My mistake was that I initially thought I couldn’t update the metas, but apparently it is possible. I was fetching from database on Presence leave, but I could just update the metas when the item is updated by the user.

So this is what it should look like:

  • When a user starts editing, it triggers Presence join event with the freshly fetched item in the metas.
  • When a user submits the form, I should update the item in the metas so it is up to date before it triggers the Presence leave event.
  • When the user leaves (by either closing the modal or the browser tab), I can still use the initially fetched item.

I have to thank you again, because I found this by re-reading the doc while writing this answer. This is where I saw the Presence.update that I initally missed :smiley:.

I might update the example repo with everything we discussed here in case someone is interested to see the result. If so, I will reply here to keep you up to date.

1 Like

I’ve updated the sample repo
Here is what it looks like with 2 different accounts side-by-side:

2 Likes