In my previous blog, I explained how to query Graphql API using Neuron. Throughout the blog, I explained the working of Neuron on page 0
. Whereas, there will be scenarios when users can have multiple blogs paginated on multiple pages. So, we need to add functionality so that users can load multiple pages. This can be achieved easily by adding a button with some text Load More
and adding an event to load
the data on the click. But we will be using a little different approach of using paginating using Infinite Scroll
. This will be a continuation of my previous blog, so let's add this feature.
What is Infinite Scroll?
Before we start, we first need to understand how Infinite scroll works. To get a fair idea about Infinite Scroll, refer to the gif image below.
This is similar to what we see on Youtube, Facebook, Instagram, etc. It keeps user engage in the content and also keep away user from the hassle of clicking the Next
button to load content.
How Infinite Scroll works?
Infinite Scroll logic is very simple, we just have to add a javascript event listener which will listen to the scroll
event. It will get triggered as soon as we reach the bottom (near the bottom) of the page. Below is the logic to detect the bottom of the page.
let scrollAt = () =>
let scrollTop = document.documentElement.scrollTop || document.body.scrollTop
let scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight
let clientHeight = document.documentElement.clientHeight
return scrollTop / (scrollHeight - clientHeight) * 100
}
We just need to call this logic in the proper LiveView lifecycle and this can be achieved with LiveView JS Hooks.
What is JS hook?
Phoenix LiveView often markets itself as that we can create reactive apps without writing JavaScript. But still, there comes a time when you have to write javascript. To integrate Javascript in our LiveView app, LiveView provides us with a concept of LiveView JS Hooks. JS Hooks allow us to register JavaScript lifecycle callbacks on DOM nodes.
How does JS Hook work?
There are different types of lifecycle callbacks like mounted
, reconnected
, and updated
. The mounted
is the first callback that is called and initialized with some initial value as soon as the element on which we are applying it is mounted in the DOM. So, here we will add our addEventListener
and initialize some value. We will create a new file for our hook file in assets/js/infinite_list.js
and add the following code.
let scrollAt = () => {
let scrollTop = document.documentElement.scrollTop || document.body.scrollTop
let scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight
let clientHeight = document.documentElement.clientHeight
return scrollTop / (scrollHeight - clientHeight) * 100
}
InfiniteList = {
page() { return this.el.dataset.page },
mounted(){
this.pending = this.page()
window.addEventListener("scroll", e => {
if(this.pending == this.page() && scrollAt() > 90){
this.pending = this.page() + 1
this.pushEvent("load-blogs", {})
}
})
},
reconnected(){ this.pending = this.page() },
updated(){ this.pending = this.page() }
}
export default InfiniteList
As discussed mounted
callback is a place where we will add eventListener
logic. It detects page bottom and triggers pushEvent
of load-blogs
. This will call handle_event
callback in our lib/fetch_hashnode_web/live/blogs_live.ex
which will have the following code logic.
def handle_event("load-blogs", _params, socket) do
page_no = socket.assigns.page_no + 1
new_blogs = page_no |> Blog.get_blogs
blogs = socket.assigns.blogs ++ new_blogs
socket = assign(socket, blogs: blogs, page_no: page_no)
{:noreply, socket}
end
The logic here is self-explanatory we are fetching blogs using Blog.get_blogs
by passing incremented page_no
to it and assigning the blogs
to the socket
struct.
Adding temporary_assigns
One last change we have to make in the blogs_live.ex
code. We have to use temporary_assigns
for our blogs
field in the mount
callback. When we assign new blogs to socket.assigns.blogs
there are chances that our data can grow, which will increase data in the socket, and ultimately our application performance will be affected. Refer to the following change
def mount(_params, _session, socket) do
blogs = Blog.get_blogs()
socket = assign(socket, blogs: blogs, page_no: 0)
{:ok, socket, temporary_assigns: [blogs: []]}
end
Adding the hook in LiveSocket
Now we will include our InfiniteList
hook in our LiveSocket. For this, we will open the app.js
file and import InfiniteList
and include it in our Hooks object, and add the Hooks to our LiveSocket constructor.
import InfiniteList from "./infinite_list"
let Hooks = { InfiniteList }
....
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}, hooks: Hooks })
// Notice `hooks: Hooks`
Now that we have added our InfiniteList
hook to our hooks list. We will call this hook in our final arrangement.
Calling the Hook
To call our Javascript logic i.e hook we’ll add the phx-hook
attribute on the div
with value as the hook name i.e InfiniteList
where we want to perform scrolling.
<div class="blogs" id="infinite-list" phx-hook="InfiniteList" phx-update="append" data-page={@page_no}>
<%= for blog <- @blogs do %>
// Display logic
<% end %>
</div>
You will also notice the data-page
attribute which has the value of @page_no
. This attribute is the reason, that we were able to access the page
number in the page
method of the InfiniteScroll
hook.
InfiniteList = {
page() { return this.el.dataset.page },
...
The phx-update=append
helps us to append or prepend the updates rather than replacing the existing contents. And don't miss to add the id
attribute as well on the div. So, the following is the list of attributes that we need to apply on the div to enable javascript interactions.
- Hooks (
phx-hook="InfiniteList"
) - phx-update (
phx-update=append
) - data attribute (
data-page={@page_no}
) - id (
id='infinite-list'
)
Conclusion
For performing Infinite scroll we have to
- Create hooks
- Add it to the hooks list in
LiveSocket
object - Add
hook
on the div where you want to perform scrolling so that it can communicate with the javascript.
I hope you like this blog. If you have any questions then please comment below. Thanks for reading 😊.