[ADD] website_sale_lazy_loading: load the /shop listing on demand

The shop pages products: every page is a full reload that throws away the
grid the visitor was reading. This appends the next page to the grid
instead, on scroll or on a "Load more products" click, per website.

The controller does not duplicate /shop. The new /shop/lazy_products calls
shop() and renders the cards out of the qcontext it prepared, so search,
categories, attributes, tags, price filter, sort order and pricelists are
supported by construction -- and so are the values other modules add to the
listing, the wishlist state among them. Out of range pages answer empty
rather than the last page again, which portal.pager would otherwise clamp
to and the frontend would append as duplicates.

The product loop of website_sale.products is replaced by a call to a shared
template, so the first page and the appended ones are the same markup: a
ribbon, a price or a button another module adds to products_item shows up
on every card, not only on the ones the initial render produced.

Progressive enhancement throughout: the first page and the pager are still
what the standard controller renders, and the pager is only hidden once the
widget is running. Without JavaScript -- and for crawlers -- the shop is
exactly what it is without this module. The page size is the shop layout's
"Products per page", the value the pager already uses, so there is nothing
to keep in sync.

The frontend takes no decision about what to show: the server sends the
mode, the URL of the listing on screen and each page of cards. It observes
a block below the grid rather than listening to scroll, restarts the public
widgets on the appended cards, and keeps a button as the fallback for a
failed request or a browser without IntersectionObserver.

Tests cover the block rendering per mode, the filters travelling in the
AJAX URL, and the endpoint on a next page, the last page, an out of range
page, a broken page number and an unknown category.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
GitHub Copilot 2026-08-24 12:33:09 +02:00
parent 5766406b6f
commit a9d6f52ca6
22 changed files with 1039 additions and 0 deletions

View file

@ -0,0 +1,17 @@
Go to *Website > Configuration > Settings > Shop - Products > Shop Product
Loading* and pick how the listing loads its pages. The setting is **per
website**:
* **Infinite scroll** (default): the next page is appended when the visitor
reaches the bottom of the listing. A *Load more products* button shows up as
a fallback if a request fails or if the browser has no
``IntersectionObserver``.
* **Load more button**: the next page is appended when the visitor clicks the
button. This is the friendlier option for keyboard and screen reader users,
and it keeps the footer reachable.
* **Pager (standard)**: turns the module off and leaves the standard
``website_sale`` pager alone.
The number of products of each page is **not** configured here: it is the
*Products per page* of the shop layout (open ``/shop`` as editor, *Edit* and
use the *Products Page* options), the same value the standard pager uses.

View file

@ -0,0 +1,3 @@
* `Criptomart <https://criptomart.net>`_:
* Development and maintenance

View file

@ -0,0 +1,10 @@
**Authors:**
* Criptomart
**Other credits:**
The approach (a server rendered fragment appended to the grid, an observer
close to the bottom of the listing and a button as a fallback) comes from the
lazy loading of the Eskaera shop in ``website_sale_aplicoop``, reimplemented
here on top of the standard ``website_sale`` controller and templates.

View file

@ -0,0 +1,20 @@
This module loads the products of the eCommerce shop (``/shop``) **on demand**
instead of paginating them: the visitor keeps the products already on screen
and the next page is appended to the grid, either when they scroll to the
bottom of the listing (infinite scroll) or when they click a *Load more
products* button.
It is the same idea as the lazy loading of ``website_sale_aplicoop``
(``/eskaera``), rebuilt for the standard shop:
* The page size is the **Products per page** of the shop layout, the very same
value the standard pager uses, so nothing has to be configured twice.
* The product cards come from the standard ``website_sale.products_item``
template, so ribbons, prices, wishlist or comparison buttons added by other
modules are rendered exactly as on the first page.
* Search, categories, attributes, tags, price filter and sort order are kept:
the appended pages are the pages of the listing currently on screen.
The first page and the pager are still rendered by the standard controller, so
visitors without JavaScript — and search engine crawlers — keep a listing they
can navigate page by page.

View file

@ -0,0 +1,6 @@
Install it like any other addon::
docker-compose run --rm odoo odoo -d odoo -i website_sale_lazy_loading --stop-after-init
docker-compose up -d
It only depends on ``website_sale``.

View file

@ -0,0 +1,24 @@
There is nothing to do at runtime: once the mode is set, ``/shop`` loads its
pages on demand.
How it works
~~~~~~~~~~~~
``/shop`` is rendered by the standard controller, with its pager. When a next
page exists, this module adds a block below the grid carrying the loading mode
and the URL of the current listing (search, category, attributes, tags, price
range and sort order included). The frontend widget then hides the pager and
requests ``/shop/lazy_products?...&page=N``, which answers with the product
cards of that page as an HTML fragment plus whether a further page exists, and
appends them to the grid.
Consequences worth knowing:
* Product links keep the page they belong to (``?page=3``), so coming back from
a product page lands on the right part of the listing.
* Changing a filter, a category or the sort order is still a normal page load;
lazy loading restarts from the first page of the new listing.
* Reloading the page starts again from the first page: the appended pages are
not part of the URL.
* The endpoint answers with the same data ``/shop`` would render for that page,
under the rights of the visitor making the request.