[LiveComponent] Infinite scroll and page is inconsistent #2545
Replies: 5 comments
-
Why would you increment your page number on getItems() ? Maybe make it a dedicated method ? |
Beta Was this translation helpful? Give feedback.
-
Sorry but I don't understand what you mean, if I don't increment it, I face an issue, and the dedicated method is already the more method from the tutorial. |
Beta Was this translation helpful? Give feedback.
-
Could you share more of your code, it's hard to help you figure the issue your experience without more context :| |
Beta Was this translation helpful? Give feedback.
-
File ContentGrid.html.twig <div class="ContentGrid" {{ attributes.defaults(stimulus_controller('appear')) }}>
<div id="contents" class="p-4">
<div class="ContentGrid_items row g-2">
{% if page > 1 %}
<article class="ContentGrid_item" id="item--{{ page - 1 }}-{{ per_page }}"></article>
{% endif %}
{% for item in this.items %}
<article class="ContentGrid_item col-12 col-lg-3 border-0 d-flex align-items-stretch"
data-live-ignore="true"
id="item--{{ page }}-{{ loop.index }}">
{{ component('ShowContent', {
content: item,
user: item.owner
}) }}
</article>
{% endfor %}
{% if this.hasMore %}
{% for i in 1..per_page %}
<article id="item--{{ page + 1 }}-{{ i }}"
data-live-ignore="true"
class="ContentGrid_item card col-12 col-lg-3 border-0 d-flex align-items-stretch"
>
{{ component('ShowContent', {
content: this.items[i-1],
user: this.items[i-1].owner
}) }}
</article>
{% endfor %}
{% endif %}
</div>
{% if this.hasMore %}
<div
class="three-bounce-spinner"
data-appear-target="loader"
data-action="appear->live#action"
data-live-action-param="debounce(750)|more"
>
<div data-loading="show">
<div class="bounce1"></div>
<div class="bounce2"></div>
<div class="bounce3"></div>
</div>
</div>
{% endif %}
</div>
</div> File ShowContent.html.twig <div class="card m-2 h-100 col-12 g-2 content-container">
<div class="card-header d-flex align-items-start justify-content-between bg-clearblue">
{{ component('ShowUser', {
user : this.getOwner,
forComment: true
}) }}
<div class="ms-auto text-end">
<small class="text-body-secondary">
{{ this.getCreationDate|ago }}
</small>
</div>
</div>
<div class="card-body">
<a href="javascript:void(0)" class="body-modal" data-bs-toggle="modal" role="button" data-bs-target="#modal-{{ this.getContent.id }}">
{% if this.noMedia %}
<p class="card-text size-18 overflow-auto">
{{ this.getContent.getText|length > 600 ? this.getContent.getText|slice(0, 600) ~ '...' : this.getContent.getText }}
</p>
{% else %}
<p class="card-text size-18">
{{ this.getContent.getText|length > 70 ? this.getContent.getText|slice(0, 70) ~ '...' : this.getContent.getText }}
</p>
<img src="{{ asset(this.getPreview) }}" class="card-img-bottom mb-2" alt="cover"/>
{% endif %}
</a>
</div>
<div class="flip-container">
<div class="card-footer sponsor-container flip-inner {% if this.getSponsor %}has-sponsor{% endif %}">
<div class="card-front px-3">
{% if this.getSponsor %}
<h5 class="">
{{ 'sponsor.headline'|trans|format(this.getSponsor.getLabel)|raw }}
</h5>
<img src="{{ asset('assets/' ~ this.getSponsor.getImagePath) }}" class="sponsor" alt="">
<p class="card-text">
{{ 'sponsor.text'|trans|raw }}
</p>
{% else %}
<a href="javascript:void(0)" class="float-start p-2 text-decoration-none" data-toggle="tooltip"
data-placement="right" title="{{ 'files.count'|trans({'comment': this.countFiles}) }}">
<div class="img-footer-card m-1 flex-column">
{{ this.countFiles }}
{% if this.getFiles|length < 2 and this.getFiles|length > 0 %}
{% for file in this.getFiles %}
<img src="{{ asset(file.preview) }}" alt="">
{% endfor %}
{% else %}
<twig:ux:icon name="fluent-mdl2:media" width="30" height="30"/>
{% endif %}
</div>
</a>
<a href="javascript:void(0)" class="float-end p-2 text-decoration-none" data-toggle="tooltip"
data-placement="right" title="{{ 'comments.count'|trans({'comment': this.commentsCount}) }}">
{{ this.commentsCount }}
<twig:ux:icon name="material-symbols:comment" width="30" height="30"/>
</a>
<div class="clearfix"></div>
<div class="col-12 bg-clearblue">
{% for group in this.getGroups %}
<h5 class="text-white p-2">{{ group }}</h5>
{% endfor %}
</div>
{% endif %}
</div>
{% if this.getSponsor %}
<div class="card-back">
<a href="javascript:void(0)" class="float-start p-2 text-decoration-none" data-toggle="tooltip"
data-placement="right" title="{{ 'files.count'|trans({'comment': this.countFiles}) }}">
<div class="img-footer-card m-1 flex-column">
{{ this.countFiles }}
{% if this.getFiles|length < 2 and this.getFiles|length > 0 %}
{% for file in this.getFiles %}
<img src="{{ asset(file.preview) }}" alt="">
{% endfor %}
{% else %}
<twig:ux:icon name="fluent-mdl2:media" width="30" height="30"/>
{% endif %}
</div>
</a>
<a href="javascript:void(0)" class="float-end p-2 text-decoration-none" data-toggle="tooltip"
data-placement="right" title="{{ 'comments.count'|trans({'comment': this.commentsCount}) }}">
{{ this.commentsCount }}
<twig:ux:icon name="material-symbols:comment" width="30" height="30"/>
</a>
<div class="clearfix"></div>
<div class="col-12 bg-clearblue">
{% for group in this.getGroups %}
<h5 class="text-white p-2">{{ group }}</h5>
{% endfor %}
</div>
<div class="col-12">
<a href="{{ this.getSponsor.url }}" target="_blank">
<h5 class="">
{{ 'sponsor.headline'|trans|format(this.getSponsor.getLabel)|raw }}
</h5>
<img src="{{ asset('assets/' ~ this.getSponsor.getImagePath) }}" class="sponsor" alt="">
</a>
</div>
</div>
{% endif %}
</div>
</div>
</div> File ContentGrid.php <?php
declare(strict_types=1);
namespace App\Application\Twig\Components;
use App\Repository\ContentRepository;
use Doctrine\ORM\Tools\Pagination\Paginator;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\ComponentToolsTrait;
use Symfony\UX\LiveComponent\DefaultActionTrait;
use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate;
use Traversable;
#[AsLiveComponent('ContentGrid')]
class ContentGrid
{
use ComponentToolsTrait;
use DefaultActionTrait;
private Paginator $items;
#[LiveProp(writable: true)]
public string $userId;
public function __construct(private ContentRepository $contentRepository)
{
}
private const PER_PAGE = 10;
#[LiveProp(writable: true)]
public int $page = 1;
#[LiveAction]
public function more(): void
{
if ($this->page === 1) {
$this->page = 2;
}
++$this->page;
}
public function hasMore(): bool
{
return \count($this->items) > ($this->page * self::PER_PAGE);
}
#[ExposeInTemplate('per_page')]
public function getPerPage(): int
{
return self::PER_PAGE;
}
public function getItems(): Traversable
{
$this->items = new Paginator($this->contentRepository->getAllContentsForUserQueryBuilder($this->userId, $this->page, self::PER_PAGE));
if ($this->page === 1) {
++$this->page;
}
return $this->items->getIterator();
}
} I'm using component inside component inside component :p reusability at his best |
Beta Was this translation helpful? Give feedback.
-
It looks like you're displaying actual items inside Reference: The expected behavior is:
This structure ensures smooth updates without unexpected element removals. |
Beta Was this translation helpful? Give feedback.
-
I'm using an infinite scroll that loads results from a request.
In my HTML, I have a first loop that queries my getItems method and a second loop using the hasMore trick.
The main issue is:
When the page first loads, both the
getItems
loop and thehasMore
loop are parsed. If my component is configured to load 10 items per page, it ends up displaying 20 items right away.However, $this->page remains at 1, and it seems that the more method is not triggered on this initial load (probably because it's not an AJAX call).
The real problem occurs on the first AJAX request—since the component still has
$this->page = 1
, it reloads the same items that were already displayed on the initial page load.I've tried many solutions, but none seem to fully resolve the issue.
Do you have any recommendations?
The closest thing to a solution so far is this:
So after I load my first 10 items I directly increase the $this->page attribute so the hasMore method on the first load starts to 2
but after that for the ajax call, $this->page is back to 1 and I put the second condition to make it starts to 2...
Beta Was this translation helpful? Give feedback.
All reactions