Upgrading¶
Breaking changes that need more than a one-line fix, with the steps to upgrade. Every release's changes are in the Release Notes.
To 26.4.4: Material Design 3¶
framework="material" was rebuilt on Material Design 3. Forms keep working with
no code change, and only apps that customize the Material style need to act.
| Changed | What to do |
|---|---|
| Material markup and CSS classes | Update CSS written against the old markup. The form keeps its md-form class; see Material Design 3 for the anatomy. |
| Colors | Override --md-sys-color-* tokens on form.md-form |
MaterialEmbeddedTheme |
Renamed MaterialTheme |
Materialize, and framework_css_tag()/framework_js_tag() for "material" |
Removed: the style never loaded Materialize, and both functions now return nothing for "material". Don't load Materialize for these forms. |
To 26.4.3: one way to build a form¶
The builder API, the React and Vue adapters and @form_validator are
removed. Importing a removed name raises an ImportError that names its
replacement.
| Removed | Replacement |
|---|---|
FormBuilder, AutoFormBuilder, create_form_from_model |
A FormModel class. For fields known only at runtime, create_model(..., __base__=FormModel); see Forms Built at Runtime. |
create_login_form, create_registration_form, create_contact_form |
The Login and Registration and Contact Form recipes |
render_form_page, FormIntegration, handle_form, handle_form_async |
render_form_html(), FormModel.validate() and result.render_with_errors(); see Handling Submissions |
@form_validator (it never ran) |
Pydantic's @model_validator(mode="after"), whose message lands in the form-wide "_errors", or FormValidator.add_cross_field_rule() |
ReactJSONSchemaFormAdapter, VueFormulateAdapter |
FormModel.get_pydantic_json_schema() or as_api_model(); see JSON API and JSON Schema |
DataTableLayout inputs named rows[i].col, csv_file |
The same names under the embedding field: employees.rows[i].col, employees.csv_file. Only code that reads these keys directly changes. |
To 26.4.1: nested forms¶
Version 26.4.1 removed the older ways of combining forms. Using any of them
raises FormLayoutRemovedError whose message contains these steps.
| Removed | Replacement |
|---|---|
TabbedLayout, VerticalLayout, HorizontalLayout wrapper classes |
Nested FormModel fields plus layout="tabbed" / "vertical" / "side-by-side" |
ListLayout(Item, ...) |
items: list[Item] = Field(default_factory=list, title="Items") |
SectionDesign(section_title=..., ...) |
Field(title=..., ui_help_text=..., ui_collapsible=True) on the sub-form field |
FormDesign(...) |
Arguments to render_form_html(...) and your page template |
parse_nested_form_data(...) in routes |
Pass the request form straight to validate() |
validate(..., flatten=True) |
Removed; result.data is always nested |
Error keys like "pets[0].name" |
result.errors["pets"][0]["name"] |
errors={"general": ...} |
errors={"_errors": ...} |
layout="tabbed" on a flat form (keyword-guessed tabs) |
Split the form into sub-forms; each becomes a tab |
Several input_type="layout" fields auto-tabbing |
layout="tabbed" on the parent |
Before:
class ContactTab(VerticalLayout):
form = Contact
class AddressTab(VerticalLayout):
form = Address
class ProfileTabs(TabbedLayout):
contact = ContactTab()
address = AddressTab()
class ProfileForm(FormModel):
tabs: ProfileTabs = Field(default_factory=ProfileTabs, input_type='layout')
data = parse_nested_form_data(await request.form())
result = ProfileForm.validate(data, submit_url='/profile')
city_error = result.errors.get('city')
After:
class ProfileForm(FormModel):
contact: Contact = Field(title='Contact')
address: Address = Field(title='Address')
html = render_form_html(ProfileForm, layout='tabbed', submit_url='/profile')
result = ProfileForm.validate(await request.form(), submit_url='/profile', layout='tabbed')
city_error = result.errors.get('address', {}).get('city')
Submitted data and saved records change shape too: the contact fields now
arrive under data["contact"] instead of at the top level.