Check out my consultancy offering at RailsReviews.com!

InfiniteScrollReflex

Successively add HTML fragments to the DOM on demand

How?

  • CableReady is used to insert a new set of items using insert_adjacent_html before a #sentinel element after a “Load more” button is clicked.
  • A scoped page morph is used to determine the next page and hides the “Load more” button when the last page is reached.

Caveat

Note that in a real-world app, you’d probably want to use model partials and collection rendering instead of inline rendering the items.

Variations

  • Use a Stimulus controller and an IntersectionObserver to automatically trigger loading:
import ApplicationController from "./application_controller";
import { useIntersection } from "stimulus-use";

export default class extends ApplicationController {
  static targets = ["button"];

  connect() {
    super.connect();
    useIntersection(this, { element: this.buttonTarget });
  }

  appear() {
    this.buttonTarget.disabled = true;
    this.buttonTarget.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';

    this.stimulate("ArticlesInfiniteScroll#load_more", this.buttonTarget);
  }
}
  • This example uses Pagy for pagination, but of course you could also just use .limit and .offset or any other pagination method.

app/reflexes/infinite_scroll_reflex.rb

class InfiniteScrollReflex < ApplicationReflex
  include Pagy::Backend

  attr_reader :collection

  def load_more
    cable_ready.insert_adjacent_html(
      selector: selector,
      html: render collection,
      position: position
    )
  end

  def page
    element.dataset.next_page
  end

  def position
    "beforebegin"
  end

  def selector
    raise NotImplementedError
  end
end

app/reflexes/articles_infinite_scroll_reflex.rb

class ArticlesInfiniteScrollReflex < InfiniteScrollReflex
  def load_more
    @pagy, @collection = pagy Article.all, page: page

    super
  end

  def selector
    "#sentinel"
  end
end

app/controllers/articles_controller.rb

class ArticlesController < ApplicationController
  def index
    @pagy, @collection = pagy Article.all unless @stimulus_reflex
  end
end

app/models/article.rb

class Article < ApplicationRecord
  has_one_attached :image
end

app/views/articles/index.html.erb

<ul>
  <%= render @collection %>
  <li id="sentinel" class="hidden"></li>
</ul>

<div id="load-more">
  <% if @pagy.page < @pagy.last %>
    <button
      data-reflex="click->InfiniteScroll#load_more"
      data-next-page="<%= @pagy.page + 1 %>"
      data-reflex-root="#load-more">
      Load more
    </button>
  <% end %>
</div>

app/views/articles/_article.html.erb

<li>
  <%= image_tag article.image %>
</li>