You can follow along if you missed the first part.
Infinite Scroll with HOTWire Part 1: Configuration
Ahmad khattab ・ Oct 17 '21
Adding Stimulus
Now as our data is ready and we can scroll to the bottom of the screen. We are ready to add a stimulus controller that is responsible for the pagination.
first, create a new file at app/javascript/controllers/pagination_controller.js
// pagination_controller.js
import { Controller } from "stimulus";
export default class extends Controller {
static values = {
url: String,
page: Number,
};
initialize() {
this.scroll = this.scroll.bind(this);
this.pageValue = this.pageValue || 1;
}
connect() {
document.addEventListener("scroll", this.scroll);
}
scroll() {
if (this.scrollReachedEnd) {
this._fetchNewPage()
}
}
async _fetchNewPage() {
// fetch new url
// update new page
// ensure that we are on the last page
}
get scrollReachedEnd() {
const { scrollHeight, scrollTop, clientHeight } = document.documentElement;
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
return distanceFromBottom < 20; // adjust the number 20 yourself
}
}
}
It's a simple controller that attaches a scroll
listener on the element and calls _fetchNewPage
when the scroll has reached the end. Now, let's populate the method body.
request.js is a minimalistic JavaScript pacakge that is set to replace Rails UJS in the near future. We will be using it to fetch new pages from the server. Let's install the package
yarn add @rails/request.js
Adding logic to the method body
What we want to do is that when the _fetchNewPage
method is called, a) request the server the urlValue
and add the pageValue
as a query param.
async _fetchNewPage() {
const url = new URL(this.urlValue);
url.searchParams.set('page', this.pageValue)
await get(url.toString(), {
responseKind: 'turbo-stream'
});
this.pageValue +=1;
}
Let's connect the controller to the dom.
<div
data-controller="pagination"
data-pagination-url-value="<%= posts_url %> "
data-pagination-page-value="<%= 2 %>">
<%= render @posts %>
</div>
Adding tubro_stream responses
The requests made by the scroll is of type "text/vnd.turbo-stream.html"
. So, we'll need to handle that type of request.
create a new file named app/views/posts/index.turbo_stream.erb
and add this code into it
<%= turbo_stream.append "posts" do %>
<%= render @posts %>
<% end %>
with this, add id="posts"
to the div for turbo to know where to append the new posts.
# posts/index.html.erb
<div
id="posts"
data-controller="pagination"
data-pagination-url-value="<%= posts_path %> "
data-pagination-page-value="<%= 2 %>">
<%= render @posts %>
</div>
Let's look at what the controller does now.
When to stop?
Obviously, an scroll should be infinite while there are records to fetch, if there are no more records we must not fetch anymore records. With our current implementation our code would send infinite requests as long the user is scrolling to the end. Let's change that.
Inside app/views/products/index.turbo_stream.erb
add this
<%= turbo_stream.append "posts" do %>
<%= render @posts %>
<% if @posts.page(@page.to_i + 1).out_of_range? %>
<span class="hidden" data-pagination-target="lastPage"></span>
<% end %>
<% end %>
Let's add a little bit code into our stimulus controller.
scroll() {
if (this.scrollReachedEnd && !this.hasLastPageTarget) {
this._fetchNewPage()
}
}
we check, if there is a lastPage
target present, then we stop fetching new page. This would only be true when there are no more pages left.
<% if @posts.page(@page.to_i + 1).out_of_range? %>
<span class="hidden" data-pagination-target="lastPage"></span>
<% end %>
Bonus, add button to load data instead of infinite scroll
Sometimes, you would like only for when a button pressed to load the data, not when the user reaches end of scrolling. Extending the controller is easy, let's perform just that. Inside pagination_controller
add these
static values = {
url: String,
page: Number,
scroll: Boolean
};
connect() {
if(!this.scrollValue) return; // return and don't attach the scroll event listener
document.addEventListener("scroll", this.scroll);
}
async paginate(e) {
await this._fetchNewPage();
e.target.blur();
}
the new scroll
boolean will determine if we should infinite-scroll or not. Change the content of app/views/posts/index.html.erb
to the following
<div
data-controller="pagination"
data-pagination-url-value="<%= posts_url %> "
data-pagination-page-value="<%= 2 %>"
data-pagination-scroll-value="false"
style="overflow-y: scroll">
<div id="posts">
<%= render @posts %>
</div>
<button data-action="click->pagination#paginate">
Load more
</button>
</div>
Now, let's look at the behaviour
Conclusion
We've first created and configured the dependencies and installed them. After that, we introduced our Stimulus pagination controller to aid us to paginate items. Then, we added a target that indicates we are on the last page, to stop the browser from sending infinite useless requests once we are in the last page. Finally, we've added another way to use the controller, that is, by clicking a button the next page shall load.
Thanks for your reading, hope it helps you in a way. Happy coding!
You can also clone the repo here
Links
Top comments (3)
How would you go about paginating from top to bottom? Ie a message chat app where you scroll up to see previous messages
I would add another target, that is the messages container. Then, proceed to modify the
scroll
method to look like thisPS. you might want to scroll the container when it loads. You can do so in the
initialize
methodThanks a lot for your article.
I noticed one issue. It is requesting the same page multiple times. So, let me share my suggestion to solve this issue.