Widgets and rendering

Passing data

Some widgets contain fixed text, others can show dynamic contents For example:

  • Const("Hello, {name}!") will be rendered as Hello, {name}!

  • Format("Hello, {name}!") will interpolate with window data and transformed to something like Hello, Tishka17!

So, widgets can use data. But data must be loaded from somewhere. To do it Windows and Dialogs have getter attribute. Getter can be either a function returning data or static dict or list of such objects.

So let’s create a function and use it to enrich our window with data.

Note

In this and later examples we will skip common bot creation and dialog registration code unless it has notable differences with quickstart

from aiogram.filters.state import StatesGroup, State

from aiogram_dialog import Window, Dialog
from aiogram_dialog.widgets.kbd import Button
from aiogram_dialog.widgets.text import Const, Format


class MySG(StatesGroup):
    main = State()


async def get_data(**kwargs):
    return {
        "name": "Tishka17",
    }


dialog = Dialog(
    Window(
        Format("Hello, {name}!"),
        Button(Const("Useless button"), id="nothing"),
        state=MySG.main,
        getter=get_data,  # here we set our data getter
    )
)

It will look like:

_images/getter.png

Since version 1.6 you do not need getter to access some common objects:

  • dialog_data -contents of corresponding field from current context. Normally it is used to store data between multiple calls and windows withing single dialog

  • start_data - data passed during current dialog start. It is also accessible using current_context

  • middleware_data - data passed from middlewares to handler. Same as dialog_manager.data

  • event - current processing event which triggered window update. Be careful using it, because different types of events can cause refreshing same window.

Widget types

Base information

Currently there are 4 kinds of widgets: texts, keyboards and media.

  • Texts used to render text anywhere in dialog. It can be message text, button title and so on.

  • Keyboards represent parts of InlineKeyboard

  • Media represent media attachment to message

  • Input allows to process incoming messages from user. Is has no representation.

Also there are 2 general types:

  • Whenable can be hidden or shown depending on data or some conditions. Currently all widgets are whenable. See: Hiding widgets

  • Actionable is any widget with action (currently only any type of keyboard). It has id and can be found by that id. It recommended for all stateful widgets (e.g Checkboxes) to have unique id within dialog. Buttons with different behavior also must have different ids.

Note

Widget id can contain only ascii letters, numbers, underscore and dot symbol.

  • 123, com.mysite.id, my_item - valid ids

  • hello world, my:item, птичка - invalid ids

Text widget types

Every time you need to render text use any of text widgets:

  • Const - returns text with no midifications

  • Format - formats text using format function. If used in window the data is retrived via getter funcion.

  • Multi - multiple texts, joined with a separator

  • Case - shows one of texts based on condition

  • Progress - shows a progress bar

  • List - shows a dynamic group of texts (similar to Select keyboard widget)

  • Jinja - represents a HTML rendered using jinja2 template

Keyboard widget types

Each keyboard provides one or multiple inline buttons. Text on button is rendered using text widget

  • Button - single inline button. User provided on_click method is called when it is clicked.

  • Url - single inline button with url

  • SwitchInlineQuery - single inline button to switch inline mode

  • Group - any group of keyboards one above another or rearranging buttons.

  • Row - simplified version of group. All buttons placed in single row.

  • Column - another simplified version of group. All buttons placed in single column one per row.

  • ScrollingGroup - the same as the Group, but with the ability to scroll through pages with buttons.

  • ListGroup - group of widgets applied repeated multiple times for each item in list

  • Checkbox - button with two states

  • Select - dynamic group of buttons intended for selection use.

  • Radio - switch between multiple items. Like select but stores chosen item and renders it differently.

  • Multiselect - selection of multiple items. Like select/radio but stores all chosen items and renders them differently.

  • Calendar - simulates a calendar in the form of a keyboard.

  • Counter - couple of buttons +/- to input a number

  • SwitchTo - switches window within a dialog using provided state

  • Next/Back - switches state forward or backward

  • Start - starts a new dialog with no params

  • Cancel - closes the current dialog with no result. An underlying dialog is shown

Media widget types

  • StaticMedia - simple way to share media by url or file path

  • DynamicMedia - some media attachment constructed dynamically

Combining texts

