Ask A Question

Notifications

You’re not receiving notifications from this thread.

Infinite Scroll in Rails with Stimulus.js Discussion

This is super slick. I wonder how easily one could append the query parameter in the URL to reflect the current page? Could History.replaceState() handle the job?

Reply

Should be pretty easy. Once the AJAX request succeeds, you can update the URL with a pushState. I would imagine you'd want pushState so you don't clobber the previous URL and it would keep the history as if you clicked each link. 👍

Reply

Can you tell me how to do it? i have implemented your infinite scrolling method but can't go back to same scroll position after coming back to the same page again. Tell me the solution. I really need to solve this issue.

Reply

This rocks, coupled it with my MessagesController for Group Chats to reduce some of the load for the larger chats with lots of messages.

Reply

So excited to see this episode. Saw it pop up on your Github a while back, and was excited to see the video.

Reply

Thank you for this.

Reply

Thanks @excid3, very neat :-) One comment though, as capturing scroll events like this is usually quite CPU-intensive, wouldn't it be better to use an IntersectionObserver? (https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver)

also see: https://m.signalvnoise.com/how-to-back-to-top-button-without-scroll-events/

Reply

I bet this could be a great way to improve that. 👍

Reply

Great episode. And a nice introduction to Stimulus.js to boot. One small comment, you may want to wrap the pagination area in a "display: none;" so it doesn't flash on the screen and isn't present when you hit the bottom.

Reply

Yup, definitely want to hide that navigation if you're using this for real. :)

Reply

For anyone implementing infinite scrolling behavior, I recommend checking out the method used by https://infinite-scroll.com

Instead of using JSON it uses regular HTML pages like you're already used to. No need to modify the controller or create a separate view template. Instead, they fetch the full http://localhost:3000/?page=2 (or whatever), but only use the content inside <div data-target="infinite-scroll.entries"> and append that to the existing div.

I thought it was a clever approach that feels very familiar as it reminds me of Turbolinks. There's very little setup and easy to maintain. With proper caching any performance hit is likely negligible.

I can see the strategy being used with only a slight modification of the code presented in the video.

Reply

So useful! I'm using this on my current project now..

Reply

Good tutorial Chris!
at 10:25 I am getting bellow error from console.log
Any idea why this is happening, please?

Error invoking action "scroll@window->infinite-scroll#scroll"
TypeError: "this.paginationTarget.querySelector(...) is null"
scroll infinite_scroll_controller.js:9
invokeWithEvent binding.js:52
handleEvent binding.js:29
handleEvent event_listener.js:30

infinite_scroll_controller.js

import { Controller } from "stimulus"
export default class extends Controller {
static targets = ["entries", "pagination"]

scroll() {

let url = this.paginationTarget.querySelector("a[rel='next']").href

var body = document.body,

html = document.documentElement
var height = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight)

if (window.pageYOffset >= height - window.innerHeight - 100) {
this.loadMore(url)
}

}

loadMore(url) {

    Rails.ajax({
  type: 'GET',
  url: url,
  dataType: 'json',
  success: (data) => {

console.log(data)
}
})

}
}

index.html.erb

<%= render "posts" %>
<%== pagy_nav @pagy %>
Reply

fyi for those still around, I solved a similar issue by following the instructions here, specifically the second part of the answer (afaik ujs is now required and started in application.js by default): https://stackoverflow.com/questions/56128114/using-rails-ujs-in-js-modules-rails-6-with-webpacker

Reply

Thank you Chris, I just implemented infinite scroll on my web site.

When I open the page for the first time, I get this error "MediaController#index is missing a template for this request format and variant. request.formats: ["text/html", "text/html"] request.variant: []"

But when I refresh the page, It works right away. I couldn't find out the reason. Could you please help me. Thanks

Reply

I'm trying to implement a reverse-direction infinite scroll (infinite scroll up) using the refactored code. Everything seems to be working so far except that the scroll bar is jumping to the top of the prepend rather than staying at the same scroll position. I tried to implement the suggestions at the below link but with no luck. Any suggestions?

https://stackoverflow.com/questions/5688362/how-to-prevent-scrolling-on-prepend

Reply

I don't get the pageYOffset on my console when I test. They are no errors displayed.

scroll(){
console.log(window.pageYOffset);
}
}

div data-controller="infinite" data-action="scroll@window->infinte#scroll"

my stimulus controller is infinite_controller.js

Reply

This is awesome Chris! I noticed that on mobile browsers the scroll handler is called multiple times, resulting in multiple network requests and several duplicate pages being loaded and appended. If anyone else runs into this, adding a simple loading state to the controller can fix this:

export default class extends Controller {
  static targets = ['entries', 'pagination'];

  initialize() {
    this.loading = false;
  }

  scroll() {
    const nextPage = this.paginationTarget.querySelector("a[rel='next']");

    if (nextPage == null) return;

    const url = nextPage.href;
    const body = document.body;
    const html = document.documentElement;
    const height = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight);

    if (window.pageYOffset >= height - window.innerHeight - 500) {
      if (this.loading) return;

      this.loadMore(url);
    }
  }

  loadMore(url) {
    this.loading = true;

    Rails.ajax({
      type: 'GET',
      url,
      dataType: 'json',
      success: data => {
        this.entriesTarget.insertAdjacentHTML('beforeend', data.entries);
        this.paginationTarget.innerHTML = data.pagination;
        this.loading = false;
      }
    });
  }
}

Just create a global this.loading variable on initialize and set to true in the loadMore method, then set to false after a successful load and voila!

Reply

I've tried this and it improves the situation but I'm still getting duplicates loading.

I think the "de-bounce" needs to be more robust but I'm not sure how to achieve it.

Reply

Oh here we go - see the episode on intersection observer api infinite scroll.

Reply

This could also be an issue with a rogue 'import controller' statement magically appearing at the bottom of application.js, which means it's actioning the JS twice

Reply

I'm having trouble migrating my Pagy pagination from Turbolinks to Turbo!

I posted about it here: https://discuss.hotwire.dev/t/pagy-infinite-scroll-and-get-post-requests/2853.

If anybody can help me, that'd be much appreciated. Thank you!

Reply

Answered in the thread I believe, but one of the things that worked for me (temporarily) was disabling turbo globally with Turbo.session.drive = false in application.js and applying it on a link-by-link basis.

Reply

I'm trying to implement multiple infinite scroll containers in a single page and having trouble. Anyone able do this successfully?

Reply

I'm getting this error in console. I've googled it, but can't find any examples, and don't know how to fix it, or even start in diagnosing it. Can anyone point me in the right direction?

env.js:14 Uncaught TypeError: readFileSync is not a function
at Object../node_modules/@rails/webpacker/package/env.js (env.js:14:1)

Reply

So for everyone on Rails7, the Rails.ajax method does not work anymore. At least for me this was the case. Therefore, you can simply switch the infinite_scroll_controller to a fetch method:

loadMore() {
let next_page = this.paginationTarget.querySelector("a[rel='next']")
if (next_page == null) { return }
let url = next_page.href
fetch(url, {
headers: { "Accept": "application/json" }
})
.then(response => response.json())
.then((data) => {
this.entriesTarget.insertAdjacentHTML('beforeend', data.entries)
this.paginationTarget.innerHTML = data.pagination
})
}

This works on mobile without any problems as well.

Reply
Join the discussion
Create an account Log in

Want to stay up-to-date with Ruby on Rails?

Join 86,796+ developers who get early access to new tutorials, screencasts, articles, and more.

    We care about the protection of your data. Read our Privacy Policy.