iTranslated by AI
Asynchronous Pagination with Rails and Stimulus.js
In older code, we used to hook into callbacks like $('#paginator').on 'ajax:success',...
and we would rewrite HTML using js.erb responses like $('#pagenate').html("<%= escape_javascript(render 'index_page') %>");.
In this article, I will explain how to implement this using Stimulus.js. We will use the Kaminari library for pagination.
We will modify the pagination in the following template to perform asynchronously. Since I am not providing the source code, please note that any mysterious fragments appearing are simply due to the difficulty of generalizing the code and are intentional specifications.
<h1>index page</h1>
<%= paginate @video_chats, remote: true %>
<% @video_chats.each do |video_chat| %>
<div>
<%= video_chat.text %>
</div>
<% end %>
--
The overall process involves invoking an asynchronous request and rewriting the HTML from a Stimulus.js controller. The Stimulus.js controller is as follows:
import { Controller } from "@hotwired/stimulus";
// Connects to data-controller="video-chats-loader"
export default class extends Controller {
static values = { url: String, keyword: String };
connect() {
this.load();
}
load(page) {
this.element.innerHTML = 'Loading...';
let url = this.urlValue;
if (page) {
url += `${url.includes("?") ? "&" : "?"}page=${page}`;
}
if (this.keywordValue) {
url += `${url.includes("?") ? "&" : "?"}keyword=${encodeURIComponent(
this.keywordValue
)}`;
}
fetch(url)
.then((response) => response.text())
.then((html) => (this.element.innerHTML = html));
}
}
To modify the HTML, we extract the <% @video_chats.each do |video_chat| %>... part into a partial and declare the data-controller.
<h1>index page</h1>
<div
data-controller="video-chats-loader"
data-video-chats-loader-keyword-value="<%= @keyword %>"
data-video-chats-loader-url-value="<%= video_chats_video_path(@video, format: :html) %>"></div>
This side is the partial. The key is params: { format: :js }. By overriding the format, it requests a JS template when the pagination link is clicked.
<%= paginate video_chats, remote: true, params: { format: :js } %>
<% video_chats.each do |video_chat| %>
<div>
<%= video_chat.text %>
</div>
<% end %>
The controller that returns the pagination response is as follows:
def video_chats
respond_to do |format|
format.js
format.html do
keyword = params[:keyword]
video = Video.find(params[:id])
video_chats = video.video_chats.order('timestamp').page(params[:page]).per(60)
video_chats = (video_chats.where('message LIKE ?', "%#{keyword}%") if keyword.present?)
render partial: 'videos/video_chats', locals: { video_chats: }
end
end
end
The template returned by format.js is as follows. It invokes the Stimulus.js controller.
var page = <%= params[:page] || 1 %>
var element = document.querySelector("[data-controller='video-chats-loader']")
var controller = window.Stimulus.getControllerForElementAndIdentifier(element, "video-chats-loader")
controller.load(page)
By connecting these, clicking the hyperlinks labeled 1 2 3 last will cause only this part to be rewritten asynchronously.
Impressions
Compared to the old implementation mentioned at the beginning, this is better because DOM rewriting is encapsulated within the controller. However, I feel it is slightly awkward to go through format.js just to invoke the Stimulus.js controller.
Also, the code is difficult to trace. When a partial is rendered indirectly from a Stimulus.js component, it is usually hard to read.
Let's just use Turbo instead!!!!!!
References
- https://stimulus.hotwired.dev/handbook/working-with-external-resources
- https://www.stimulus-components.com/docs/stimulus-content-loader
That's all.
Discussion