To combine multiple texts you can use Multi widget. You can use any texts inside it. Also you can provide a string separator. In simple cases you can just concatenate widgets using + operator.

from aiogram_dialog.widgets.text import Multi, Const, Format


# let's assume this is our window data getter
async def get_data(**kwargs):
    return {"name": "Tishka17"}


# This will produce text `Hello! And goodbye!`
text = Multi(
    Const("Hello!"),
    Const("And goodbye!"),
    sep=" ",
)

# This one will produce text `Hello, Tishka17, and goodbye {name}!`
text2 = Multi(
    Format("Hello, {name}"),
    Const("and goodbye {name}!"),
    sep=", ",
)

# This one will produce `01.02.2003T04:05:06`
text3 = Multi(
    Multi(Const("01"), Const("02"), Const("2003"), sep="."),
    Multi(Const("04"), Const("05"), Const("06"), sep=":"),
    sep="T"
)

To select one of the texts depending on some condition you should use Case. The condition can be either a data key or a function:

from typing import Dict

from aiogram_dialog import DialogManager
from aiogram_dialog.widgets.text import Case, Const, Format


# let's assume this is our window data getter
async def get_data(**kwargs):
    return {"color": "red", "number": 42}


# This will produce text `Square`
text = Case(
    {
        "red": Const("Square"),
        "green": Const("Unicorn"),
        "blue": Const("Moon"),
    },
    selector="color",
)


# This one will produce text `42 is even!`
def parity_selector(data: Dict, case: Case, manager: DialogManager):
    return data["number"] % 2


text2 = Case(
    {
        0: Format("{number} is even!"),
        1: Const("It is Odd"),
    },
    selector=parity_selector,
)

Jinja HTML rendering

It is very easy to create safe HTML messages using Jinja2 templates. Documentation for template language is available at official jinja web page

To use it you need to create text using Jinja class instead of Format and set proper parse_mode. If you do not want to set default parse mode for whole bot you can set it per-window.

For example you can use environment substitution, cycles and filters:

from aiogram.filters.state import StatesGroup, State

from aiogram_dialog import Window
from aiogram_dialog.widgets.text import Jinja


class DialogSG(StatesGroup):
    ANIMALS = State()


# let's assume this is our window data getter
async def get_data(**kwargs):
    return {
        "title": "Animals list",
        "animals": ["cat", "dog", "my brother's tortoise"]
    }


html_text = Jinja("""
<b>{{title}}</b>
{% for animal in animals %}
* <a href="https://yandex.ru/search/?text={{ animal }}">{{ animal|capitalize }}</a>
{% endfor %}
""")

window = Window(
    html_text,
    parse_mode="HTML",  # do not forget to set parse mode
    state=DialogSG.ANIMALS,
    getter=get_data
)

It will be rendered to this HTML:

<b>Animals list</b>
* <a href="https://yandex.ru/search/?text=cat">Cat</a>
* <a href="https://yandex.ru/search/?text=dog">Dog</a>
* <a href="https://yandex.ru/search/?text=my brother&#39;s tortoise">My brother&#39;s tortoise</a>

If you want to add custom filters or do some configuration of jinja Environment you can setup it using aiogram_dialog.widgets.text.setup_jinja function

Keyboards

Button

In simple case you can use keyboard consisting of single button. Button consts of text, id, on-click callback and when condition.

Text can be any Text widget, that represents plain text. It will receive window data so your button will have dynamic caption

Callback is normal async function. It is called when user clicks a button Unlike normal handlers you should not call callback.answer(), as it is done automatically.

from aiogram.types import CallbackQuery

from aiogram_dialog import DialogManager
from aiogram_dialog.widgets.kbd import Button
from aiogram_dialog.widgets.text import Const


async def go_clicked(callback: CallbackQuery, button: Button,
                     manager: DialogManager):
    await callback.message.answer("Going on!")


go_btn = Button(
    Const("Go"),
    id="go",  # id is used to detect which button is clicked
    on_click=go_clicked,
)
_images/button.png

If it is unclear to you where to put button, check Quickstart

Url

Url represents a button with an url. It has no callbacks because telegram does not provide any notifications on click.

Url itself can be any text (including Const or Format)

