Infinite Scroll with HOTWire - Part 2: Adding Stimulus 
Tags: rails, ruby, hotwire, javascript
Published: 2021-10-17 19:51:56

You can follow along if you missed the first part.
{% post https://dev.to/rockwell/infinite-scroll-with-hotwire-part-1-configuration-4n32 %}

## 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`

```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](https://github.com/rails/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.

```js
  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.

```erb
<div
  data-controller="pagination"
  data-pagination-url-value="<%= posts_url %> "
  data-pagination-page-value="<%= 2 %>">
  <%= render @posts %>
</div>
```

<img width="100%" style="width:100%" src="https://media.giphy.com/media/d7zdaTxxYYpIZWVQnu/giphy.gif">

## 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

```erb
<%= 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.

```erb
# 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.

<img width="100%" style="width:100%" src="https://media.giphy.com/media/ueCLJjJtJ6fBaj5cIN/giphy.gif">.

## 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

```erb
<%= 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.

```js
  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. 

```erb
  <% 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 

```js
   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

```erb
<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

<img width="100%" style="width:100%" src="https://media.giphy.com/media/PRznalMcfwSDfvsZ7W/giphy.gif">

## 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](https://github.com/A7madXatab/hotwire-infinite-scroll)

Links
- [request.js](https://github.com/rails/request.js)
- [MDN URL docs] (https://developer.mozilla.org/en-US/docs/Web/API/URL)