Form UX¶
Features that apply across form views (and related list interactions): tabbed notebooks, sticky actions, keyboard save, success toasts, and opening related records in a floating dialog instead of leaving the page.
Introduced in v0.21.0 unless noted otherwise.
Tabbed notebooks¶
Long forms can mix flat sections (stacked cards) and notebooks
(Odoo-style tabs). Each tab is a page(...) with its own field grid.
Python builders¶
from pyvelm.builders import Field, FormView, Notebook, Page, ViewsData
views_data = (
ViewsData.make()
.views(
FormView.make("country.form")
.model("res.country")
.section("identity", "Identity", ["name", "code", "continent_id"])
.notebook(
"subdivisions",
Notebook.make("subdivisions")
.title("Subdivisions")
.page(
"states",
"States / provinces",
[Field.make("state_ids").edit_toggle().list_view("state.compact")],
cols=1,
)
.page(
"cities",
"Cities",
[Field.make("city_ids").edit_toggle().list_view("city.compact")],
),
),
)
)
Legacy form_view / section / notebook / page / field functions
still work — see Building UIs.
| Builder | Purpose |
|---|---|
FormView.make(...).section(...) |
Single card of fields |
Notebook.make(...).page(...) |
Tab container + tab pages |
Field.make("x").edit_toggle() |
Per-field widget / O2M options |
The first page is selected by default. The active tab is remembered per
notebook in localStorage (key pv-nb-<module>-<view>-<notebook_name>).
View XML¶
Equivalent arch (normalized to the same structure as builders):
<form>
<section name="identity" title="Identity">
<field name="name"/>
</section>
<notebook name="lines">
<page name="invoice" title="Invoice lines">
<field name="invoice_line_ids" widget="dialog" list_view="move.line.invoice"/>
</page>
<page name="entry" title="Journal items">
<field name="entry_line_ids" widget="inline" list_view="move.line.entry"/>
</page>
</notebook>
</form>
View inheritance¶
Target a field inside a notebook page with a path through pages:
See Extending views.
When to use notebooks vs edit_toggle¶
| Pattern | Use when |
|---|---|
| Notebook | Several different One2many fields (States vs Cities) or unrelated heavy widgets should not share one scroll |
edit_toggle |
One field can be edited as a dialog table or an inline grid without duplicating the field on two tabs |
You can combine both: notebook tabs that each host an edit_toggle field.
Live example: pyvelm/modules/geo_data/views/geo.py (country form with
States / Cities notebook tabs).
Sticky action bar¶
On form views, the mode badge (display / edit / new) and action buttons (Edit, Save, Cancel, Delete, header actions) stay pinned at the top while you scroll long field content.
This is pure layout CSS (.pv-form-actions-bar); no extra configuration.
Save with Ctrl+S / Cmd+S¶
In edit or new mode, Ctrl+S (Windows/Linux) or Cmd+S (macOS) clicks the same control as the green Save or Create button — including HTMX validation and the success toast.
The shortcut is not captured when focus is inside:
- HTML / code field editors (
.pv-html-editor,.pv-code-editor) - A textarea inside an inline One2many grid (so Enter/Ctrl+S behave as expected in multi-line cells)
Buttons carry data-pv-form-save and a tooltip (Save (Ctrl+S)).
Save confirmation toast¶
After a successful Save or Create on a full-page form, a green toast
appears (e.g. Saved Kenya. / Created …). The server sets
HX-Trigger: pv-toast on the POST response; the layout routes it to the
global toast stack (window.pvToast).
Saving inside PvDialog also shows a toast before the dialog closes.
Failed saves (422 validation or ORM errors) do not toast — errors stay on the form.
Opening related records (PvDialog)¶
Many2one values and One2many rows can open the comodel form in a floating, draggable dialog instead of navigating away. The page behind stays put.
Many2one — display (lists and forms)¶
When the comodel has a form view, the cell shows:
- Plain text — the display name (not a link).
- Open button — small external-link icon (Odoo-style) beside the label.
Clicking the button opens PvDialog with that record in display mode.
Use Open full page in the dialog title bar to navigate to the normal
/web/views/.../record/{id} route.
Many2one — edit (combobox)¶
The edit widget keeps the searchable combobox; the same open button sits
to the right of the input. It calls PvDialog.open() (no full-page navigation).
Create and edit… in the dropdown still opens the comodel new form in the dialog.
One2many — dialog table¶
With widget="dialog" (or Dialog on an edit_toggle field), each row
and Add open the child form in PvDialog. After a successful child save,
the parent table can refresh (data-pv-dialog-refresh on the trigger).
Declarative dialog triggers¶
Any element can open the dialog:
<a href="/web/views/partners/partner.form/record/42"
data-pv-dialog
data-pv-dialog-title="Partner"
data-pv-dialog-refresh>
Open
</a>
| Attribute | Purpose |
|---|---|
data-pv-dialog |
Mark as dialog trigger (or use data-pv-dialog-url) |
data-pv-dialog-url |
URL to load (defaults to href) |
data-pv-dialog-title |
Dialog title bar |
data-pv-dialog-refresh |
After save, re-fetch the parent form shell on the page |
Imperative API (from Alpine or scripts):
window.PvDialog.open({
url: '/web/views/geo_data/geo_data.country.form/record/115',
title: 'Kenya',
});
Dialog requests send X-PV-Dialog: 1 so the server returns a body-only
form fragment (no sidebar) and skips out-of-band pager swaps on the parent
page. Successful saves return 204 + pv-dialog-saved instead of swapping
the dialog body to display mode.
Lists — row click vs open button¶
List rows are still clickable to open the record form on the full page.
Clicks on buttons, links, the M2O open button, checkboxes, or
drag handles do not trigger row navigation (pvListRowNavigate).
Autosave on navigation¶
Unchanged since earlier releases, but pairs with the above:
- In edit / new, if the form is dirty and you click a normal in-app link (sidebar, breadcrumb), the framework POSTs the form first, then follows the link on success.
- Save, Cancel, and Ctrl+S use their own flows (not double-saving).
- Back / tab close uses the browser Leave site? prompt when dirty.
See Building UIs → Autosave on navigation.
Related guides¶
| Topic | Doc |
|---|---|
One2many edit_toggle, inline grid keys, list_view |
One2many on parent forms |
| List / form / kanban views, widgets | Building UIs |
| Notebook field paths in inheritance | Extending views |
| Geo Data country notebook demo | Geo data |