from aiogram_dialog.widgets.kbd import Url
from aiogram_dialog.widgets.text import Const

go_btn = Url(
    Const("Github"),
    Const('https://github.com/Tishka17/aiogram_dialog/'),
)
_images/url.png

Grouping buttons

Normally you will have more than one button in your keyboard.

Simplest way to deal with it - unite multiple buttons in a Row, Column or other Group. All these widgets can be used anywhere you can place a button.

Row widget is used to place all buttons inside single row. You can place any keyboard widgets inside it (for example buttons or groups) and it will ignore any hierarchy and just place telegram buttons in a row.

from aiogram.types import CallbackQuery

from aiogram_dialog import DialogManager
from aiogram_dialog.widgets.kbd import Button, Row
from aiogram_dialog.widgets.text import Const


async def go_clicked(callback: CallbackQuery, button: Button,
                     manager: DialogManager):
    await callback.message.answer("Going on!")


async def run_clicked(callback: CallbackQuery, button: Button,
                      manager: DialogManager):
    await callback.message.answer("Running!")


row = Row(
    Button(Const("Go"), id="go", on_click=go_clicked),
    Button(Const("Run"), id="run", on_click=run_clicked),
    Button(Const("Fly"), id="fly"),
)
_images/row.png

Column widget is like a row, but places everything in a column, also ignoring hierarchy.

from aiogram_dialog.widgets.kbd import Button, Column
from aiogram_dialog.widgets.text import Const

column = Column(
    Button(Const("Go"), id="go"),
    Button(Const("Run"), id="run"),
    Button(Const("Fly"), id="fly"),
)
_images/column.png

Group widget does more complex unions. By default, it places one keyboard below another. For example, you can stack multiple rows (or groups, or whatever)

from aiogram_dialog.widgets.kbd import Button, Group, Row
from aiogram_dialog.widgets.text import Const

group = Group(
    Row(
        Button(Const("Go"), id="go"),
        Button(Const("Run"), id="run"),
    ),
    Button(Const("Fly"), id="fly"),
)
_images/group.png

Also it can be used to produce rows of fixed width. To do it just set width to desired value. Row and Column widgets are groups with predefined width.

from aiogram_dialog.widgets.kbd import Button, Group
from aiogram_dialog.widgets.text import Const

group = Group(
    Button(Const("Crawl"), id="crawl"),
    Button(Const("Go"), id="go"),
    Button(Const("Run"), id="run"),
    Button(Const("Fly"), id="fly"),
    Button(Const("Teleport"), id="tele"),
    width=2,
)
_images/group_width.png

ScrollingGroup widget combines buttons into pages with the ability to scroll forward and backward and go to the last or first page with buttons. You can set the height and width of the keyboard. If there are not enough buttons for the last page, the keyboard will be filled with empty buttons keeping the specified height and width.

from aiogram_dialog.widgets.kbd import Button, ScrollingGroup
from aiogram_dialog.widgets.text import Const


def test_buttons_creator(btn_quantity):
    buttons = []
    for i in btn_quantity:
        i = str(i)
        buttons.append(Button(Const(i), id=i))
    return buttons


test_buttons = test_buttons_creator(range(0, 100))

scrolling_group = ScrollingGroup(
    *test_buttons,
    id="numbers",
    width=6,
    height=6,
)
_images/scrolling_group1.png _images/scrolling_group2.png

Checkbox

Some of the widgets are stateful. They have some state which is affected by on user clicks.

One of such widgets is Checkbox. It can be in checked and unchecked state represented by two texts. On each click it inverses its state.

If a dialog with checkbox is visible, you can check its state by calling is_checked method and change it calling set_checked

As button has on_click callback, checkbox has on_state_changed which is called each time state switched regardless the reason

from aiogram_dialog import DialogManager, ChatEvent
from aiogram_dialog.widgets.kbd import Checkbox, ManagedCheckboxAdapter
from aiogram_dialog.widgets.text import Const


async def check_changed(event: ChatEvent, checkbox: ManagedCheckboxAdapter,
                        manager: DialogManager):
    print("Check status changed:", checkbox.is_checked())


check = Checkbox(
    Const("✓  Checked"),
    Const("Unchecked"),
    id="check",
    default=True,  # so it will be checked by default,
    on_state_changed=check_changed,
)
_images/checkbox_checked.png _images/checkbox_unchecked.png

Note

State of widget is stored separately for each separate opened dialog. But all windows in dialog share same storage. So, multiple widgets with same id will share state. But at the same time if you open several copies of same dialogs they will not mix their states

Select

Select acts like a group of buttons but data is provided dynamically. It is mainly intended to use for selection a item from a list.

Normally text of selection buttons is dynamic (e.g. Format). During rendering an item text, it is passed a dictionary with:

  • item - current item itself

  • data - original window data

  • pos - position of item in current items list starting from 1

  • pos0 - position starting from 0

So the main required thing is items. Normally it is a string with key in your window data. The value by this key must be a collection of any objects. If you have a static list of items you can pass it directly to a select widget instead of providing data key.

Next important thing is ids. Besides a widget id you need a function which can return id (string or integer type) for any item.

import operator
from typing import Any

from aiogram.types import CallbackQuery

from aiogram_dialog import DialogManager
from aiogram_dialog.widgets.kbd import Select
from aiogram_dialog.widgets.text import Format


# let's assume this is our window data getter
async def get_data(**kwargs):
    fruits = [
        ("Apple", '1'),
        ("Pear", '2'),
        ("Orange", '3'),
        ("Banana", '4'),
    ]
    return {
        "fruits": fruits,
        "count": len(fruits),
    }


async def on_fruit_selected(callback: CallbackQuery, widget: Any,
                            manager: DialogManager, item_id: str):
    print("Fruit selected: ", item_id)


fruits_kbd = Select(
    Format("{item[0]} ({pos}/{data[count]})"),  # E.g `✓ Apple (1/4)`
    id="s_fruits",
    item_id_getter=operator.itemgetter(1),
    # each item is a tuple with id on a first position
    items="fruits",  # we will use items from window data at a key `fruits`
    on_click=on_fruit_selected,
)
_images/select.png

Note

Select places everything in single row. If it is not suitable for your case - simply wrap it with Group or Column

Radio

Radio is staeful version of select widget. It marks each clicked item as checked deselecting others. It stores which item is selected so it can be accessed later

Unlike for the Select you need two texts. First one is used to render checked item, second one is for unchecked. Passed data is the same as for Select

Unlike in normal buttons and window they are used to render an item, but not the window data itself.

Also you can provide on_state_changed callback function. It will be called when selected item is changed.

import operator

from aiogram_dialog.widgets.kbd import Radio
from aiogram_dialog.widgets.text import Format


# let's assume this is our window data getter
async def get_data(**kwargs):
    fruits = [
        ("Apple", '1'),
        ("Pear", '2'),
        ("Orange", '3'),
        ("Banana", '4'),
    ]
    return {
        "fruits": fruits,
        "count": len(fruits),
    }


fruits_kbd = Radio(
    Format("🔘 {item[0]}"),  # E.g `🔘 Apple`
    Format("⚪️ {item[0]}"),
    id="r_fruits",
    item_id_getter=operator.itemgetter(1),
    items="fruits",
)
_images/radio.png

Useful methods:

  • get_checked - returns an id of selected items

  • is_checked - returns if certain id is currently selected

  • set_checked - sets the selected item by id

Multiselect

Multiselect is another kind of stateful selection widget. It very similar to Radio but remembers multiple selected items

Same as for Radio you should pass two texts (for checked and unchecked items). Passed data is the same as for Select

import operator

from aiogram_dialog.widgets.kbd import Multiselect
from aiogram_dialog.widgets.text import Format


# let's assume this is our window data getter
async def get_data(**kwargs):
    fruits = [
        ("Apple", '1'),
        ("Pear", '2'),
        ("Orange", '3'),
        ("Banana", '4'),
    ]
    return {
        "fruits": fruits,
        "count": len(fruits),
    }


fruits_kbd = Multiselect(
    Format("✓ {item[0]}"),  # E.g `✓ Apple`
    Format("{item[0]}"),
    id="m_fruits",
    item_id_getter=operator.itemgetter(1),
    items="fruits",
)

After few clicks it will look like:

_images/multiselect.png

