# Custom rendering

Take over a paginator's whole message with render(). Covers a page with no buttons on it, and placing the nav buttons back yourself.

By default a paginator sends one container with your items as lines and a row of nav buttons under them. That's the whole layout. A page with a header, a thumbnail per entry, or the buttons somewhere else needs more.

`render` takes that job. It replaces the page seedcord would have built, nav row included. seedcord adds nothing around what you return.

The function receives two arguments. `view` is the [`PageView`](https://docs.seedcord.org/packages/core/latest/page-view) the source returned, holding this page's items. `controls` is a [`PaginatorControls`](https://docs.seedcord.org/packages/core/latest/paginator-controls), which builds nav buttons already pointed at the right page.

## A page with no buttons on it

Start with a `render` that ignores `controls` and returns a string.

```ts title="src/handlers/Feed.ts"
import {
    ButtonRoute,
    CursorSource,
    Paginator,
    SlashHandler,
    SlashRoute
} from '@seedcord/gateway';

interface Post {
    id: number;
    text: string;
}

export const Feed = new Paginator({
    prefix: 'feed',
    source: new CursorSource((_ctx, page, perPage) =>
        fetchPosts(page, perPage)
    ),
    render: (view) =>
        `Page ${view.page + 1}, ${view.items.length} posts`
});

@ButtonRoute(Feed.cursor)
export class FeedNav extends Feed.Handler {}

@SlashRoute('feed')
export class FeedList extends SlashHandler<'feed'> {
    public async execute(): Promise<void> {
        await Feed.start(this);
    }
}
```

If you run that, you get one line of text with no way to reach page two. `FeedNav` is wired correctly. It never fires, since no button on the message carries its route. Everything below puts the buttons back.

A string is one of three shapes `render` can return.

{/* prettier-ignore-start */}

| what you return        | what seedcord sends                                         |
| ---------------------- | ----------------------------------------------------------- |
| a string               | one text component holding it                               |
| an array of components | those components, in order                                  |
| a whole reply object   | exactly that, so `files` and `allowedMentions` pass through |

{/* prettier-ignore-end */}

## Putting the buttons back

[`controls.row()`](https://docs.seedcord.org/packages/core/latest/paginator-controls#row) returns a finished action row. Name the keys you want, in the order you want them on screen.

```ts title="src/components/FeedCard.ts"
import { BuilderComponent } from '@seedcord/gateway';

import type {
    PageView,
    PaginatorControls
} from '@seedcord/gateway';

interface Post {
    id: number;
    text: string;
}

export class FeedCard extends BuilderComponent<'container'> {
    constructor(view: PageView<Post>, controls: PaginatorControls) {
        super('container');

        const lines = view.items
            .map((post) => `- ${post.text}`)
            .join('\n');

        this.instance
            .addTextDisplayComponents((text) =>
                text.setContent(lines)
            )
            .addActionRowComponents(
                controls.row('prev', 'indicator', 'next')
            );
    }
}
```

Now `render` takes both arguments and hands the card back.

```ts
export const Feed = new Paginator({
    prefix: 'feed',
    source: new CursorSource((_ctx, page, perPage) =>
        fetchPosts(page, perPage)
    ),
    render: (view, controls) => [
        new FeedCard(view, controls).component
    ]
});
```

The `controls.row('prev', 'indicator', 'next')` call inside `FeedCard` builds each button with its target page already set, and disables it at the ends of the list. **Prev** on page one comes back greyed out with nothing left for you to wire.

### The control keys

`row()` and `button()` both take one of five keys.

```ts
controls.row('
```

The indicator is a disabled button showing the page text, `Page 2 of 4` when the source counted and `Page 2` when it couldn't. A cursor source never counts, so `last` has no page to point at and `controls` always disables it there. `first` still works, since page zero needs no total. The fence above skips both and keeps three keys, the same row seedcord's default draws for a cursor source.

An [`ArraySource`](https://docs.seedcord.org/packages/gateway/latest/array-source) knows its total, so a row there can take every key.

`row()` throws on an empty list, on more than five keys, and on the same key twice. Two copies of one control get the same custom id, which Discord rejects.

### One button at a time

`row()` builds every button with its default label and color. [`controls.button()`](https://docs.seedcord.org/packages/core/latest/paginator-controls#button) returns them one at a time so you can pass cosmetics.

```ts
this.instance.addActionRowComponents((row) =>
    row.addComponents(
        controls.button('prev', {
            style: ButtonStyle.Primary,
            emoji: { name: '⬅️' }
        }),
        controls.button('indicator'),
        controls.button('next', {
            style: ButtonStyle.Primary,
            emoji: { name: '➡️' }
        })
    )
);
```

{/* prettier-ignore-start */}

| option  | what it sets                                                          |
| ------- | --------------------------------------------------------------------- |
| `label` | the button text                                                       |
| `style` | `Primary`, `Secondary`, `Success`, or `Danger`                        |
| `emoji` | an emoji payload, `{ name }` for unicode or `{ id }` for a custom one |

{/* prettier-ignore-end */}

An emoji with no `label` gives an icon-only button, which is what `prev` and `next` are above. The indicator keeps its page text either way.

> **Warning**
>
> Call cosmetic setters only. `setCustomId` overwrites the page the button targets. `setStyle` with `ButtonStyle.Link` or `ButtonStyle.Premium` strips the custom id entirely, since Discord allows no custom id on either style. Any of the three leaves a button that reaches no handler.

If your own header prints the total, check `view.totalPages` first, since a cursor source leaves it `undefined`. [Sources](/components/pagination-sources) lists every field on a `PageView`.

[Paging it yourself](/components/pagination-headless) is the last step, dropping the paginator and keeping the page math.
