feat(member): stream the overview with keyset infinite scroll instead of loading every member

This commit is contained in:
Simon 2026-07-03 11:31:44 +02:00
parent b09cdf7f3a
commit b79d7ac9ea
13 changed files with 1067 additions and 451 deletions

View file

@ -953,6 +953,11 @@ defmodule MvWeb.CoreComponents do
doc:
"when true, first header/body column gets sticky left positioning to keep selection controls visible"
attr :viewport_bottom, :any,
default: nil,
doc:
"optional phx-viewport-bottom event (string) rendered on the streamed tbody so the built-in InfiniteScroll hook appends the next page without shifting the scroll position; nil omits the binding"
slot :col, required: true do
attr :label, :string
attr :class, :string
@ -964,6 +969,10 @@ defmodule MvWeb.CoreComponents do
slot :action, doc: "the slot for showing user actions in the last table column"
slot :footer,
doc:
"optional after-rows content rendered as a full-width row in a separate, non-streamed tbody. Combined with viewport_bottom it acts as the infinite-scroll sentinel (e.g. a loading indicator) that fires the load event and disappears once no more pages remain."
def table(assigns) do
assigns =
with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do
@ -986,6 +995,15 @@ defmodule MvWeb.CoreComponents do
assigns =
assign(assigns, :first_row_click_col_idx, first_row_click_col_idx)
# Total column span for full-width footer rows (e.g. the infinite-scroll
# loading indicator): fixed columns + dynamic custom-field columns + the
# optional action column.
col_count =
length(assigns.col) + length(assigns[:dynamic_cols] || []) +
if assigns.action != [], do: 1, else: 0
assigns = assign(assigns, :col_count, col_count)
~H"""
<div
id={@row_click && "#{@id}-keyboard"}
@ -1021,7 +1039,10 @@ defmodule MvWeb.CoreComponents do
</th>
</tr>
</thead>
<tbody id={@id} phx-update={is_struct(@rows, Phoenix.LiveView.LiveStream) && "stream"}>
<tbody
id={@id}
phx-update={is_struct(@rows, Phoenix.LiveView.LiveStream) && "stream"}
>
<tr
:for={row <- @rows}
id={@row_id && @row_id.(row)}
@ -1126,6 +1147,21 @@ defmodule MvWeb.CoreComponents do
</td>
</tr>
</tbody>
<%!-- After-rows footer in its own, non-streamed tbody so it stays put
outside the phx-update="stream" container. When viewport_bottom is set it
doubles as the infinite-scroll sentinel: rendered only while more pages
exist, so once the last page loads it disappears and stops firing. --%>
<tbody
:if={@footer != [] && @viewport_bottom}
id={"#{@id}-footer"}
phx-viewport-bottom={@viewport_bottom}
>
<tr>
<td colspan={@col_count}>
{render_slot(@footer)}
</td>
</tr>
</tbody>
</table>
</div>
"""