Phx-update="append" seems to replace items instead of appending

Hi! I’m creating an infinitescroll and the current behaviour is: when I scroll at the very bottom, existing elements are replaced instead of adding it at the bottom. I’m using scrivener_ecto for pagination, following the liveview docs and threads on how to build it.

created live component :

def render(assign) do
...
<div class="grid grid-cols-3 auto-rows-[32rem] gap-2">
  <%= for image <- @images do %>
    <div id={"image-#{image.slug}"}>
      <a href={Routes.image_path(@socket, :show, image.slug)}>
        <img class="object-cover h-full" src={image.url} />
      </a>
      <a
        class="absolute bottom-4 left-4 flex gap-2 items-center"
        href={Routes.user_profile_path(@socket, :show, image.user.username)}
      >
        <img
          class="rounded-full w-9 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-900"
          src={Accounts.avatar_url(@socket, image.user.avatar)}
        />
        <span><%= image.user.username %></span>
      </a>
    </div>
  <% end %>
</div>
<div id="infinite-scroll" phx-hook="InfiniteScroll" phx-update="append" data-page={@page}></div>
...

Then added in a template:

# live/gallery_live/index.html.heex
...
<section>
  <.live_component
    module={GalleryComponent}
    id="gallery"
    images={@images}
    page={@page}/>
</section>

created live_view

# live/gallery_live/index.ex
@impl true
def mount(_params, _session, socket) do
  {:ok,
   socket
   |> assign(page: 1)
   |> assign(per_page: 10)
   |> fetch(), temporary_assigns: [images: []]}
end

@impl true
def handle_event("load-more", _unsigned_params, %{assigns: assigns} = socket) do
  {:noreply, assign(socket, page: assigns.page + 1) |> fetch()}
end

defp fetch(%{assigns: %{page: page, per_page: per_page}} = socket) do
  images = Something.paginate_images(page: page, per_page: per_page)
  assign(socket, images: images)
end

js file for observing scroll events

// assets/js/infinite_scroll.js
export const InfiniteScroll = {
  page() {return this.el.dataset.page;},
  loadMore(entries) {
    const target = entries[0];
    if (target.isIntersecting && this.pending == this.page()) {
      this.pending = this.page() + 1;
      this.pushEvent("load-more", {});
    }
  },
  mounted() {
    this.pending = this.page();
    this.observer = new IntersectionObserver(
      (entries) => this.loadMore(entries),
      {
        root: null, // window by default
        rootMargin: "0px",
        threshold: 1.0,
      }
    );
    this.observer.observe(this.el);
  },
  beforeDestroy() {
    this.observer.unobserve(this.el);
  },
  updated() {
    this.pending = this.page();
  },
};

added hooks

// assets/js/app.js
...
import {InfiniteScroll} from "./infinite_scroll"

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {hooks: {InfiniteScroll}, params: {_csrf_token: csrfToken}})
...

Am I missing something? I’m not sure why child elements were replaced instead of adding them down.

1 Like

Just as a sanity check, is this a typo and you actually have phx-update="append"?

If you want the append to happen on the list of images, surely you’re going to need the hook on that element?

ala this ElixirCasts code

<tbody id="infinite-scroll" phx-hook="InfiniteScroll" phx-update="append" data-page={@page}>
  <%= for album <- @albums do %>
    <tr id="...">
      <td>...</td>
    </tr>
  <% end %>
</tbody>

Thanks @cmo for pointing that out, I’ve updated it above (phx-update="append"). It is still replacing instead of appending

Another one: Are you sure id’s are unique? Afaik replacing is expected for matching ids.

yes, it is unique. The example above is using intersection observer. If I’m using scrollTop/Height:

  let scrollTop = document.documentElement.scrollTop || document.body.scrollTop
  let scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight
  let clientHeight = document.documentElement.clientHeight

  return scrollTop / (scrollHeight - clientHeight) * 100

it seems working. I have no idea what’s missing with using intersection observer. Also I just realized, they deprecate beforeDestroy phx-hook callback.

I think I’ve got it. the phx-update="append" should be on the div outside of the for loop.

<div class="grid grid-cols-3 auto-rows-[32rem] gap-2" phx-update="append">
  <%= for image <- @images do %>
    <div id={"image-#{image.slug}"}

hi @lubien

Thanks for replying :pray:. I already moved phx-update attribute outside (above and below the for list comprehension.), and it’s still the same thing.

The issue appears when I’m using intersection observer api, beyond that it’s working (or maybe im using it wrong).

It seems working now, I’m basing on this article:

<div id="infinite-scroll-body" phx-update="append">
  <div class="grid grid-cols-3 auto-rows-[32rem] gap-2">
    <%= for image <- @images do %>
      <div id={"image-#{image.slug}"}>
        ...
      </div>
    <% end %>
  </div>
</div>
<div id="infinite-scroll-marker" phx-hook="InfiniteScroll" data-page={@page}></div>
...
  destroyed() {
    this.observer.unobserve(this.el);
  },
  updated() {
    this.pending = this.page();
  },
...

My guess is that the div tag where phx-hook resides should be at the bottom (target.isIntersecting) and not combining it with phx-update attribute above.

1 Like