Shoji has no dedicated infinite-scroll plugin — its entire integration surface for both demos
below is updateSlides() (or its addSlides()/removeSlides()
sugar), a data-shoji-id marker on each thumbnail, and a plugin choice. Everything
else on this page — fetching, rendering, scroll position, virtualization — is plain host code
Shoji never sees. See the
Infinite scroll guide for what those touchpoints
do and why.
Each new page is handed to gallery.addSlides() — sugar over
updateSlides() for the append case, so there's no host-side item array to keep
in sync with it (gallery.items is Shoji's own copy).
ActiveThumbnail
is enabled here — tiles are only ever appended, never removed, so there's no risk of it
tracking a thumbnail that's since disappeared.
This page never virtualizes the DOM it appends to — every tile ever loaded
stays mounted, with no loading="lazy" either. Fine for a demo, or a real
gallery bounded to some thousands of photos in a session, but a genuinely unbounded scroll
would eventually need the same windowing the bidirectional demo already does, just for one
direction instead of two.
Loading…
Loading first page…
A virtualized grid (see the guide for what that means) rebuilds the full item array from its
own loaded-item map and hands it to updateSlides() every time that map grows —
deliberately not addSlides()/removeSlides(), unlike the
forward-only demo: this grid already needs its own id → item map for the tile
pool's own O(1) lookups (assignTile()/releaseTile()), so there's
no state to save by also asking Shoji to track a second copy piecemeal — one full,
already-sorted array per load is simpler than reasoning about which of two Shoji calls (add
one side, remove the other) a given load needs. That array stays small regardless (capped by
MAX_LOADED_ROWS
below), so re-sorting it every time costs nothing measurable.
ActiveThumbnail is deliberately left off — this grid can unmount the exact tile
it would be highlighting the moment that tile scrolls outside the rendered window.
Loading…
Forward-only — enable the plugin, then addSlides() (sugar over
updateSlides() for the append case, see the
guide) each new page. No host-side item array to
keep in sync — gallery.items is already Shoji's own copy:
const gallery = new Shoji(grid, { items: [], plugins: [Shoji.ActiveThumbnail] });
async function loadNextPage() {
const newItems = await fetchNextPage();
gallery.addSlides(newItems); // appends by default
for (const item of newItems) grid.appendChild(renderThumb(item)); // sets a.dataset.shojiId
}
function openById(id) {
const index = gallery.items.findIndex((i) => i.id === id);
if (index !== -1) gallery.open(index);
}
Bidirectional calls the plain updateSlides() instead of
addSlides()/removeSlides() — it already needs its own
id → item map for the tile pool's own lookups, so there's no state to save by
also asking Shoji to track a second copy piecemeal; one full, already-sorted array per load is
simpler than reasoning about which of two Shoji calls a given load needs. Sourced from that
map, sorted before every call — Map iteration order is insertion order, not
numeric order, and an older page's ids are always smaller but still inserted last:
function sortedItems() {
return [...itemsById.values()].sort((a, b) => idNum(a.id) - idNum(b.id));
}
gallery.updateSlides(sortedItems()); // called after every load, in both directions
Click-to-open, identical in both demos — resolved fresh from the id, never a captured index:
function openById(id) {
const index = sortedItems().findIndex((i) => i.id === id);
if (index !== -1) gallery.open(index);
}
Everything else on this page — the sentinel/IntersectionObserver pattern, the
fixed-row virtual list, the scroll-position math — is in the script above, and covered
conceptually in the guide. None of it touches
Shoji.