Sources
Decide where a paginator's items come from, whether that's an array you already hold or one page at a time from your database. Covers a source without a total, the page every source returns, and writing one of your own.
A source decides which items belong on page N. Pagination used the simpler of the two sources seedcord ships. This page covers the other one, plus writing your own.
ArraySource loads the whole list on every click and slices it. For a mod log with fifty thousand rows, that's fifty thousand rows read to show ten.
CursorSource fetches one page at a time and reports whether another follows, which is what a SQL LIMIT/OFFSET query or a paged HTTP API gives you.
import {
function ButtonRoute<const Defs extends readonly AnyCustomId[]>(
...defs: Defs
): <TCtor extends AnyHandlerCtor>(
constructor: AssertComponentRoute<InteractionKind.Button, Defs, TCtor>
) => void
ButtonRoute,
class CursorSource<Item>CursorSource,
class Paginator<Item, const Prefix extends string>Paginator,
class SlashHandler<
Route extends keyof SlashRegistry,
Cache extends CacheType = CacheFor<Route>
>
SlashHandler,
function SlashRoute<const Route extends keyof SlashRegistry>(
...routes: Route[]
): <TCtor extends AnyHandlerCtor>(
constructor: AssertSlashRoute<Route, TCtor>
) => void
SlashRoute
} from '@seedcord/gateway';
interface interface EntryEntry {
Entry.at: stringat: string;
Entry.action: stringaction: string;
}
export const const History: Paginator<Entry, "history">History = new new Paginator<Entry, "history">(config: PaginatorConfig<Entry, "history", PageContext>): Paginator<Entry, "history">Paginator({
PaginatorConfig<Entry, "history", PageContext>.prefix: "history"prefix: 'history',
PaginatorConfig<Entry, "history", PageContext>.source: PageSourceBase<Entry, PageContext>source: new new CursorSource<Entry>(fetch: (ctx: PageContext, page: number, perPage: number) => Promisable<{
items: readonly Entry[];
hasNext: boolean;
}>, opts?: {
perPage?: number;
}): CursorSource<Entry>
CursorSource(
(ctx: PageContextctx, page: numberpage, perPage: numberperPage) =>
function readAuditPage(
guildId: string | undefined,
offset: number,
limit: number
): Promise<{
items: Entry[];
hasNext: boolean;
}>
readAuditPage(ctx: PageContextctx.PageContext.guild: Guild | nullguild?.BaseGuild.id: string | undefinedid, page: numberpage * perPage: numberperPage, perPage: numberperPage),
{ perPage?: numberperPage: 10 }
),
PaginatorConfig<Entry, "history", PageContext>.renderItem?: ItemRender<Entry>renderItem: (entry: Entryentry) => `\`${entry: Entryentry.Entry.at: stringat}\` ${entry: Entryentry.Entry.action: stringaction}`
});
@ButtonRoute<readonly [PageCursor<"history">]>(defs_0: PageCursor<"history">): <TCtor>(constructor: AssertComponentRoute<InteractionKind.Button, readonly [PageCursor<"history">], TCtor>) => voidButtonRoute(const History: Paginator<Entry, "history">History.PaginatorBase<Entry, "history", PageContext>.cursor: PageCursor<"history">cursor)
export class class HistoryNavHistoryNav extends const History: Paginator<Entry, "history">History.Paginator<Entry, "history">.Handler: PaginatorHandlerCtor<"history">Handler {}
@SlashRoute<"history">(...routes: "history"[]): <TCtor>(constructor: AssertSlashRoute<"history", TCtor>) => voidSlashRoute('history')
export class class HistoryListHistoryList extends class SlashHandler<
Route extends keyof SlashRegistry,
Cache extends CacheType = CacheFor<Route>
>
SlashHandler<'history'> {
public async HistoryList.execute(): Promise<void>execute(): interface Promise<T>Promise<void> {
await const History: Paginator<Entry, "history">History.Paginator<Entry, "history">.start(handler: RepliableHandler<Repliables>, n?: number): Promise<SentMessage>start(this);
}
}The fetcher receives the page number and the page size, so page * perPage above is the offset. Return the slice and a hasNext flag, as a promise or a plain object.
A cursor source can't see where the list ends, so it passes a page past the last one to your fetcher unchanged. Return an empty slice with hasNext: false there. A true there leaves Next enabled, so someone can keep clicking into empty pages.
Gateway and http differ
The fetcher's first argument is a PageContext. Gateway's carries guild: Guild | null while http's carries guildId: string | null, so ctx.guild fails to compile there.
Without a total
CursorSource never asks for a total, because counting every row costs a second query. Three things follow.
totalPagescomes backundefined.- The nav row drops First and Last, leaving Prev, the indicator, and Next.
- The indicator reads
Page 3with no total after it.
Use ArraySource when the list is small enough to load whole and you want the count.
The page a source returns
Every source returns a PageView, the two shipped ones and yours alike.
| field | what it holds |
|---|---|
items | the items on this page |
page | the zero-based page number, clamped to the last page by ArraySource |
perPage | the page size the source used |
totalPages | the count, or undefined when the source cannot get one cheaply |
hasPrev | whether a page comes before this one |
hasNext | whether a page comes after it |
perPage defaults to 10 on both sources. A zero, a fraction, or a negative throws PaginationInvalidPerPage while you construct the source.
Writing your own
PageSource declares one method. Write your own for something neither shipped source does, like reporting a real total on a query you still fetch one slice at a time.
export class AuditSource implements PageSource<Entry> {
constructor(private readonly perPage: number) {}
public async page(
ctx: PageContext,
n: number
): Promise<PageView<Entry>> {
const total = await countAuditRows(ctx.guild?.id);
const totalPages = Math.max(
1,
Math.ceil(total / this.perPage)
);
const page = Math.min(
Math.max(0, Math.trunc(n)),
totalPages - 1
);
return {
items: await readAuditRows(
ctx.guild?.id,
page * this.perPage,
this.perPage
),
page,
perPage: this.perPage,
totalPages,
hasPrev: page > 0,
hasNext: page < totalPages - 1
};
}
}One COUNT beside the page query fills in totalPages, which puts First and Last back without loading the table.
Clamp n yourself, as the sample does. Say the table had eight pages when a button was minted. After someone deletes half the rows, that button still carries page 7. start() also passes any number you give it.
seedcord has rendered every page so far. Custom rendering hands you the whole message.