A library for creating forms in aiogram3
pip install aiogram3-form
Form is a set of fields that you want your user to fill in. Forms are defined as classses derived from aiogram3_form.Form
and contain fields with annotated types and aiogram3_form.FormField
values. You should not inherit your form class from your another form class (so no deep inheritance).
When you start your form with .start()
method, your bot will ask a user input values one by one until the end. Filling in the last field of a form means "submitting" it.
Forms use default aiogram FSM.
import asyncio
from aiogram import Bot, Dispatcher, F
from aiogram.fsm.context import FSMContext
from aiogram3_form import Form, FormField
bot = Bot(token="YOUR_TOKEN")
dispatcher = Dispatcher()
class NameForm(Form):
first_name: str = FormField(enter_message_text="Enter your first name please")
second_name: str = FormField(
enter_message_text="Enter your second name please",
filter=(F.text.len() > 10) & F.text,
)
age: int = FormField(enter_message_text="Enter age as integer")
@NameForm.submit(
router=dispatcher, # also might use aiogram.Router here
clear_state=False, # if you want to keep fsm state,
# defaults to True (sets state to None before calling your submit function)
)
async def name_form_submit_handler(form: NameForm):
# handle form submitted data
# also supports aiogram standart DI (e. g. middleware, filter data, etc.)
# you can do anything you want in here
# .answer() method of a form object is not the only way to reply
# use can also use bot.send_message with form.chat_id for example
await form.answer(f"{form.first_name} {form.second_name} of age {form.age}")
@dispatcher.message(F.text == "/form")
async def form_handler(_, state: FSMContext):
await NameForm.start(bot, state) # start your form
asyncio.run(dispatcher.start_polling(bot))
You can use any type of keyboard in your form, but only reply keyboards are actually useful (because forms only accept messages as input, so no callback queries).
from aiogram.utils.keyboard import ReplyKeyboardBuilder
from aiogram.types import KeyboardButton
FRUITS = ("Orange", "Apple", "Banana")
fruits_markup = (
ReplyKeyboardBuilder().add(*(KeyboardButton(text=f) for f in FRUITS)).as_markup()
)
class FruitForm(Form):
fruit: str = FormField(
enter_message_text="Pick your favorite fruit",
filter=F.text.in_(FRUITS) & F.text,
reply_markup=fruits_markup,
)
Default filters are built in for types: str
(checks for message text and returns it), int
(tries to convert message text to int), float
, datetime.date
, datetime.datetime
, aiogram.types.PhotoSize
, aiogram.types.Document
, aiogram.types.Message
Supported form filter kinds: sync function, async function, aiogram magic filter.
If your filter is a function (sync or async), it should take aiogram.types.Message
as the first argument and return False
, if it does not pass. If a filter passed, it should return the value you want in your form data.
Magic filters return None
on failure, so it's a special case that is handled differently.
If your filter fails and error_message_text
is provided in FormField
call, an error message will be sent with the provided text.
def sync_fruit_form_filter(message: types.Message) -> str | bool:
if message.text not in FRUITS:
return False
return message.text
async def async_fruit_form_filter(
message: types.Message,
): ... # some async fruit filtering
class FruitForm(Form):
fruit: str = FormField(
enter_message_text="Pick your favorite fruit",
filter=sync_fruit_form_filter, # you can pass an async filter here as well
reply_markup=fruits_markup,
error_message_text="Thats an invalid fruit",
)
Enter callbacks enable you to write your own enter functions for form fields. In case you provide your own enter callback, the enter_message_text
parameter of aiogram3.FormField
will be ignored.
from aiogram import types
async def enter_fruit_callback(chat_id: int, user_id: int, data: dict[str, Any]) -> types.Message:
# do whatever you want in here
print(f"user {user_id} has just entered the fruit callback in chat {chat_id}!")
return await bot.send_message(
chat_id, "Hey, pick your favorite fruit please", reply_markup=fruits_markup
)
class FruitForm(Form):
fruit: str = FormField(
enter_callback=enter_fruit_callback,
filter=sync_fruit_form_filter,
error_message_text="Thats an invalid fruit",
)