Infinite Scroll - 1/2
Infinite scroll allows users to continuously load content as they scroll down the page.
Part One
of this demo shows how to append new items
to the page with a LiveComponent
.
This component is quite standard: the page number as a LiveProp
, a LiveAction
to load the next page, and a getItems
method to retrieve the page results.
When called, the LiveAction
simply increments the page number and renders again,
displaying the next page of results.
But... how can we keep
the previous results instead of replacing
them?
// ... use statements hidden - click to show
#[AsLiveComponent('ProductGrid')]
class ProductGrid
{
use ComponentToolsTrait;
use DefaultActionTrait;
private const PER_PAGE = 10;
#[LiveProp]
public int $page = 1;
public function __construct(private readonly EmojiCollection $emojis)
{
}
#[LiveAction]
public function more(): void
{
++$this->page;
}
public function hasMore(): bool
{
return \count($this->emojis) > ($this->page * self::PER_PAGE);
}
public function getItems(): array
{
$emojis = $this->emojis->paginate($this->page, self::PER_PAGE);
$colors = $this->getColors();
$items = [];
foreach ($emojis as $i => $emoji) {
$items[] = [
'id' => $id = ($this->page - 1) * self::PER_PAGE + $i,
'emoji' => $emoji,
'color' => $colors[$id % \count($colors)],
];
}
return $items;
}
public function getColors(): array
{
return [
'#fbf8cc', '#fde4cf', '#ffcfd2',
'#f1c0e8', '#cfbaf0', '#a3c4f3',
'#90dbf4', '#8eecf5', '#98f5e1',
'#b9fbc0', '#b9fbc0', '#ffc9c9',
'#d7ffc9', '#c9fffb',
];
}
}
The solution involves the use of data-live-ignore
attributes, and just a little
bit of trickery in the ProductGrid
component.
You just need to simulate the presence of the previous results in the HTML. The
empty div
with the data-live-ignore
attribute and previous page id (under the 🦊)
is enough to trick the LiveComponent
into thinking that the previous results
are still there, and cannot be modified.
Then, instead of replacing the results, the LiveComponent
will add the new ones in continuation!
<div class="ProductGrid" {{ attributes }}>
<div id="results" style="display: flex; gap: 1rem; flex-direction: column;" class="p-4">
{% if page > 1 %}
{# 🦊 #}
{# Adding a fake "previous page" div is enough to trick the system #}
{# It must have the same ID than the original page #}
<div class="ProductGrid_page" id="page--{{ page - 1 }}" data-live-ignore="true"></div>
{% endif %}
{# Current page #}
<div class="ProductGrid_page" id="page--{{ page }}" data-live-ignore="true">
{% for item in this.items %}
<article class="ProductGrid_item" style="--color: {{ item.color }};">
<div class="ProductGrid_media">
<svg><use href="#svg-tshirt"/></svg>
<span>{{ item.emoji }}</span>
</div>
<data class="ProductGrid_price" value="{{ item.id }}" data-currency="$">
{{ item.id + 1 }}<small>.99</small>
</data>
</article>
{% endfor %}
</div>
<div style="display: grid; place-content: center;padding-block: 2rem;">
{% if this.hasMore %}
<button data-action="live#action" data-live-action-param="more" class="ProductGrid_more">
Load More
</button>
{% else %}
<span class="code">The End</span>
{% endif %}
</div>
</div>
</div>