Handling nested lists in LiveView Streams (Grouped by date)

Is there a way I can implement a live view stream for things that have been grouped_by? When I try it, it’s leaving things that I thought would have been removed since it’s within the dive. (ie: the hr tag)
I’ve done it like this

  def render(assigns) do
    assigns = assign_new(assigns, :current_admin, fn -> nil end)

    ~H"""
    <div phx-update="stream" id="event-list-stream-wrapper">
      <div :for={{id, {date, event_list}} <- @events} id={id} class="space-y-1">
        <h2 class="pl-2 mb-2 text-lg font-bold bg-orange-50">
          <%= Calendar.strftime(date, "%a, %d ") %>
        </h2>
        <div :for={event <- event_list} id={event.id} class="pb-2 pl-1 space-y-2">
          <% last_event_id = List.last(event_list).id %>

          <.live_component
            module={ClimateCollectiveWeb.EventLive.EventComponent}
            id={event.id}
            event={event}
            current_admin={@current_admin}
          />
          <hr :if={event.id != last_event_id} />
        </div>
      </div>
    </div>
    """
  end

Logic for separating events

 def separate_event_stream_by_date(events) do
    Enum.group_by(events, fn event ->
      DateTime.to_date(event.starts_at)
    end)
    |> Enum.sort_by(fn {date, _} -> date end, {:asc, Date})
    |> Enum.to_list()
  end
 defp assign_events(socket, events) do
    separated_events = Event.separate_event_stream_by_date(events)
    stream(socket, :events, separated_events, reset: true)
  end

ezgif-2-8014c10d4f

Thank you!

I believe the id needs to go on a single element immediately inside the loop – in your code, it’s being used on a nested live component.

Try something closer to:

~H"""
<div :for={{id, el} <- @mystream}>
  <div id={id}>...</div>
</div>
"""

That id is how Phoenix is mapping stream data to dom nodes to add/remove.

Edit: I think all of what I’ve written above is wrong :see_no_evil:

Are you sure your DOM id’s are unique here? Does the js console report any errors? liveSocket.enableDebug() should already be running on the client in development and complaining if so. Also your stream comprehension is consuming @events rather than @streams.events. Is this a typo?

All of them have unique ID’s from what I see.

the code is a component that I passed through @streams.events as @events. Here’s the unwrapped version

<div id="event-streamer">
  <div phx-update="stream" id="event-list-stream-wrapper">
    <div :for={{id, {date, event_list}} <- @streams.events} class="space-y-1" id={id}>
      <h2 class="pl-2 mb-2 text-lg font-bold bg-orange-50">
        <%= Calendar.strftime(date, "%a, %d ") %>
      </h2>
      <div :for={event <- event_list} id={event.id} class="pb-2 pl-1 space-y-2">
        <% last_event_id = List.last(event_list).id %>

        <.live_component
          module={ClimateCollectiveWeb.EventLive.EventComponent}
          id={event.id}
          event={event}
          current_admin={@current_admin}
        />
        <hr :if={event.id != last_event_id} />
      </div>
    </div>
  </div>
</div>

No complaints were received unfortunately

My stream config is this

 |> stream_configure(:events,
        dom_id: fn {date, _event_list} ->
          "event-date-#{Date.to_string(date)}"
        end
      )

Since I’m separating events by dates, do I need to make each event list a stream as well?

Thanks for getting back to me, both of you, it means a lot!

We need to see the full example. It’s not yet clear to me how you’re filtering the stream items. If you’re trying to update grouped events within each stream item, you’ll need to re insert the parent each time, but it’s not fully clear what the goals are without seeing all the code.

Hey so the goal is to replace the entire event stream on update for v1.

On mount, and button click I query db for events for that month.

 ClimateCollective.Events.list_this_months_events_as_admin(
        month: date.month,
        year: date.year
      )

From there I break down the events by day

 def separate_event_stream_by_date(events) do
    Enum.group_by(events, fn event ->
      DateTime.to_date(event.starts_at)
    end)
    |> Enum.sort_by(fn {date, _} -> date end, {:asc, Date})
    |> Enum.to_list()
  end

then I assign it to the socket

  defp assign_events(socket, events) do
    separated_events = Event.separate_event_stream_by_date(events)
    stream(socket, :events, separated_events, reset: true)
  end

then I render the events

<div id="event-streamer">
  <div phx-update="stream" id="event-list-stream-wrapper">
    <div :for={{id, {date, event_list}} <- @streams.events} class="space-y-1" id={id}>
      <h2 class="pl-2 mb-2 text-lg font-bold bg-orange-50">
        <%= Calendar.strftime(date, "%a, %d ") %>
      </h2>
      <div :for={event <- event_list} id={event.id} class="pb-2 pl-1 space-y-2">
        <% last_event_id = List.last(event_list).id %>

        <.live_component
          module={ClimateCollectiveWeb.EventLive.EventComponent}
          id={event.id}
          event={event}
          current_admin={@current_admin}
        />
        <hr :if={event.id != last_event_id} />
      </div>
    </div>
  </div>
</div>

I use this call for

<.input
      type="checkbox"
      name="toggle_events"
      label="Hide past events"
      phx-click="toggle_past_events"
      value={@hide_past_events}
    />

from there a user clicks

  <.input
      type="checkbox"
      name="toggle_events"
      label="Hide past events"
      phx-click="toggle_past_events"
      value={@hide_past_events}
    />

which fires

  def handle_event("toggle_past_events", _, socket) do
    hide? = !socket.assigns.hide_past_events

    socket = assign(socket, :hide_past_events, hide?)
    events = get_events_by_socket_data(socket)

    {:noreply, assign_events(socket, events)}
  end

I’ve attached the gist, I hope this is enough! I’m sorry if this isn’t!

Hey so I see that it appears to be tied to this:

I made a PR for it but not confident about it: