Infinite Scroll

Scroll down to load more items automatically. Uses htmx preload extension to fetch next page in background.

Uses preload="init" to fetch next page immediately after current one loads. When you scroll down, the content is already cached - instant display!

Tip: You can navigate directly to any page via URL, e.g. jump to last page.

You're viewing from page 10 of 10. Earlier items are not loaded. Back to start

Item #91

This is the description for item number 91.

Page 10 of 10 | ID: 91
Item #92

This is the description for item number 92.

Page 10 of 10 | ID: 92
Item #93

This is the description for item number 93.

Page 10 of 10 | ID: 93
Item #94

This is the description for item number 94.

Page 10 of 10 | ID: 94
Item #95

This is the description for item number 95.

Page 10 of 10 | ID: 95
Item #96

This is the description for item number 96.

Page 10 of 10 | ID: 96
Item #97

This is the description for item number 97.

Page 10 of 10 | ID: 97
Item #98

This is the description for item number 98.

Page 10 of 10 | ID: 98
Item #99

This is the description for item number 99.

Page 10 of 10 | ID: 99
Item #100

This is the description for item number 100.

Page 10 of 10 | ID: 100
Loading...
Source Code View on GitHub
<?php

declare(strict_types=1);

namespace App\Controller;

use Mdxpl\HtmxBundle\Request\HtmxRequest;
use Mdxpl\HtmxBundle\Response\HtmxResponse;
use Mdxpl\HtmxBundle\Response\HtmxResponseBuilder;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
use Symfony\Component\Routing\Attribute\Route;

#[Route('/infinite-scroll', name: 'app_infinite_scroll')]
final class InfiniteScrollController extends AbstractController
{
    public function __invoke(HtmxRequest $htmx, #[MapQueryParameter] int $page = 1): HtmxResponse
    {
        $itemsPerPage = 10;
        $totalItems = 100;
        $hasMore = ($page * $itemsPerPage) < $totalItems;

        $items = [];
        $start = ($page - 1) * $itemsPerPage + 1;
        $end = min($page * $itemsPerPage, $totalItems);

        for ($i = $start; $i <= $end; $i++) {
            $items[] = [
                'id' => $i,
                'title' => "Item #{$i}",
                'description' => "This is the description for item number {$i}.",
            ];
        }

        $viewData = [
            'items' => $items,
            'currentPage' => $page,
            'nextPage' => $page + 1,
            'hasMore' => $hasMore,
            'totalPages' => (int) ceil($totalItems / $itemsPerPage),
        ];

        $builder = HtmxResponseBuilder::create($htmx->isHtmx);

        if ($htmx->isHtmx) {
            usleep(300000);

            return $builder
                ->success()
                ->viewBlock('infinite_scroll.html.twig', 'items', $viewData)
                ->build();
        }

        return $builder
            ->success()
            ->view('infinite_scroll.html.twig', $viewData)
            ->build();
    }
}
{% block items %}
{% for item in items %}
<article
    {% if loop.last and hasMore %}
        hx-get="{{ path('app_infinite_scroll', {'page': nextPage}) }}"
        hx-trigger="revealed"
        hx-swap="afterend"
        hx-replace-url="true"
        preload="init"
    {% endif %}
>
    <header><strong>{{ item.title }}</strong></header>
    <p>{{ item.description }}</p>
    <footer>
        <small>Page {{ currentPage }} of {{ totalPages }} | ID: {{ item.id }}</small>
    </footer>
</article>
{% endfor %}

{% if not hasMore %}
<article>
    <p><ins>You've reached the end!</ins> No more items to load.</p>
    {% if currentPage > 1 %}
    <footer>
        <a href="{{ path('app_infinite_scroll') }}" role="button" class="outline">Back to start</a>
    </footer>
    {% endif %}
</article>
{% endif %}
{% endblock %}