Pagination Examples
Contents
Pagination Examples#
Here are few examples of pagination with nextcord-ext-menus
.
The built-in pagination classes (MenuPages
and ButtonMenuPages
)
handle splitting data into pages, sending the initial message, timeout actions, and button actions for
first page
, previous page
,
next page
, last page
,
and stop
all for you.
The examples below are for button pagination. If you want to use reaction menus, you can simply
replace the ButtonMenuPages
with MenuPages
and replace parameters such as
clear_buttons_after
and disable_buttons_after
with clear_reactions_after
.
Contents
Basic Pagination#
In this example, we’ll create a button pagination menu that shows items in a message for each page.
To get started, we will need a PageSource
object that will define how many list items to
show on each page, and how to display them. In the example below, ListPageSource
is
subclassed to provide these details: per_page
is set to 4 to show four items per page and
format_page
is defined to display a given four entries in a message, each on a separate line.
All that is left is instantiating and starting the menu.
We will use the basic pagination class ButtonMenuPages
to handle the pagination
and pass it the page source which holds our list of entries as the source
parameter.
We can optionally pass additional parameters such as the button style and any parameters
supported by ButtonMenu
. Then we can start the menu by calling
pages.start()
.
from nextcord.ext import commands, menus
bot = commands.Bot(command_prefix="$")
class MyPageSource(menus.ListPageSource):
def __init__(self, data):
# this is where you can set how many items you want per page
super().__init__(data, per_page=4)
async def format_page(self, menu, entries):
# this is where you can format the entries for the page
return "\n".join(entries)
@bot.command()
async def pages_example(ctx):
data = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]
pages = menus.ButtonMenuPages(
source=MyPageSource(data),
delete_message_after=True,
)
await pages.start(ctx)
bot.run('token')
Paginated Embeds Using Fields#
In this example, we will be creating an embed with fields instead of showing the entries in message content.
Since fields have a name and value, the data we pass into the PageSource
is a list of
tuples. The first item in the tuple is the name and the second is the value.
If you do not want to be restricted to having a name and value for each item, you can use the
embed description as shown in the example that follows this one.
The entries argument in format_page
will also be a list of tuples now, so we can iterate
over the entries and create a field for each using entry[0]
and entry[1]
as the name
and value respectively.
The return value of format_page
is a nextcord.Embed
which will appear in the
message when the page is shown.
class MyEmbedFieldPageSource(menus.ListPageSource):
def __init__(self, data):
super().__init__(data, per_page=4)
async def format_page(self, menu, entries):
embed = nextcord.Embed(title="Entries")
for entry in entries:
embed.add_field(name=entry[0], value=entry[1], inline=True)
embed.set_footer(text=f'Page {menu.current_page + 1}/{self.get_max_pages()}')
return embed
@bot.command()
async def button_embed_field(ctx):
fields = [
("Black", "#000000"),
("Blue", "#0000FF"),
("Brown", "#A52A2A"),
("Green", "#00FF00"),
("Grey", "#808080"),
("Orange", "#FFA500"),
("Pink", "#FFC0CB"),
("Purple", "#800080"),
("Red", "#FF0000"),
("White", "#FFFFFF"),
("Yellow", "#FFFF00"),
]
pages = menus.ButtonMenuPages(
source=MyEmbedFieldPageSource(fields),
clear_buttons_after=True,
)
await pages.start(ctx)
Paginated Embeds Using Descriptions#
In this example, we will use the embed description to show entries.
data
is a list of strings, so we can join entries with "\n"
to create the description.
class MyEmbedDescriptionPageSource(menus.ListPageSource):
def __init__(self, data):
super().__init__(data, per_page=6)
async def format_page(self, menu, entries):
embed = Embed(title="Entries", description="\n".join(entries))
embed.set_footer(text=f'Page {menu.current_page + 1}/{self.get_max_pages()}')
return embed
@bot.command()
async def button_embed_description(ctx):
data = [f'Description for entry #{num}' for num in range(1, 51)]
pages = menus.ButtonMenuPages(
source=MyEmbedDescriptionPageSource(data),
disable_buttons_after=True,
)
await pages.start(ctx)
Custom Emojis#
To use custom emojis in pagination, you can subclass ButtonMenuPages
and override
the FIRST_PAGE
, PREVIOUS_PAGE
, NEXT_PAGE
. LAST_PAGE
, and STOP
attributes.
Then, when instantiating the menu, you will use your custom class’s name in place of
menus.ButtonMenuPages
.
class CustomButtonMenuPages(menus.ButtonMenuPages):
FIRST_PAGE = "<:pagefirst:899973860772962344>"
PREVIOUS_PAGE = "<:pageprev:899973860965888010>"
NEXT_PAGE = "<:pagenext:899973860840050728>"
LAST_PAGE = "<:pagelast:899973860810694686>"
STOP = "<:stop:899973861444042782>"
@bot.command()
async def custom_buttons(ctx):
data = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]
pages = CustomButtonMenuPages(source=MySource(data))
await pages.start(ctx)
GroupByPageSource#
GroupByPageSource
is an alternative to ListPageSource
that allows you to
group entries into multiple sublists similar to itertools.groupby()
. Only entries
having the same group key will be displayed together on a single page. In the example below,
there are three keys: test
, other
, and okay
. Entries will be paginated within
each group, but when all entries from a group have been displayed, the next group will only
start on the next page.
class Test:
def __init__(self, key, value):
self.key = key
self.value = value
data = [
Test(key=key, value=value)
for key in ['test', 'other', 'okay']
for value in range(20)
]
class Source(menus.GroupByPageSource):
async def format_page(self, menu, entry):
joined = '\n'.join(f'{i}. <Test value={v.value}>' for i, v in enumerate(entry.items, start=1))
return f'**{entry.key}**\n{joined}\nPage {menu.current_page + 1}/{self.get_max_pages()}'
@bot.command()
async def group_by_page_source_example(ctx):
pages = menus.ButtonMenuPages(
source=Source(data, key=lambda t: t.key, per_page=12),
clear_reactions_after=True,
)
await pages.start(ctx)
AsyncIteratorPageSource#
Another way to paginate is to use an AsyncIteratorPageSource
which works with async
iterators for lazy fetching of data. This is useful when you have a large amount of data to
paginate and you don’t want to load all of it into memory.
Instead of a list of data, you pass a generator that yields entries as they are needed.
class Test:
def __init__(self, value):
self.value = value
def __repr__(self):
return f'<Test value={self.value}>'
async def generate(number):
for i in range(number):
yield Test(i)
class Source(menus.AsyncIteratorPageSource):
def __init__(self):
super().__init__(generate(9), per_page=4)
async def format_page(self, menu, entries):
start = menu.current_page * self.per_page
return f'\n'.join(f'{i}. {v!r}' for i, v in enumerate(entries, start=start))
@bot.command()
async def async_iterator_page_source_example(ctx):
pages = menus.ButtonMenuPages(source=Source())
await pages.start(ctx)
Paginated Help Command Cog#
Here is an example of a paginated help command with nextcord-ext-menus
.
It can be loaded as an extension just as any other Cog
.
For more details on how this can be used, check out this useful gist.
from typing import List, Tuple
import nextcord
from nextcord.ext import commands, menus
class HelpPageSource(menus.ListPageSource):
"""Page source for dividing the list of tuples into pages and displaying them in embeds"""
def __init__(self, help_command: "NewHelpCommand", data: List[Tuple[str, str]]):
self._help_command = help_command
# you can set here how many items to display per page
super().__init__(data, per_page=2)
async def format_page(self, menu: menus.ButtonMenuPages, entries: List[Tuple[str, str]]):
"""
Returns an embed containing the entries for the current page
"""
prefix = self._help_command.context.clean_prefix
invoked_with = self._help_command.invoked_with
# create embed
embed = nextcord.Embed(title="Bot Commands", colour=self._help_command.COLOUR)
embed.description = (
f'Use "{prefix}{invoked_with} command" for more info on a command.\n'
f'Use "{prefix}{invoked_with} category" for more info on a category.'
)
# add the entries to the embed
for entry in entries:
embed.add_field(name=entry[0], value=entry[1], inline=True)
# set the footer to display the page number
embed.set_footer(text=f'Page {menu.current_page + 1}/{self.get_max_pages()}')
return embed
class HelpButtonMenuPages(menus.ButtonMenuPages):
"""Subclass of ButtonMenuPages to add an interaction_check"""
def __init__(self, ctx: commands.Context, **kwargs):
super().__init__(**kwargs)
self._ctx = ctx
async def interaction_check(self, interaction: nextcord.Interaction) -> bool:
"""Ensure that the user of the button is the one who called the help command"""
return self._ctx.author == interaction.user
class NewHelpCommand(commands.MinimalHelpCommand):
"""Custom help command override using embeds and button pagination"""
# embed colour
COLOUR = nextcord.Colour.blurple()
def get_command_signature(self, command: commands.core.Command):
"""Retrieves the signature portion of the help page."""
return f"{self.context.clean_prefix}{command.qualified_name} {command.signature}"
async def send_bot_help(self, mapping: dict):
"""implements bot command help page"""
prefix = self.context.clean_prefix
invoked_with = self.invoked_with
embed = nextcord.Embed(title="Bot Commands", colour=self.COLOUR)
embed.description = (
f'Use "{prefix}{invoked_with} command" for more info on a command.\n'
f'Use "{prefix}{invoked_with} category" for more info on a category.'
)
# create a list of tuples for the page source
embed_fields = []
for cog, commands in mapping.items():
name = "No Category" if cog is None else cog.qualified_name
filtered = await self.filter_commands(commands, sort=True)
if filtered:
# \u2002 = en space
value = "\u2002".join(f"`{prefix}{c.name}`" for c in filtered)
if cog and cog.description:
value = f"{cog.description}\n{value}"
# add (name, value) pair to the list of fields
embed_fields.append((name, value))
# create a pagination menu that paginates the fields
pages = HelpButtonMenuPages(
ctx=self.context,
source=HelpPageSource(self, embed_fields),
disable_buttons_after=True
)
await pages.start(self.context)
async def send_cog_help(self, cog: commands.Cog):
"""implements cog help page"""
embed = nextcord.Embed(
title=f"{cog.qualified_name} Commands",
colour=self.COLOUR,
)
if cog.description:
embed.description = cog.description
filtered = await self.filter_commands(cog.get_commands(), sort=True)
for command in filtered:
embed.add_field(
name=self.get_command_signature(command),
value=command.short_doc or "...",
inline=False,
)
embed.set_footer(
text=f"Use {self.context.clean_prefix}help [command] for more info on a command."
)
await self.get_destination().send(embed=embed)
async def send_group_help(self, group: commands.Group):
"""implements group help page and command help page"""
embed = nextcord.Embed(title=group.qualified_name, colour=self.COLOUR)
if group.help:
embed.description = group.help
if isinstance(group, commands.Group):
filtered = await self.filter_commands(group.commands, sort=True)
for command in filtered:
embed.add_field(
name=self.get_command_signature(command),
value=command.short_doc or "...",
inline=False,
)
await self.get_destination().send(embed=embed)
# Use the same function as group help for command help
send_command_help = send_group_help
class HelpCog(commands.Cog, name="Help"):
"""Displays help information for commands and cogs"""
def __init__(self, bot: commands.Bot):
self.__bot = bot
self.__original_help_command = bot.help_command
bot.help_command = NewHelpCommand()
bot.help_command.cog = self
def cog_unload(self):
self.__bot.help_command = self.__original_help_command
def setup(bot: commands.Bot):
bot.add_cog(HelpCog(bot))