I am trying to use a stream to show a list of people and use “infinite scroll” to allow a user to scroll up and down the list. The list of people is fetched from the database sorted by first name, last name and then an id key.
I am using the example code in the “Scroll Events and Infinite Stream Pagination” almost exactly: Bindings — Phoenix LiveView v0.20.14 (hexdocs.pm) but I am experiencing some behaviour that is really hurting my brain.
If I scroll down the produced list of names everything is fine. The next page loads in correctly, the new data is appended to the end of the stream and if needed, data at the top of the stream is replaced.
When I scroll up to go to a previous page the behaviour is very different. The data for the previous page is fetched correctly but the new data is prepended to the very top of the stream which immediately breaks the ordering of the list.
The new data gets added to the top of the stream which means the user will scroll up past the left over stream items that have first names beginning with say, a K and then hit the newly fetched items that might begin with an M.
You can see the result here:
The first 20 results are the product of the “scroll up” pagination query and the ones below were pre-existing from the previous “scroll down” queries.
I’m clearly doing something wrong but I’m not sure what. I don’t know if I’m supposed to be compensating for the existing items in the stream when I calculate the offset on a scroll up or if I’m supposed to be re-ordering the stream manually in some way to ensure cohesion in the list. I’m fairly new to Streams and this is my learning case!
My code is almost verbatim the code from the documentation:
The template:
<div
:if={@live_action == :index}
id="user_container"
phx-update="stream"
phx-viewport-top={@page > 1 && "prev-page"}
phx-viewport-bottom={!@end_of_timeline? && "next-page"}
phx-page-loading
>
<div
:for={{dom_id, user} <- @streams.user_search_results}
id={dom_id}
class="mt-8 grid grid-col-1 gap-12"
>
<%= user.first_name %> <%= user.last_name %> (<%= user.employee_number %>)
</div>
<div :if={@end_of_timeline?} class="mt-5 text-[50px] text-center">
🎉 You made it to the beginning of time 🎉
</div>
</div>
the pagination functions (per_page is currently 20):
defp paginate_results(socket, 1, filters) do
filters = Map.delete(filters, :offset)
users =
Users.find_users(filters)
socket =
stream(socket, :user_search_results, users, reset: true)
|> assign(:filters, filters)
socket
end
defp paginate_results(socket, new_page, filters) when new_page >= 1 do
%{per_page: per_page, page: current_page} = socket.assigns
filters = Map.put(filters, :limit, per_page) |> Map.put(:offset, (new_page - 1) * per_page)
users =
Users.find_users(filters)
{users, at, limit} =
if new_page >= current_page do
{users, -1, per_page * 3 * -1}
else
{Enum.reverse(users), 0, per_page * 3}
end
case users do
[] ->
assign(socket, end_of_timeline?: at == -1) |> assign(:filters, filters)
[_ | _] = users->
socket
|> assign(end_of_timeline?: false)
|> assign(:page, new_page)
|> assign(:filters, filters)
|> stream(:user_search_results, users, at: at, limit: limit)
end
end
def handle_event("next-page", _, socket) do
end
def handle_event("prev-page", %{"_overran" => true}, socket) do
{:noreply, paginate_results(socket, 1, socket.assigns.filters)}
end
def handle_event("prev-page", _, socket) do
if socket.assigns.page > 1 do
{:noreply, paginate_results(socket, socket.assigns.page - 1, socket.assigns.filters)}
else
{:noreply, socket}
end
end