Layouts¶
Once a form has sub-forms, the same model can be shown as stacked sections,
tabs, columns, collapsible panels or a step-by-step wizard. The model doesn't
change: you pick the presentation with one layout= argument.
from pydantic_schemaforms import Field, FormModel
class Contact(FormModel):
email: str = Field(title='Email', ui_element='email')
phone: str = Field('', title='Phone', ui_element='tel')
class Address(FormModel):
street: str = Field(title='Street')
city: str = Field(title='City')
class Preferences(FormModel):
newsletter: bool = Field(False, title='Send me the newsletter')
language: str = Field(
'en', title='Language', ui_element='select', ui_options={'choices': ['en', 'fr', 'de']}
)
class AccountForm(FormModel):
model_config = {'title': 'About you'}
name: str = Field(title='Full name')
contact: Contact = Field(title='Contact details')
address: Address = Field(title='Address')
preferences: Preferences = Field(title='Preferences', ui_order=1)
Pass the layout both when rendering and when validating, so a failed submit comes back in the same layout:
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from models import AccountForm
from pydantic_schemaforms import html, render_form_html_async
app = FastAPI()
FORM_OPTIONS = {'layout': 'tabbed', 'include_framework_assets': True, 'asset_mode': 'cdn'}
@app.get('/account', response_class=HTMLResponse)
async def show_account():
return await render_form_html_async(AccountForm, submit_url='/account', **FORM_OPTIONS)
@app.post('/account', response_class=HTMLResponse)
async def save_account(request: Request):
result = AccountForm.validate(await request.form(), submit_url='/account', **FORM_OPTIONS)
if not result.is_valid:
return await result.render_with_errors_async()
name = result.data['name']
return html(t'<p>Saved {name}.</p>')
from flask import Flask, request
from models import AccountForm
from pydantic_schemaforms import html, render_form_html
app = Flask(__name__)
FORM_OPTIONS = {'layout': 'tabbed', 'include_framework_assets': True, 'asset_mode': 'cdn'}
@app.get('/account')
def show_account():
return render_form_html(AccountForm, submit_url='/account', **FORM_OPTIONS)
@app.post('/account')
def save_account():
result = AccountForm.validate(request.form, submit_url='/account', **FORM_OPTIONS)
if not result.is_valid:
return result.render_with_errors()
name = result.data['name']
return html(t'<p>Saved {name}.</p>')
The layouts¶
layout= |
What you get |
|---|---|
"vertical" (default) |
The form's own fields first, then each sub-form as a titled section |
"tabbed" |
One tab for the form's own fields, then one tab per sub-form |
"accordion" |
The same groups as collapsible panels, the first one open |
"side-by-side" |
The same groups as columns in one row |
"wizard" |
The same groups as steps, one at a time, with Back and Next |
Each sub-form, and each repeating sub-form, is one group. The grouped
layouts need at least one sub-form; on a flat form they raise a ValueError
telling you to split it into nested models.
After a failed submit, the first tab, panel or step that contains an error is the one that opens, and the error summary at the top stays visible.
Group titles and order¶
- A sub-form's group is titled by its field's
title("Contact details"). - The first group holds the form's own fields (here, the name). Its title is
the model's title,
model_config = {"title": "About you"}, or "General" when the model has none. - Groups follow the order the fields are declared in.
ui_orderchanges it without moving code:ui_order=1puts Preferences right after the first group.
Group fields without nesting¶
Sub-forms are right when the data is really nested. For "these four fields under a heading" or "first and last name on one row", keep the model flat:
from pydantic_schemaforms import Field, FormModel
class SignupForm(FormModel):
first_name: str = Field(title='First name', ui_cols=6)
last_name: str = Field(title='Last name', ui_cols=6)
street: str = Field(title='Street', ui_group='Address', ui_cols=8)
unit: str = Field('', title='Unit', ui_group='Address', ui_cols=4)
city: str = Field(title='City', ui_group='Address')
notes: str = Field(
'',
title='Notes',
ui_element='textarea',
ui_group='More',
ui_group_collapsible=True,
ui_group_collapsed=True,
)
result.data stays flat, with no address key: groups only affect how the form looks.
ui_groupgathers fields with the same title into one section, placed where its first field is declared. Underlayout="tabbed","accordion","side-by-side"or"wizard", each group becomes its own tab, panel, column or step, alongside any sub-forms.ui_colsis a width out of 12. Consecutive fields share a row until they pass 12, and a field with noui_colsstarts a new row. Below 768px wide, every field is full width. It works the same under all three styles.ui_group_collapsible=Truelets the user fold the group away, andui_group_collapsed=Truestarts it closed; a group with an error always opens. These apply inlayout="vertical", since a tab or panel already folds.ui_group_show_whenhides the whole group, heading included, using the same rules asui_show_when. Its fields need defaults. Under"tabbed","accordion"and"wizard", the tab, panel or step stays in place, empty.
The ui_group_* options go on any one field of the group. render_display() shows groups
as titled sections too, without the column widths. Inside a model_list item, ui_group
and ui_cols have no effect, since each item already arranges its own fields;
ui_group_show_when still hides each of the group's fields.
Wizard¶
layout="wizard" shows one group at a time with a progress header and Back
and Next buttons; the submit button is on the last step.
from models import AccountForm
from pydantic_schemaforms import render_form_html
form_html = render_form_html(
AccountForm,
layout='wizard',
wizard_options={'next_label': 'Continue', 'linear': True},
submit_url='/account',
)
- One form, one POST. All steps are in the same
<form>, so the routes are exactly the ones above withlayout="wizard". No state is kept between steps. - Next checks the current step with the browser's own validation
(
required,min,pattern, ...) and stays put if something is wrong. Pydantic still validates everything on submit. - Without JavaScript, every step is shown, stacked, with a normal submit button.
wizard_options sets the button labels, hides the progress header, or lets
the header jump to any step ("linear": False). ui_step_title on a
sub-form field gives its step a different title from its title. All the
options are in the Layouts reference.
Layouts for content that isn't a form field
The library also has layout building blocks, such as a grid, tabs and cards, for arranging HTML you've already rendered. They're for page composition, not for forms; see Layout Primitives.
Next: Styling.