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?
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. 👍
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.
This rocks, coupled it with my MessagesController for Group Chats to reduce some of the load for the larger chats with lots of messages.
So excited to see this episode. Saw it pop up on your Github a while back, and was excited to see the video.
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/
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.
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.
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
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
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
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
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
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!
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.
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
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!
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.
I'm trying to implement multiple infinite scroll containers in a single page and having trouble. Anyone able do this successfully?
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)
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.