Recently I needed to add infinite scroll to a list view on a Rails app I’m working on. It turned out to be pretty straightforward, so here are the steps I took to get it working.
This tutorial assumes use of a default Rails 5.1.4 installation, with the addition of haml and jquery-rails. This means Sprockets, coffee, and turbolinks are in use. However most of the following code is trivial to convert if you aren’t using these helpers. I’m also referencing a Post model, but again, if you’re using a different class all you need to do is replace any reference to Post with your class.
You can view a demo app made using this tutorial here, and you can also clone it on github.
First you’ll need to add the will_paginate gem to your gemfile, and include the jquery.inview plugin (put this in the vendor/assets/javascripts folder).
Gemfile
application.js
After that, add a per_page parameter to your model. For example, in post.rb:
Then, in your controller, replace the query with a paginated query, and add format.js to the format response. I’ve included an example showing how to add pagination to the index view, and also how to add it to a scoped query. For example, in posts_controller.rb:
You’ll need a partial that renders your items. For simplicity, I’ve created a shared partial called _posts.html.haml in the shared folder:
Edit your template and add a Load More link. For example, in index.html.haml. We’ve included a test to see if there actually is a next page of content, if not – don’t display the Load More link.
Then create your js template, index.js.haml. Notice that the same next_page method is called on the posts to hide the Load More link when there’s no more content.
And finally add the javascript to load posts automatically when the Load More link is in view, in posts.coffee
And you’ll be good to go! This is all you need to do to add pagination with infinite scroll. If you have any problems, check the demo app and see if you can find any differences.
If this post has helped you out, feel free to consider throwing a small donation my way.
Comments are closed.
64 comments
thanks helped me a lot
Very nice. Just what I was looking for, without having to use an infinite scrolling gem. By the way, you should update your README.md on your github (https://github.com/levymetal/infinite-scroll) to fix the broken link.
Nice spot, thanks for letting me know!
No problem! Thanks to your posts clarity I got this working really quickly. Any advice on paginating by association? I have products and categories (a many-to-many association using has_many :through). Everything’s perfect on the “all products” page, but when I get only products from a certain category with something like this: Product.where(:id => @category.product_ids).paginate(:page => params[:page]) … the page continues to load all products up to the total amount in the DB. But the initial ActiveRecord relation ( for Product.where(:id => @category.product_ids) ) is accurate. Any ideas?
Hmm, that should work, will_paginate definitely supports it. Have you got a github repo I can clone to have a look? Or can you clone my repo and make an quick demo?
Sure thing. Here’s the demo repo: https://github.com/batmanbury/infinite_scroll_demo
Don’t forget to rake db:seed after all the other setup to get the test products and categories.
Thanks for looking! I’ll check back here for any ideas you’ve got.
Very strange, it appears to be working properly for me: https://www.youtube.com/watch?v=D-hjn02QRe4 (I changed the constant to 2). Could be something in your environment?
Are you sure it was really working? I’d love to fix this. I tried changing the per_page constant to 2, and other values, but that didn’t to the trick. I think if you’d continue scrolling, you’d see the same thing. The infinite scrolling itself is working, like in your video, but that wasn’t the problem. It just continues to load and load, when there should only be something like 24 results. Instead it always goes to 100. Hmmm…frustrating. Thanks for looking though.
You’re right, it wasn’t working properly. The problem is in _product_listing.html.erb. Your “Load More” link is pointing to the products controller, not the category controller. So on the first lazy load, it just goes and loads all products. You’d want something like this:
category_path(:category => @category, :page => products.next_page)
You’d also need to change it in the categories/show.js.erb view as well.
Ah of course! Thanks for spotting that. All things considered, getting this tutorial plus the question answered, I’d like to send something your way. I see you have a donation link. Or would you prefer Bitcoin?
Thanks very much mate. Please don’t feel obliged to; I’d never ask or expect anything. This blog is for the community! But if you’d still like to throw a few coins my way, Paypal is great!
You bet. Enjoy a beer on me!
Thanks heaps mate! Have a great week!
Great tutorial, just one edit for anyone struggling to refresh the a href using the index.js.erb file. Try using “.attr(‘href’, ‘ @posts.next_page) %>’);” instead of “.attr(‘href’, ‘#{posts_path page: @posts.next_page}’);” You need to make sure you escape javascript first.
Thanks for the comment. I’m just wondering, why would you need to escape posts_path? It returns a URL, which means it would never contain any escapable characters (you can check the characters here: http://api.rubyonrails.org/classes/ActionView/Helpers/JavaScriptHelper.html). Can you provide a bit more explanation as to why this is necessary?
Hi Christian, sorry I should have phrased that better. It makes no sense but when I don’t escape javascript the button keeps looping through the pages continuously without ending and removing itself on the last page. I’m currently using rails 4. Hope that helps.
Are you sure it has to do with escaping? If you’re using ERB instead of HAML, #{posts_path page: @posts.next_page} won’t work – you need to use ERB tags: . However, you shouldn’t need to use the J tag as well. It should just work with @posts.next_page %>.
Sorry Christian, I feel like an idiot. Thats exactly what it was, I had mistook the index.js.haml for normal ERB instead of HAML. Apologies for wasting your time.
Haha no need to apologise, we both learnt something which is always a success!
Hello Cristian, this works out of the box, thanks a lot.
Just one thing, when infinite scrolling, it will only reload the first time. The second request wont work. I have a model called photos instead of posts, do I need to change something in my coffeescript? Thanks!
Enrique, it’s very hard to say what the problem is without seeing any code. You’ll see that I reference the Post model a lot in my code, so you need to change all the references from Post to Photo. If you can link me to your project in github, or send me to a URL where I can see your app in action, perhaps I’ll be able to find the issue.
Hello Cristian, here is my code.
Index.js.haml
$(‘.photos’).append(‘#{escape_javascript(render partial: “spotlist”, :locals => { :photos => @photos })}’);
$(‘a.load-more-spots’).attr(‘href’, ‘#{spots_ultimos_path page: @photos.next_page}’);
– unless @photos.next_page
$(‘a.load-more-spots’).remove();
spots_infinite_scroll.js.coffee
$ ->
loading_posts = false
$(‘a.load-more-spots’).on ‘inview’, (e, visible) ->
return if loading_posts or not visible
loading_posts = true
$.getScript $(this).attr(‘href’), ->
loading_posts = false
View:
@photos.next_page), :class => ‘load-more-spots’, :remote => true if @photos.next_page %>
•
Reply
•
Share ›
Hello Cristian, here is my code.
Index.js.haml
$(‘.photos’).append(‘#{escape_javascript(render partial: “spotlist”, :locals => { :photos => @photos })}’);
$(‘a.load-more-spots’).attr(‘href’, ‘#{spots_ultimos_path page: @photos.next_page}’);
– unless @photos.next_page
$(‘a.load-more-spots’).remove();
spots_infinite_scroll.js.coffee
$ ->
loading_posts = false
$(‘a.load-more-spots’).on ‘inview’, (e, visible) ->
return if loading_posts or not visible
loading_posts = true
$.getScript $(this).attr(‘href’), ->
loading_posts = false
View:
@photos.next_page), :class => ‘load-more-spots’, :remote => true if @photos.next_page %>
Enrique, there’s a million factors that could be causing this your issue. Sending me a couple of bits of code doesn’t help me debug it (unless there was a very obvious syntax error, which you’d already know about). Those bits of code don’t tell me what the rest of your app is doing, they don’t tell me what your CSS is doing, they don’t tell me if you’ve got any other interfering scripts, and I can’t even see the bug in action. That’s why I asked you to link me to the repo, so I can see ALL the code, and link me to a URL, so I can see the bug in action instead of trying to figure it out based on your comments. There’s only one way that I can help you, and that’s if you link me to a copy of your repo, AND you link me to a URL where I can see the app working in action. I hate to sound rude, but if you can’t do both of these things, I will not be able to help you.
Sorry Christian for that. The problem of reloading was a CSS issue i think, I’ve cleaned every CSS class and it works fine.
I have another problem (bigger one).
The thing is, my Photo Controller Actions (latest photos, zone, category….) all use the :indextemplate -> They use render :index
Thats a problem, because you use static routes in your tutorial:
In index.js file:
$(‘a.load-more-spots’).attr(‘href’, ‘#{spots_ultimos_path page: @photos.next_page}’);
In view:
@photos.next_page), :class => ‘load-more-spots’, :remote => true if @photos.next_page %>
Is there a way to have dynamic routes so I can use different routes within the same call?
Something like:
$(‘a.load-more-spots’).attr(‘href’, ‘#{DYNAMIC_PATH page: @photos.next_page}’);
@photos.next_page), :class => ‘load-more-spots’, :remote => true if @photos.next_page %>
I see what you mean, my code refers to one specific path (posts_path). I guess what you want is the current path. So I googled that exactly: rails current_path. The very first result linked to an example using url_for. I didn’t nut out exactly how to use it in your situation, but I’m 99% sure this is what you need. Obviously I’m not going to write the solution for you, and neither do I know the answer anyway, but this should point you in the correct direction. If it still doesn’t work, head over to Stack Overflow and ask a new question there.
this breaks my javascript on scroll in rails 4. I have ajax comments in the index view, it works on existing content but not the new content that autoloads on scroll
This is the most common problem with loading content dynamically. You have a bit of javascript that initialises your comments on page load. This script runs once, when the page loads. This means that when you load more content dynamically, like my post describes, you have to run your comment initialiser function again on the new content. Obviously, without seeing your code or knowing what gem you’re using for ajax comments, I really can’t tell you where to look. I would recommend looking through the docs for your gem to see what you need to do to get it to work for dynamically loaded content.
I followed this guide http://twocentstudios.com/blog/2012/11/15/simple-ajax-comments-with-rails/ I’m using rails 4, I have a comments.js.coffee in my js assets, How would i reinitialize it?
That’s a pretty big post, and again without seeing your code, I can’t really say for sure how to fix it. You might be able to get away with simply delegating the events in your comments.js.coffee file. I’ve made a gist here with some updated code: https://gist.github.com/levymetal/d8d9fc71d5eee3abd8cf, and you can learn about delegated events here: http://api.jquery.com/on/#direct-and-delegated-events. That should get the comment form working (assuming I got the code right; I haven’t tested it), but I don’t know if you’ve any other javascript that needs to be run when more content is loaded.
Basically I need to know how to renintialize a js file in the javascript assets path, also, the $(document).on ‘ready page:load’, -> for rails 4 doesn’t work for some reason, after I go back to the page with turbolinks, the scroll doesn’t work. Only on refresh
EDIT:
Never mind, this worked ‘$(document).on ‘ready, page:load’, ->’ comma after ready
I’m using ‘$(document).on ‘ready page:load’, ->’ in one of my Rails 4 projects and it works fine, and jQuery definitely doesn’t require a comma between events (see here). So I’m not sure why it didn’t work for you. However, I like your point of using page:change instead of page:load, that’s definitely the correct way to use it, so I’ll update my post.
Unfortunately without seeing your project, I just can’t help you out any further. There isn’t a blanket fix that you can copy & paste into your project that will make infinite scroll work with your ajax comments. You need to find the functions that initialise your ajax comments, and you need to run those functions when the new content is loaded, OR use delegated events like in my previous comment.
Forgot to come here and reply, the gist worked. Thank so much. You’re awesome man 🙂
Same code without haml and coffee please!
I did a quick Google search and within 2 minutes I found services to convert HAML to ERB and Coffee to pure JS (hint: you can convert coffee directly on the try tab of http://coffeescript.org/). I’m not your personal coding assistant; please at least make an attempt to convert it yourself before asking me to do work for you for free!
yup 😉 I burned that server converting …hehe
hey christian really good tutorial, just in case for the guys who have problems with the duplicated requests when you are scrolling Im using the code shared in this tutorial for showing tumblr posts but I noticed that sometimes the callback launches the same requests two times so I wrote the following code:
$ ->
isProcesing = false
$(‘a.load-more-posts’).on ‘inview’, (e, visible) ->
return unless visible
if isProcesing
return
isProcesing = true
$.getScript $(@).attr(‘href’), (data, textStatus, jqxhr) ->
isProcesing = false
return
Hi Jose,
Thanks for your comment. You’re right, the function may get called twice if the user scrolls before the content has been loaded (this may trigger the inview event multiple times). The formatting in your comment didn’t come out too well (disqus doesn’t like to format code), but your suggestion is very useful so I have updated the tutorial with an a modified version of your code that will prevent the event from firing multiple times.
Thanks for the input!
I’ made rails app from scratch and following your tutorial
I’m getting the reference error visible is not defined, from the posts.js.coffee , I have included jquery.inview plugin. What elsce can be wrong? Your cloned repo works just fine.
If you’re getting an error on the inview event, it definitely means the plugin is not included correctly. Make sure that the plugin is in the correct location (check your console to make sure the plugin is actually being loaded or if it’s following a broken link), and make sure you are binding the event on jQuery.ready(). You can also try to ensure that the jquery.inview plugin is included before any files that make a call to it, but that shouldn’t matter if they are bound within jQuery.ready().
If this still doesn’t work, compare every single line of my code in this blog post to your code, and you should find the problem. If you do everything exactly the same as I do in this post, it will work.
If this still doesn’t help, give me access to the repo and I’ll see if I can find out why it’s not working.
Hello Chrisitian,
I’ve followed all your instructions but infinite scroll not working. Posts show only after click on the `Load more posts` link. Do you think what could be the cause of the problem? I’m using Rails 4, but I cloned your repository and started it with my Gemfile and everything worked anyway.
Thanks!
I solved this issue. The problem was in ‘posts’ class. It was just my silly mistake
Hi Christian, thank you so much! That’s exactly what I need.
Hi Christian,
Just wanted to let you know that this was very helpful, so thanks a bunch!
I find that HAML makes my life a bit easier, but I wouldn’t go out of my way to use it (for example, I don’t use it on any non-ruby projects). Having said that, I’m surprised you ditched coffeescript too, I can’t really see an argument against it. If you want you can clone the whole project and convert it, and I’ll add a link to it from the main repo – https://github.com/levymetal/infinite-scroll.
Awesome!
Tks!
jb
Looks like your gists are broken.
Fixed. Thanks!
Hi Christian, I created a brand new rails app to try this out., its not working and I’m getting the reference error visible is not defined, from the posts.js.coffee. I don’t have any other code in it. Could I get some help please?
https://github.com/sherwyngoh/railslazy
Make sure you include the inview plugin in app/assets/javascripts/application.js (as per the second snippet): //= require jquery.inview.min.js
I haven’t checked the rest of the code, but if this is all that’s missing, then it should work.
hey Christian Varga
all the previous problem were solved.
just one concern
when i created another file like, pictures file, and adding “load more photos” link and doing other modifications, it doesnt load nything. when i checked the rails server log, it was not loading pictures.js.haml and pictures.js.coffee. only index.js.haml and post.js.coffee.
all files are in one folder, post(except the shared ones)
do i have to include everything in one file??
There are just too many variables in your question for me to be able to provide a useful answer. In short, the answer is no; it does not have to all be in one file. It all depends on how you set up your controller. The name of the file needs to be the same as the method that calls it in the controller. For example, I created a file named index.js.haml because the index method is the one that utilises infinite scroll. But without seeing your code, I really can’t tell you what the problem is. I’d recommend reading up on how controllers work to understand this more, the rails guides provide good information: http://guides.rubyonrails.org/action_controller_overview.html#methods-and-actions
your tutorial seems very easy and lightweight than others.
i have an issue though. i m not good with haml. i tried googling for haml to erb, but wasnt able to find nything. so can u pls help me with it. specifically for both js files.
and also in which folder are the index.js.haml and posts.js.coffee suppose to be placed. posts folder or app> assets> javascripts folder??
hey Christian Varga
it seems, after loading the haml gem, it kind of acts wierd. on clicking load more posts, in addition to next 10 results, it shows “load more post” link and below it next 10 results are also shown. so the link is kind of sandwiched between the genuine next 10 results and the results of page 3, which shud only appear on clicking the link second time.
it is like
-first 10 results on page load-
-load more post .. click
-first 10 results on page load-
-next 10 results on link click-
load more post
-10 more results-
index.js.haml should be placed in /app/views/posts. posts.js.coffee should be placed in /app/assets/javascripts. My code is very basic; haml is just abstracting away the HTML. Create some gists on github if you’re still having trouble and I can show you how to modify them to erb, otherwise you can just include the haml gem.
I stumbled on this post randomly Googling, but as someone who’s new to Rails and was stuck on this exact issue, it was an absolute lifesaver. Thanks for posting this.
Infinite scroll and the browser back button is a tough one. We’re loading content in dynamically, yet the browser back button doesn’t store what was in the browsers memory to take you back; it performs a hard refresh to the last URL and simply tries to scroll to the last position on the document. This doesn’t work on an infinite page, because the hard refresh only loads the first set of content.
Twitter have figured out a way to handle it; Facebook haven’t (Facebook won’t take you back to the last position if you let it load in a few sets of content). So some people don’t even think it’s that important. But I’ll admit it’s a pretty poor user experience.
It depends how important you think this really is, the best way would be to utilise the HTML5 History API and a bit of clever javascript. You could load in the single/show post view via javascript into a separate content div, keeping the list view in the DOM but simply hiding it with CSS. You can change the browser’s URL with the HTML5 history API, and then you could bind an event to the browsers back button to prevent it from hard-reloading the last page, and instead simply show the list view again and hide the content div.
The specific issue I was having was with browsers caching the Javascript in posts.js and as such not registering the ‘inview’ event on cached pages, which I solved by just moving the script into the HTML doc. I imagine there’s a more elegant solution, but it was the first thing I tried that worked. All of my previous development experience has been native apps with C++, Java or Python, and I think this (caching problems) was one of those web gotchas I just had to run into for myself.
I toyed around briefly with the history API and URL modifications, but I had trouble getting it to play nice and I think accomplishing something useful with how little I know about Javascript right now would probably take significant learning. It’s definitely something I’d come back to, though. In any case, I appreciate the reply, and the tutorial was immensely useful regardless.
Great article! Can someone assist me with an error? “SyntaxError: unexpected -> ” .
this looks like coffe script. if you are not using it or haven’t added the .coffee extension it will look for normal js.
Good! Thank you!
Thanks brother, I really like this and I’m going to test it