Skip to content

Forms

Model utilities for Air.

Provides a thin wrapper around :class:pydantic.BaseModel that knows how to generate matching :class:air.forms.AirForm subclasses on demand.

AirModel

Bases: BaseModel

Base class for models that integrate tightly with Air forms.

to_form classmethod

to_form(*, name=None, includes=None, widget=None)

Return an :class:AirForm instance bound to cls.

Parameters:

Name Type Description Default
name str | None

Optional explicit class name for the generated form.

None
includes Sequence[str] | None

Optional iterable of field names to render (defaults to all fields).

None
widget Callable | None

Optional custom rendering callable.

None

Returns:

Type Description
AirForm

An instance of :class:AirForm that validates against cls.

Example:

from collections.abc import Sequence

from pydantic import BaseModel

import air
from air.forms import default_form_widget

app = air.Air()


class ContactModel(air.AirModel):
    name: str
    email: str
    phone: str | None = None


def custom_widget(
    model: type[BaseModel],
    data: dict | None = None,
    errors: list | None = None,
    includes: Sequence[str] | None = None,
) -> air.Div:
    return air.Div(
        air.P("Custom form styling:"),
        air.Raw(default_form_widget(model, data, errors, includes)),
        class_="custom-form",
    )


def get_contact_form() -> air.AirForm:
    return ContactModel.to_form(
        name="CustomContactForm",  # Custom form class name
        includes=["name", "email"],  # Only render these fields
        widget=custom_widget,  # Custom rendering function
    )


@app.page
def index() -> air.Html:
    contact_form = get_contact_form()
    return air.Html(
        air.H1("Contact Form"),
        air.P("This form demonstrates name, includes, and widget parameters"),
        air.Form(
            contact_form.render(),
            air.Button("Submit", type="submit"),
            method="post",
            action="/submit",
        ),
    )


@app.post("/submit")
async def submit(request: air.Request) -> air.Html:
    form_data = await request.form()
    contact_form = get_contact_form()

    if contact_form.validate(form_data):
        return air.Html(
            air.H1("Success"),
            air.P(f"Name: {contact_form.data.name}"),
            air.P(f"Email: {contact_form.data.email}"),
        )

    return air.Html(
        air.H1("Error"),
        air.P(f"Errors: {len(contact_form.errors or [])}"),
    )


if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app, host="127.0.0.1", port=8000)
Source code in src/air/models.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
@classmethod
def to_form(
    cls,
    *,
    name: str | None = None,
    includes: Sequence[str] | None = None,
    widget: Callable | None = None,
) -> AirForm:
    """Return an :class:`AirForm` instance bound to ``cls``.

    Args:
        name: Optional explicit class name for the generated form.
        includes: Optional iterable of field names to render (defaults to all fields).
        widget: Optional custom rendering callable.

    Returns:
        An instance of :class:`AirForm` that validates against ``cls``.

    Example:

        from collections.abc import Sequence

        from pydantic import BaseModel

        import air
        from air.forms import default_form_widget

        app = air.Air()


        class ContactModel(air.AirModel):
            name: str
            email: str
            phone: str | None = None


        def custom_widget(
            model: type[BaseModel],
            data: dict | None = None,
            errors: list | None = None,
            includes: Sequence[str] | None = None,
        ) -> air.Div:
            return air.Div(
                air.P("Custom form styling:"),
                air.Raw(default_form_widget(model, data, errors, includes)),
                class_="custom-form",
            )


        def get_contact_form() -> air.AirForm:
            return ContactModel.to_form(
                name="CustomContactForm",  # Custom form class name
                includes=["name", "email"],  # Only render these fields
                widget=custom_widget,  # Custom rendering function
            )


        @app.page
        def index() -> air.Html:
            contact_form = get_contact_form()
            return air.Html(
                air.H1("Contact Form"),
                air.P("This form demonstrates name, includes, and widget parameters"),
                air.Form(
                    contact_form.render(),
                    air.Button("Submit", type="submit"),
                    method="post",
                    action="/submit",
                ),
            )


        @app.post("/submit")
        async def submit(request: air.Request) -> air.Html:
            form_data = await request.form()
            contact_form = get_contact_form()

            if contact_form.validate(form_data):
                return air.Html(
                    air.H1("Success"),
                    air.P(f"Name: {contact_form.data.name}"),
                    air.P(f"Email: {contact_form.data.email}"),
                )

            return air.Html(
                air.H1("Error"),
                air.P(f"Errors: {len(contact_form.errors or [])}"),
            )


        if __name__ == "__main__":
            import uvicorn

            uvicorn.run(app, host="127.0.0.1", port=8000)
    """

    return to_form(cls, name=name, includes=includes, widget=widget)