.md

Pagination & infinite scroll

Load data in pages by passing start and count options to items().

Key dcupl functions

Function Purpose
query.items({ start, count }) Get a slice of items
fn.metadata().currentSize Total number of matches (for calculating pages)
query.applyOptions({ start, count }) Store the page in the list's query
query.count() Number of items items() returns (the page, once one is set)

Basic pagination

Pass start and count to items() to load a specific page:

// Page 1: items 0-19
const page1 = productList.catalog.query.items({ start: 0, count: 20 });

// Page 2: items 20-39
const page2 = productList.catalog.query.items({ start: 20, count: 20 });

// Page 3: items 40-59
const page3 = productList.catalog.query.items({ start: 40, count: 20 });

Calculate total pages

Use fn.metadata().currentSize to determine how many pages exist. It counts every item that matches the current query, even when a page is stored with applyOptions() (then query.count() returns the length of that page):

const PAGE_SIZE = 20;
const totalItems = productList.catalog.fn.metadata().currentSize;
const totalPages = Math.ceil(totalItems / PAGE_SIZE);

Load more (infinite scroll)

Accumulate items as user scrolls:

import type { ListItem } from '@dcupl/common';

const PAGE_SIZE = 20;
let currentPage = 0;
let allItems: ListItem[] = [];

function loadMore() {
  const start = currentPage * PAGE_SIZE;
  const newItems = productList.catalog.query.items({ start, count: PAGE_SIZE });

  allItems = [...allItems, ...newItems];
  currentPage++;

  // More items exist while fewer than all matches are loaded
  return allItems.length < productList.catalog.fn.metadata().currentSize;
}

// Filters or data changed: start over, or the loaded pages mix old and new results
productList.onResultChange(() => {
  currentPage = 0;
  allItems = [];
  loadMore();
});

Comparing with currentSize stops exactly at the end; checking newItems.length === PAGE_SIZE would request one more, empty page when the total is a multiple of the page size. Reads with start and count don't change the list's query, so they don't trigger onResultChange.

Pagination with sorting

Combine pagination with sort options. Sort order values must be uppercase ('ASC' / 'DESC'); with a lowercase value the whole sort is ignored and a warning is logged:

type Product = ListItem & { name: string; price: number };

const sortedPage = productList.catalog.query.items<Product>({
  start: 0,
  count: 20,
  sort: {
    attributes: ['price'],
    order: ['ASC'],
  },
});

Sorting doesn't change the result type: items() returns the list's item type, Product[] on a list created with lists.create<Product>() and any[] on an untyped list. Pass the type to the read (items<Product>(…)) to get typed items from an untyped list.

Reset pagination after filter

When filters change, reset to first page:

// Apply new filter
productList.catalog.query.setCondition({
  attribute: 'category',
  operator: 'eq',
  value: 'Electronics',
});

// Get filtered first page
const results = productList.catalog.query.items({ start: 0, count: 20 });

Keep the page in the list query

Instead of passing start and count on every call, store them with applyOptions(). items() then returns the current page, and list.onResultChange() notifies you whenever the page, the filters or the data change. A filter change keeps start, so reset it yourself:

const PAGE_SIZE = 20;

// Store the first page (other options such as sort are kept)
productList.catalog.query.applyOptions({ start: 0, count: PAGE_SIZE });

// Re-render whenever the result changes
const unsubscribe = productList.onResultChange(() => {
  renderProducts(productList.catalog.query.items());
});

// Next page
productList.catalog.query.applyOptions({ start: PAGE_SIZE });

// New filter: go back to the first page first, then filter
productList.catalog.query.applyOptions({ start: 0 });
productList.catalog.query.setCondition({ attribute: 'category', operator: 'eq', value: 'Electronics' });

Complete flow

A paged product list with next/previous buttons, kept in the list's query:

import { Dcupl } from '@dcupl/core';
import type { ListItem } from '@dcupl/common';

type Product = ListItem & { name: string; category: string; price: number };

function renderProducts(items: Product[], page: number, pageCount: number) {
  console.log(
    `Page ${page + 1} of ${pageCount}:`,
    items.map((p) => p.name),
  );
}

const dcupl = new Dcupl();
dcupl.models.set({
  key: 'Product',
  properties: [
    { key: 'name', type: 'string' },
    { key: 'category', type: 'string' },
    { key: 'price', type: 'float' },
  ],
});
dcupl.data.set(
  Array.from({ length: 45 }, (_, i) => ({
    key: `p${i}`,
    name: `Product ${i}`,
    category: i % 3 === 0 ? 'Books' : 'Electronics',
    price: i * 10,
  })),
  { model: 'Product' },
);
await dcupl.init();

const PAGE_SIZE = 20;
const productList = dcupl.lists.create({ modelKey: 'Product' });
let page = 0;

const pageCount = () => Math.ceil(productList.catalog.fn.metadata().currentSize / PAGE_SIZE);

// 1. Re-render whenever the page, the filters or the data change
productList.onResultChange(() => {
  renderProducts(productList.catalog.query.items<Product>(), page, pageCount());
});

// 2. Store the first page in the list's query (renders page 1 of 3)
productList.catalog.query.applyOptions({ start: 0, count: PAGE_SIZE });

// 3. Next / previous page
function goToPage(next: number) {
  const target = Math.max(0, Math.min(next, pageCount() - 1));
  if (target === page) return;
  page = target;
  productList.catalog.query.applyOptions({ start: page * PAGE_SIZE });
}
const nextPage = () => goToPage(page + 1);
const prevPage = () => goToPage(page - 1);

// 4. A new filter starts at the first page again
function filterByCategory(category: string) {
  page = 0;
  // skipProcessing: store the option without recomputing; setCondition() recomputes once
  productList.catalog.query.applyOptions({ start: 0 }, { skipProcessing: true });
  productList.catalog.query.setCondition({ attribute: 'category', operator: 'eq', value: category });
}

nextPage(); // Page 2 of 3
filterByCategory('Books'); // Page 1 of 1 (15 books)