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.

Item #1

This is the description for item number 1.

Page 1 of 10 | ID: 1
Item #2

This is the description for item number 2.

Page 1 of 10 | ID: 2
Item #3

This is the description for item number 3.

Page 1 of 10 | ID: 3
Item #4

This is the description for item number 4.

Page 1 of 10 | ID: 4
Item #5

This is the description for item number 5.

Page 1 of 10 | ID: 5
Item #6

This is the description for item number 6.

Page 1 of 10 | ID: 6
Item #7

This is the description for item number 7.

Page 1 of 10 | ID: 7
Item #8

This is the description for item number 8.

Page 1 of 10 | ID: 8
Item #9

This is the description for item number 9.

Page 1 of 10 | ID: 9
Item #10

This is the description for item number 10.

Page 1 of 10 | ID: 10
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 %}