Skip to main content

Show a list a page at a time

Goal​

Show a table twenty rows at a time, let somebody search it, and put Previous and Next under it that know when to stop.

Why this exists​

A drawn list loads what you ask it for. Ask for everything and a table of nine hundred rows arrives in one response and scrolls forever; ask for twenty and nothing on the screen can say whether those twenty are the last twenty or the first of nine hundred.

So the query grew two things: a page (skip), and a place to put the count (countInto).

One query does all of it​

loadRecords("par_case", "cases", {
top: num(getState("pageSize")),
skip: getState("page") * num(getState("pageSize")),
contains: { par_name: getState("query") },
orderBy: "par_name",
countInto: "caseTotal"
});

num(…) is there because the page size comes from a dropdown, and every control that offers a choice hands back the words rather than the number — see The page size below.

PartIs
tophow many rows this page holds
skiphow many to step over first — rows, not pages
wherefield/value pairs that must match exactly, all of them
containsfield/substring pairs — what a search box needs
orderBy / descendingthe order
countIntoa state field for how many rows match in all, ignoring the page

skip is rows rather than a page number because top is already rows. A screen holding both would have to keep them consistent, and the way that goes wrong is quiet: somebody changes the page size, forgets to reset the page, and lands on page 7 of a list that now has 3.

An empty contains value contributes nothing, so you can pass your search box straight through. A box nobody has typed in means everything, not rows containing the empty string.

countInto is free. The platform's query counts anyway, so asking for the total costs no second request. A screen without a Next button leaves it out and pays nothing.

Steps​

  1. Declare the state the list needs: cases ([]), caseTotal (0), page (0), pageSize ("20" — the dropdown's own value, so it is words), query ("").

  2. Put the query above in the screen's Loaded handler.

  3. Draw Previous and Next, and bind their Enabled:

    • Previous — state.page > 0
    • Next — (state.page + 1) * num(state.pageSize) < state.caseTotal
  4. Their handlers move the page and run the query again:

    setState("page", getState("page") + 1);
    loadRecords( … the same query … );
  5. Draw the caption. A number cannot be added to a sentence directly — BS Lang refuses to mix a string and a number and says Only number or string — so wrap it:

    "Showing " + text(state.cases.length()) + " of " + text(state.caseTotal)
  6. Draw the search box as a text box bound to state.query, and give it a change handler that goes back to page zero and re-runs the query. A text box commits when it is left or when Enter is pressed, so that is one query per search rather than one per letter.

The page size​

One dropdown, at the right-hand end of the row, offering 10, 20, 30, 40, 50, 100 and 200:

  • Draw a box, make it a Dropdown control, and type the sizes into its options list.
  • Bind its Value to state.pageSize — a plain state path, so what somebody chooses is written back before anything else runs.
  • Give it a change handler that sets the page back to zero and re-runs the query.
  • Put the words Page size inside the box rather than beside it. What you draw inside a control becomes its placeholder and the name a screen reader announces; a caption in front of it repeats what the unopened dropdown already says.

It hands back the words, not the number. Every control that offers a choice does — so state.pageSize holds "20", and using it in arithmetic needs num(…):

num(state.pageSize) * state.page

Without it BS Lang refuses the pair and says Only number or string, which is the same rule that makes a caption need text(…) — the two are opposite directions of the same conversion. num(…) answers 0 for anything that is not a number, so an untouched picker cannot break the screen.

Always go back to page zero when the shape of the list changes — a new page size, a new search. A filtered list that stayed on page seven shows nothing, and reads as a broken search.

Searching a picker​

The same contains is what makes a lookup usable. In the Support desk sample the contact picker's card has its own text box, whose change handler re-runs

loadRecords("contact", "contacts", { top: 8, orderBy: "fullname",
contains: { fullname: getState("contactQuery") } });

That is the environment's answer, not a filter over the eight rows already in the card — which is the difference between a picker and a picker that can only find what it has already fetched.

What is not here​

There is no or. contains takes several fields and ANDs them, which is not what a search box across three columns means. Searching any of several fields is where a filter stops being one line in a handler and becomes the query designer the platform already has.

Troubleshooting​

  • The caption says Only number or string. — a number is being added to a sentence. Wrap it in text(…). The same message on a sum is the opposite problem: a control's words are being used as a number, so wrap that in num(…).
  • The page size does nothing, or the list goes blank when it changes. — state.pageSize holds the dropdown's words. top and skip need num(…) round it.
  • Next never disables. — nothing is writing the total. Add countInto to the query.
  • Searching finds nothing after the first page. — the search handler is not resetting page.
  • The list re-queries on every keystroke. — the handler is on something that fires per character. Put it on the box's change, which fires when it is left.