Infinite Scroll - 2/2
The second and final part of the Infinite Scroll Serie, with a new range of (lovely) T-Shirts!
Now with automatic loading on scroll
, a new trick and amazing loading animations
!
So... how does it work?
Let's focus on two aspects. But first, did you scroll
all the way to the end?
You might have missed some important details about performance... 🍿
Morphin' Trick
This trick is very similar to the one used in the
Part 1/2
of this "Infinite Scroll" demo with LiveComponent.
Previous Results
🐯 For the previous page results, we add one fake item
corresponding to the last item of the previous page,
with the same id
. This allows the addition of new elements after the existing ones.
1<div class="ProductGrid" {{ attributes.defaults(stimulus_controller('appear')) }}>
2
3 <div id="results" style="display: flex; gap: 1rem; flex-direction: column;" class="p-4">
4 <div class="ProductGrid_items">
5
6 {# 🐯- Last result from previous page #}
7 {% if page > 1 %}
8 <article id="item--{{ page - 1 }}-{{ per_page }}"></article>
9 {% endif %}
Current Page Results
🦊 For the current page results, we use both id
and data-live-ignore
.
This allows the conservation of previous elements in the DOM.
11 {# 🦊 - Current page #}
12 {% for item in this.items %}
13 <article
14 id="item--{{ page }}-{{ loop.index }}"
15 class="ProductGrid_item"
16 data-live-ignore
17 style="--i: {{ item.id }};"
18 >
19 <div class="ProductGrid_media">
Next Page
🐼 Finally, for the next page results, we add placeholders with an id
.
This, combined with the "previous page" trick, force the order of insertion of the next elements
... with no div in between!
31 {# 🐼 - Next page #}
32 {% for i in 1..per_page %}
33 <article id="item--{{ page + 1 }}-{{ i }}"
34 class="ProductGrid_item"
35 style="--i: {{ (page * per_page) + i - 1 }};"
Intersection Observer
We want to load the next results automatically when the user scrolls to the bottom of the page.
We create a small Stimulus controller that leverages the IntersectionObserver
API
to trigger a custom appear
event when its target element becomes visible in the viewport.
By making a dedicated controller, we can keep the logic separate from the product grid. And we can reuse it in other components!
import { Controller } from '@hotwired/stimulus';
/* stimulusFetch: 'lazy' */
export default class extends Controller {
static targets = ['loader'];
loaderTargetConnected(element) {
this.observer ??= new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.dispatchEvent(new CustomEvent('appear', {detail: {entry}}));
}
});
});
this.observer?.observe(element);
}
loaderTargetDisconnected(element) {
this.observer?.unobserve(element);
}
}
LiveComponent + Stimulus
We now add the appear
controller to our ProductGrid
component, thanks to the stimulus_controller
method.
1<div class="ProductGrid" {{ attributes.defaults(stimulus_controller('appear')) }}>
2
3 <div id="results" style="display: flex; gap: 1rem; flex-direction: column;" class="p-4">
Event -> LiveAction
Finally, we add the loader
target to the first "item" placeholder we added for the next page.
🦁 And we configure the appear
event to call the more
action of the live controller
, when
the event is triggered.
Doing so, we can load the next page when the first item of the next page is visible.
With no need for a "Load More" button!
32 {% for i in 1..per_page %}
33 <article id="item--{{ page + 1 }}-{{ i }}"
34 class="ProductGrid_item"
35 style="--i: {{ (page * per_page) + i - 1 }};"
36
37 {# 🦁 - The trigger #}
38 {% if loop.first %}
39 data-appear-target="loader"
40 data-action="appear->live#action"
41 data-live-action-param="debounce(750)|more"
42 {% endif %}
43
44 >
45 <div class="ProductGrid_media">