DEMOS / LiveComponent

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.

ux.symfony.com
🐻
$ 1.99
🐨
$ 2.99
🐼
$ 3.99
🦥
$ 4.99
🦦
$ 5.99
🦨
$ 6.99
🦘
$ 7.99
🦡
$ 8.99
🐾
$ 9.99
🦃
$ 10.99

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
use App\Service\EmojiCollection;
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;

#[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.defaults(stimulus_controller('scroll')) }}>

    <div class="p-4">
        <div id="results" style="display: flex; gap: 1rem; flex-direction: column;">

            {% 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" data-num="{{ item.id }}"
                             style="--color: {{ item.color }};">
                        <div class="ProductGrid_media">
                            <svg>
                                <use href="#svg-tshirt"/>
                            </svg>
                            <span>{{ item.emoji }}</span>
                        </div>
                        <data value="{{ item.id }}">$ {{ item.id + 1 }}<small>.99</small></data>
                    </article>
                {% endfor %}
            </div>

            {% if this.hasMore %}
                <div style="display: grid; place-content: center;padding: 4rem;">
                    <button
                        data-action="live#action"
                        data-live-action-param="more"
                        data-scroll-target="loader"
                        class="btn btn-primary">Load More (page {{ page + 1 }})
                    </button>
                </div>
            {% endif %}

        </div>
    </div>

</div>