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>
115 lines
5 KiB
ReStructuredText
115 lines
5 KiB
ReStructuredText
================================
|
|
Website Sale - Lazy Loading
|
|
================================
|
|
|
|
.. image:: https://img.shields.io/badge/license-AGPL--3-blue.svg
|
|
:target: https://www.gnu.org/licenses/agpl-3.0-standalone.html
|
|
:alt: License: AGPL-3
|
|
.. image:: https://img.shields.io/badge/Odoo-18.0-blue
|
|
:alt: Odoo: 18.0
|
|
|
|
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.
|
|
|
|
What the module does
|
|
====================
|
|
|
|
- Adds a per-website setting choosing how the listing loads its pages:
|
|
infinite scroll, a *Load more* button, or the standard pager.
|
|
- Adds ``/shop/lazy_products``, which renders one page of the listing as an
|
|
HTML fragment (the product cards only) plus whether a further page exists.
|
|
- Replaces the product loop of ``website_sale.products`` with a call to a
|
|
shared template, so the first page and the appended ones are rendered by the
|
|
very same markup.
|
|
- Hides the standard pager from the frontend once the widget takes over.
|
|
|
|
The page size is the **Products per page** of the shop layout, the same value
|
|
the standard pager uses, so nothing has to be configured twice.
|
|
|
|
Design notes
|
|
============
|
|
|
|
- The ``/shop`` controller is not duplicated: ``/shop/lazy_products`` calls
|
|
``shop()`` and renders the grid out of the values it prepared. Everything
|
|
the standard listing supports — search, categories, attributes, tags, price
|
|
filter, sort order, pricelists — is therefore supported as is, and so are
|
|
the values other modules add to the shop (the wishlist state, for example).
|
|
- The product cards come from ``website_sale.products_item``, so ribbons,
|
|
prices, add-to-cart, wishlist or comparison buttons added by other modules
|
|
are rendered exactly as on the first page. Public widgets are restarted on
|
|
the appended cards through ``widgets_start_request``.
|
|
- Progressive enhancement: the first page and the pager are still rendered by
|
|
the standard controller. Without JavaScript, the pager stays visible and the
|
|
shop behaves exactly as it does without this module — which is also what
|
|
search engine crawlers get.
|
|
- The frontend takes no decision about *what* to show: the server sends the
|
|
mode, the URL of the current listing and each page of cards.
|
|
|
|
Configuration
|
|
=============
|
|
|
|
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 per page is the *Products per page* of the shop layout
|
|
(open ``/shop`` as editor, *Edit*, then the *Products Page* options).
|
|
|
|
Usage
|
|
=====
|
|
|
|
There is nothing to do at runtime. 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.
|
|
|
|
Known limitations
|
|
=================
|
|
|
|
- The listing is only appended, never reduced: it grows until the visitor
|
|
leaves or reloads the page.
|
|
- Infinite scroll pushes the footer away as long as pages remain; use the
|
|
button mode if the footer matters on your site.
|
|
|
|
Technical
|
|
=========
|
|
|
|
::
|
|
|
|
controllers/website_sale.py /shop values + /shop/lazy_products endpoint
|
|
models/website.py shop_lazy_loading (per website)
|
|
views/..._templates.xml shared grid template + lazy loading block
|
|
static/src/js/... public widget appending the pages
|
|
|
|
Tests: ``docker-compose run --rm odoo odoo $ODOO_ADDONS -d odoo --test-enable
|
|
--stop-after-init -u website_sale_lazy_loading``
|
|
|
|
Credits
|
|
=======
|
|
|
|
**Authors:** Criptomart
|
|
|
|
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.
|