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.
| Part | Is |
|---|---|
top | how many rows this page holds |
skip | how many to step over first — rows, not pages |
where | field/value pairs that must match exactly, all of them |
contains | field/substring pairs — what a search box needs |
orderBy / descending | the order |
countInto | a 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
-
Declare the state the list needs:
cases([]),caseTotal(0),page(0),pageSize("20"— the dropdown's own value, so it is words),query(""). -
Put the query above in the screen's Loaded handler.
-
Draw Previous and Next, and bind their Enabled:
- Previous —
state.page > 0 - Next —
(state.page + 1) * num(state.pageSize) < state.caseTotal
- Previous —
-
Their handlers move the page and run the query again:
setState("page", getState("page") + 1);loadRecords( … the same query … ); -
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) -
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 intext(…). The same message on a sum is the opposite problem: a control's words are being used as a number, so wrap that innum(…). - The page size does nothing, or the list goes blank when it changes. —
state.pageSizeholds the dropdown's words.topandskipneednum(…)round it. - Next never disables. — nothing is writing the total. Add
countIntoto 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.