Recommended resources
The freely available resources this lesson will direct you to.
Hello! In our last lesson, we focused on a crucial architectural concept: designing a structured format for callback_data to represent a hierarchical menu. We established a convention like menu:settings to embed navigation information directly into our buttons, turning them from simple triggers into self-describing links.
Today, we're shifting from design to implementation. We will bring that data structure to life by building the "router" that consumes it. The goal for this lesson is to implement a handler that edits a message to display a sub-menu based on a user's selection. We will write the code that intercepts a button click, parses our custom callback data, and dynamically updates the message to show the correct sub-menu, creating a seamless navigation experience for the user.
From Data to Dynamic UI
Given your background in front-end development, you can think of this process as being very similar to client-side routing in a single-page application. Our structured callback_data string, like menu:settings, acts as a "URL." Our goal now is to build the "router" that listens for these "URL" changes and renders the corresponding "page" or "view"βin our case, by editing the message text and its inline keyboard.
Let's start by defining the two "pages" of our menu: a main menu and a settings sub-menu.
import { Bot, InlineKeyboard } from "grammy";
import "dotenv/config";
// Define the content for our menus
const mainText = "Welcome! This is the main menu. Choose an option:";
const settingsText = "Here are the settings. What would you like to configure?";
// 1. Main Menu Keyboard
const mainMenu = new InlineKeyboard()
.text("π Status", "action:status") // We'll handle actions later
.text("βοΈ Settings", "menu:settings"); // Navigates to the settings menu
// 2. Settings Sub-Menu Keyboard
const settingsMenu = new InlineKeyboard()
.text("π Notifications", "action:toggle_notifs")
.text("β¬
οΈ Back", "menu:main"); // Navigates back to the main menu
Notice how the callback_data for the "Settings" button is "menu:settings" and for the "Back" button is "menu:main". This is the structured data our router will use.
Implementing the Menu Router
Instead of creating separate handlers for every button (bot.callbackQuery("settings", ...)), we will now create a single, powerful handler using a regular expression. This handler will act as our central router for all menu navigation.
// Assume 'bot' is already initialized
// const bot = new Bot(process.env.BOT_TOKEN!);
// The Menu Router
bot.callbackQuery(/^menu:(.+)/, async (ctx) => {
// `ctx.match[1]` will contain the part of the string matched by `(.+)`
// For "menu:settings", `ctx.match[1]` will be "settings"
// For "menu:main", `ctx.match[1]` will be "main"
const destination = ctx.match[1];
// Always answer the callback query to remove the "loading" state
await ctx.answerCallbackQuery();
// Use a switch statement to "route" to the correct menu
switch (destination) {
case "settings":
// Edit the message to show the settings menu
await ctx.editMessageText(settingsText, { reply_markup: settingsMenu });
break;
case "main":
// Edit the message to show the main menu
await ctx.editMessageText(mainText, { reply_markup: mainMenu });
break;
default:
// Optional: handle unknown destinations
await ctx.reply("Sorry, I don't recognize this menu destination.");
}
});
This single handler is now responsible for all navigation events that start with menu:. It parses the destination from the callback data and uses ctx.editMessageText() to seamlessly transition the user between the main menu and the settings sub-menu without sending a new message. This is the core of creating a tech UI that feels like a mini-application within the chat.
The noqta.tn tutorial you can find below provides a good example of using multiple bot.callbackQuery handlers for each button. Our regex approach is a more scalable alternative, and you can now see the trade-offs clearly.
Build a Telegram Bot with grammY and TypeScript: From Zero to ...
This tutorial demonstrates the fundamental pattern of editing a message in response to a callback query. It's a great practical example of the concepts we're discussing.
Focus on Step 4, titled "Inline Keyboards and Callback Queries". Observe how it defines mainMenu and settingsMenu, and then uses separate handlers like bot.callbackQuery("settings", ...) and bot.callbackQuery("back_to_menu", ...) to call ctx.editMessageText. Compare this to our single regex-based router to see how we've centralized that logic.
Complete Example
Let's put all the pieces together into a complete, runnable bot.ts file.
import { Bot, InlineKeyboard, Context } from "grammy";
import "dotenv/config";
// --- Menu Definitions ---
const mainText = "Welcome! This is the main menu. Choose an option:";
const settingsText = "Here are the settings. What would you like to configure?";
const mainMenu = new InlineKeyboard()
.text("π Status", "action:status") // Not handled in this example
.text("βοΈ Settings", "menu:settings");
const settingsMenu = new InlineKeyboard()
.text("π Notifications", "action:toggle_notifs") // Not handled
.text("β¬
οΈ Back", "menu:main");
// --- Bot Initialization ---
const token = process.env.BOT_TOKEN;
if (!token) throw new Error("BOT_TOKEN is not set in .env file");
const bot = new Bot(token);
// --- Command Handlers ---
bot.command("start", async (ctx) => {
await ctx.reply(mainText, {
reply_markup: mainMenu,
});
});
// --- Menu Router ---
bot.callbackQuery(/^menu:(.+)/, async (ctx) => {
const destination = ctx.match[1];
await ctx.answerCallbackQuery();
switch (destination) {
case "settings":
await ctx.editMessageText(settingsText, { reply_markup: settingsMenu });
break;
case "main":
await ctx.editMessageText(mainText, { reply_markup: mainMenu });
break;
default:
// Sending a reply for an unknown destination might be better
// than editing the message, as the context might be lost.
await ctx.reply("Unknown menu destination.");
}
});
// --- Start the Bot ---
bot.start();
console.log("Bot started!");
If you run this code, you'll see that clicking the "βοΈ Settings" button edits the message to show the settings menu, and clicking "β¬ οΈ Back" edits it back to the main menu. You've successfully implemented a hierarchical menu system!
The Official Abstraction: The menu Plugin
Building this router manually is an excellent way to understand the core mechanics. However, as we discussed, this is a very common requirement, and grammY provides a dedicated plugin for it. Our manual implementation maps directly to the concepts within the official menu plugin.
Interactive Menus (menu) | grammY
Now that you've built the logic yourself, let's see how the official plugin abstracts it away. This section will look very familiar.
Find the section titled "Navigation Between Menus". Notice the .submenu() and .back() methods. Our menu:settings callback data is conceptually equivalent to using .submenu("Credits", "credits-menu"), and our menu:main is a manual implementation of .back("Go Back"). The plugin handles the routing and message editing for you declaratively.
By understanding how to build this manually, you're now perfectly positioned to appreciate the convenience and power of the menu plugin when you decide to use it for more complex UIs.
Conclusion
In this lesson, we bridged the gap between data design and a functional user interface. You learned how to implement a central "router" that intercepts structured callback_data, parses it, and edits a message to display the appropriate sub-menu. This pattern is fundamental to building the kind of sophisticated, interactive bot UIs you're aiming for.
Key Takeaways:
- A single handler using a regular expression (
bot.callbackQuery(/^menu:(.+)/, ...) can act as a scalable router for menu navigation. - The matched parts of the regex are available in
ctx.match, allowing you to extract routing information. ctx.editMessageText()is the key method for changing the message content and its inline keyboard to create a smooth, single-message navigation experience.- The manual implementation of a menu router provides a solid foundation for understanding higher-level abstractions like the official grammY
menuplugin.
In our next lesson, we will refine this system by implementing a more robust "back" button functionality, which is crucial for navigating deeper menu hierarchies.