Other useful options are:

  • min_selected - limits minimal number of selected items ignoring clicks if this restriction is violated. It does not affect initial state.

  • max_selected - limits maximal number of selected items

  • on_state_changed - callback function. Called when item changes selected state

To work with selection you can use this methods:

  • get_checked - returns a list of ids of all selected items

  • is_checked - returns if certain id is currently selected

  • set_checked - changes selection state of provided id

  • reset_checked - resets all checked items to unchecked state

Warning

Multiselect widgets stores state of all checked items even if they disappear from window data. It is very useful when you have pagination, but might be unexpected when data is really removed.

Calendar

Calendar widget allows you to display the keyboard in the form of a calendar, flip through the months and select the date. The initial state looks like the days of the current month. It is possible to switch to the state for choosing the month of the current year or in the state of choosing years.

from datetime import date

from aiogram.types import CallbackQuery

from aiogram_dialog import DialogManager
from aiogram_dialog.widgets.kbd import Calendar


async def on_date_selected(callback: CallbackQuery, widget,
                           manager: DialogManager, selected_date: date):
    await callback.answer(str(selected_date))


calendar = Calendar(id='calendar', on_click=on_date_selected)
_images/calendar1.png _images/calendar2.png _images/calendar3.png

Media

StaticMedia

StaticMedia allows you to share media files by their path os URLs. Though address supports string interpolation as it can be Text widget, other parameters remain static.

You can use it providing path or url to the file, it’s ContentType and additional parameters if required. Also you might need to change media type (type=ContentType.Photo) or provide any additional params supported by aiogram using media_params

Be careful using relative paths. Mind the working directory.

from aiogram_dialog.widgets.media import StaticMedia

windows = Window(
    StaticMedia(
        path="/home/tishka17/python_logo.png"),
        type=ContentType.PHOTO,
    ),
    state=DialogSG.greeting,
)

It will look like:

_images/static_media.png

For more complex cases you can read source code of StaticMedia and create your own widget with any logic you need.

Note

Telegram allows to send files using file_id instead of uploading same file again. This make media sending much faster. aiogram_dialog uses this feature and caches sent file ids in memory

If you want to persistent file_id cache, implement MediaIdStorageProtocol and pass instance to your dialog registry

DynamicMedia

StaticMedia allows you to share any supported media files. Just return a MediaAttachment from data getter and set selector for a field name. Other option is to pass a callable returning MediaAttachment as a selector

Other media sources

Sometimes you have some custom sources for media files: neither file in fulesystem, not URL in the interner, nor existing file in telegram. It could be some internal storage like database or private s3-compatible one or even runtime generated objects.

In this case recommended steps to solve a problem are:

  1. Generate some custom URI identifying you media. It could be string like “bot://1234” or whatever you want

  2. Inherit from MessageManager class and redefine get_media_source method to load data identified by your URI from custom source

  3. Pass you message manager instance when constructing Registry

With such implementation you will be able to implement custom media retrieving and keep usage of existing media widgets and file id caching

Hiding widgets

Actually every widget can be hidden including texts, buttons, groups and so on. It is managed by when attribute. It can be either a data key, a predicate function or a F-filter (from magic-filter)

from typing import Dict

from aiogram.filters.state import StatesGroup, State
from magic_filter import F

from aiogram_dialog import Window, DialogManager
from aiogram_dialog.widgets.common import Whenable
from aiogram_dialog.widgets.kbd import Button, Row, Group
from aiogram_dialog.widgets.text import Const, Format, Multi


class MySG(StatesGroup):
    main = State()


async def get_data(**kwargs):
    return {
        "name": "Tishka17",
        "extended": False,
    }


def is_tishka17(data: Dict, widget: Whenable, manager: DialogManager):
    return data.get("name") == "Tishka17"


window = Window(
    Multi(
        Const("Hello"),
        Format("{name}", when="extended"),
        sep=" "
    ),
    Group(
        Row(
            Button(Const("Wait"), id="wait"),
            Button(Const("Ignore"), id="ignore"),
            when=F["extended"],
        ),
        Button(Const("Admin mode"), id="nothing", when=is_tishka17),
    ),
    state=MySG.main,
    getter=get_data,
)
_images/whenable.png

If you only change data setting "extended": True the window will look differently

_images/whenable_extended.png