Form Views
Form views display and edit single records.
Basic Form
Section titled “Basic Form”{ "data_type": "UiView", "identifier": "contact_form_view", "type": "Form", "model": "Contact", "arch": [ { "type": "row", "content": [ { "type": "column", "span": 6, "content": [ { "type": "field", "name": "name", "properties": { "widget": "TextInput", "size": "lg" } } ] } ] } ]}Layout Structure
Section titled “Layout Structure”12-Column Grid
Section titled “12-Column Grid”Forms use a 12-column grid system:
{ "type": "row", "content": [ {"type": "column", "span": 6, "content": [...]}, {"type": "column", "span": 6, "content": [...]} ]}Nested Rows
Section titled “Nested Rows”{ "type": "column", "span": 6, "content": [ { "type": "row", "content": [ {"type": "column", "span": 6, "content": [...]}, {"type": "column", "span": 6, "content": [...]} ] } ]}Row Gutter (Column Spacing)
Section titled “Row Gutter (Column Spacing)”Rows use a responsive gutter (spacing between columns) that defaults to { base: 16, md: 100 }. You can override this per-row using the properties.gutter property:
{ "type": "row", "properties": { "gutter": 16 }, "content": [ {"type": "column", "span": 4, "content": [...]}, {"type": "column", "span": 4, "content": [...]}, {"type": "column", "span": 4, "content": [...]} ]}| Gutter Value | Use Case |
|---|---|
16 | Tight spacing for related fields (e.g., City/State/Zip) |
{ base: 16, md: 100 } | Default - responsive spacing |
| Custom number | Fixed pixel spacing |
Status Bar
Section titled “Status Bar”Display status at the top:
{ "type": "statusbar", "name": "status", "properties": { "widget": "Badge", "variant": "dot", "colors": { "draft": "gray", "confirmed": "blue", "done": "green", "cancelled": "red" } }}Action Buttons
Section titled “Action Buttons”Trigger model methods:
{ "type": "actionButton", "properties": { "label": "Confirm", "icon": "Check", "variant": "primary", "method": "confirm_order", "visible": "Q(status__eq='draft')" }}Action Button Properties
Section titled “Action Button Properties”| Property | Description |
|---|---|
label | Button text |
icon | Lucide icon name |
variant | primary, outline, subtle |
method | Model method to call |
context | Custom context passed to method |
auto_refetch | Reload after action |
instance | Pass instance to method |
Button Context
Section titled “Button Context”Action buttons automatically pass context to backend methods. Access via self._context:
{ "type": "actionButton", "properties": { "label": "Send Email", "method": "action_send_email", "context": { "template": "welcome_email", "send_copy": true } }}class Contact(Model): async def action_send_email(self): # Auto-provided context active_model = self._ctx.get('active_model') # 'Contact' active_ids = self._ctx.get('active_ids') # Selected record IDs
# Custom context from button template = self._ctx.get('template') # 'welcome_email' send_copy = self._ctx.get('send_copy') # True
for record_id in active_ids: record = await Contact.get(record_id) await record.send_email(template=template) return {'type': 'close', 'reload': True}Auto-provided context keys:
| Key | Description |
|---|---|
active_model | Model name |
active_ids | List of selected record IDs |
Link Buttons (Statistics)
Section titled “Link Buttons (Statistics)”Show related record counts:
{ "type": "linkButton", "name": "order_count", "properties": { "label": "Orders", "icon": "ShoppingCart", "action": "orders_action", "filter": "customer" }}Organize content in tabs:
{ "type": "tab", "title": "Contact Info", "properties": { "icon": "User" }, "content": [ {"type": "field", "name": "email", "properties": {"widget": "TextInput"}}, {"type": "field", "name": "phone", "properties": {"widget": "TextInput"}} ]}Accordions
Section titled “Accordions”Collapsible sections:
{ "type": "accordion", "title": "Additional Details", "defaultOpen": false, "properties": { "icon": "Info" }, "content": [ {"type": "field", "name": "notes", "properties": {"widget": "TextArea"}} ]}Accordion and Tab Icons
Section titled “Accordion and Tab Icons”Both accordions and tabs support an optional properties.icon property using Lucide icon names. If no icon is specified, LayoutList is used as the default.
| Property | Description |
|---|---|
properties.icon | Lucide icon name (e.g., User, ShoppingCart, Settings) |
defaultOpen | (Accordion only) Whether to start expanded (default: true) |
Page Tabs (Full-Width Header Tabs)
Section titled “Page Tabs (Full-Width Header Tabs)”Page tabs provide a full-width tab navigation bar rendered in the header area (beneath the secondary header). This is ideal for settings pages or forms with multiple sections that need prominent navigation. Tabs automatically collapse into a “More” dropdown when they overflow the available width.
{ "type": "pageTabs", "content": [ { "type": "tab", "name": "general", "title": "General", "properties": { "icon": "DatabaseZap" }, "content": [ { "type": "paper", "title": "System Identity", "content": [ { "type": "row", "content": [ { "type": "column", "span": 6, "content": [ { "type": "field", "name": "system_name" } ] } ] } ] } ] }, { "type": "tab", "name": "security", "title": "Security", "properties": { "icon": "Shield" }, "content": [...] } ]}Key Features
Section titled “Key Features”- Header rendering: Tabs appear in the header area (full-width, not inside form Paper)
- Pill-shaped styling: Active tab has a light background with pill-shaped borders
- Overflow handling: Tabs that don’t fit collapse into a “More” dropdown automatically
- Icon support: Each tab can have an icon via
properties.icon(Lucide icon names) - Paper cards: Each tab’s content is rendered as separate Paper cards
Tab Properties
Section titled “Tab Properties”| Property | Description |
|---|---|
name | Unique identifier for the tab |
title | Display text for the tab |
properties.icon | Lucide icon name (e.g., Settings, Shield, User) |
content | Array of layout elements (paper, row, field, etc.) |
Extending pageTabs via View Inheritance
Section titled “Extending pageTabs via View Inheritance”Other modules can add tabs to an existing pageTabs using view inheritance:
{ "data_type": "UiView", "name": "configuration_form_view_mymodule", "identifier": "configuration_form_view_mymodule", "inherited_view": "configuration_form_view", "type": "Form", "model": "Configuration", "arch": { "operations": [ { "action": "add", "target": { "type": "pageTabs" }, "position": "inside", "value": { "type": "tab", "name": "mymodule", "title": "My Module", "properties": { "icon": "Settings" }, "content": [ { "type": "paper", "title": "My Settings", "content": [ { "type": "field", "name": "my_setting" } ] } ] } } ] }}When to Use pageTabs vs Regular Tabs
Section titled “When to Use pageTabs vs Regular Tabs”| Use Case | Recommended |
|---|---|
| Settings/Configuration forms | pageTabs - full-width header navigation |
| Multi-section forms with many tabs | pageTabs - handles overflow gracefully |
| Related data within a form | Regular tabs - inline with form content |
| Quick toggles between few sections | Regular tabs - simpler, inline |
Fields
Section titled “Fields”Basic Field
Section titled “Basic Field”{ "type": "field", "name": "email", "properties": { "widget": "TextInput", "label": "Email Address", "placeholder": "Enter email", "required": true }}Common Field Properties
Section titled “Common Field Properties”| Property | Description |
|---|---|
widget | Widget type (TextInput, DataCombo, etc.) |
label | Field label (overrides model description) |
placeholder | Input placeholder |
nolabel | Hide the label |
size | Size: xs, sm, md, lg, xl |
visible | Boolean or Q-expression for conditional visibility |
readonly | Boolean or Q-expression for read-only |
required | Boolean or Q-expression for required |
groups | List of group identifiers for access control |
filter | Q-expression to filter related records (DataCombo/MultiCombo) |
context | Default values for new records (see Field-Level Context) |
Embedded Lists
Section titled “Embedded Lists”Display related records:
{ "type": "field", "name": "order_lines", "properties": { "widget": "List", "create": true, "delete": true, "view": "order_line_list_view", "context": { "form_identifier": "order_line_form_view", "default_qty": 1 } }}| Property | Description |
|---|---|
view | List view identifier |
create | Allow creating records |
delete | Allow deleting records |
editable | true for inline editing, "modal" for modal editing |
insertPosition | Where new items are added: "bottom" (default) or "top" |
context | Default values and view references |
The context object supports:
default_<field>: Pre-populate field when adding new rowsform_identifier: Form view for modal editing
See Field-Level Context for more details.
Element Properties
Section titled “Element Properties”All layout elements (row, column, accordion, tabs, tab, fieldset, paper, group), fields, and action buttons support behavioral properties inside the properties object:
| Property | Type | Description |
|---|---|---|
visible | Boolean/String | If false, element is hidden. Can be a Q-expression for conditional visibility. Default: true |
readonly | Boolean/String | If true, field(s) are read-only. Can be a Q-expression. Propagates to children. |
required | Boolean/String | If true, field is required. Can be a Q-expression for conditional requirement. |
groups | Array | List of group identifiers. Element is visible only if user belongs to at least one group. |
Note: All field configuration goes in properties. Structural attributes (type, name, span, content, title) stay at the top level.
Conditional Visibility
Section titled “Conditional Visibility”Show/hide elements based on conditions:
{ "type": "field", "name": "company_name", "properties": { "widget": "TextInput", "visible": "Q(type__eq='company')" }}Static Visibility
Section titled “Static Visibility”Hide an element unconditionally:
{ "type": "field", "name": "internal_id", "properties": { "widget": "TextInput", "visible": false }}Conditional Readonly
Section titled “Conditional Readonly”Make a field read-only based on conditions:
{ "type": "field", "name": "price", "properties": { "widget": "NumberInput", "readonly": "Q(status__neq='draft')" }}Conditional Required
Section titled “Conditional Required”Make a field required based on conditions. The Q-expression is evaluated dynamically whenever form values change, so validation adapts in real-time:
{ "type": "field", "name": "shipping_address", "properties": { "widget": "DataCombo", "required": "Q(requires_shipping__eq=true)" }}When requires_shipping becomes true, the field shows the required asterisk and form submission validates it. When requires_shipping is false, the field is optional.
Q Expression Syntax
Section titled “Q Expression Syntax”Q expressions use the format Q(field__operator=value):
"Q(status__eq='draft')""Q(is_customer=True)""Q(amount__gt=1000)""Q(stage__is_done__eq=false)""Q(type__eq='company') & Q(active__eq=True)""Q(status__in=['draft', 'sent'])""(Q(a__eq=1) | Q(b__eq=2)) & Q(c__eq=3)"Operators:
| Operator | Description | Example |
|---|---|---|
eq | Equals (default if omitted) | Q(state__eq='Draft') or Q(state='Draft') |
neq | Not equals | Q(code__neq='en') |
gt | Greater than | Q(amount__gt=100) |
gte | Greater than or equal | Q(amount__gte=100) |
lt | Less than | Q(amount__lt=100) |
lte | Less than or equal | Q(amount__lte=100) |
contains | String contains (case-sensitive) | Q(name__contains='test') |
icontains | String contains (case-insensitive) | Q(name__icontains='test') |
ncontains | String does not contain | Q(name__ncontains='draft') |
nicontains | String does not contain (case-insensitive) | Q(name__nicontains='draft') |
isnull | Is null or empty array | Q(user__isnull=True) |
isnotnull | Is not null and not empty | Q(user__isnotnull=True) |
in | Value in list | Q(state__in=['Draft','Pending']) |
nin | Value not in list | Q(state__nin=['Done','Cancelled']) |
startswith | String starts with | Q(name__startswith='INV') |
endswith | String ends with | Q(name__endswith='.pdf') |
Value types:
- Booleans:
True,False,true,false - Numbers:
100,45.67 - Strings:
'value'or"value" - Arrays (for in/nin):
['Draft', 'Pending']
Shorthand equality: Q(is_customer=True) is equivalent to Q(is_customer__eq=True)
Nested field access (deep nesting supported):
- Access related fields:
Q(stage__is_done__eq=true)accessesrecord.stage.is_done - Deep nesting is fully supported:
Q(contact__company__currency__code__eq='USD') - The system automatically extracts and prefetches all intermediate relation paths
- Example:
Q(order__partner__country__code__in=['US','CA'])auto-prefetchesorder,order__partner,order__partner__country
Logical operators:
- AND:
Q(a__eq=1) & Q(b__eq=2) - OR:
Q(a__eq=1) | Q(b__eq=2) - Precedence:
&binds tighter than| - Grouping:
(Q(a__eq=1) | Q(b__eq=2)) & Q(c__eq=3)
Reactive Q Expressions
Section titled “Reactive Q Expressions”Q expressions in visible, readonly, and required properties are re-evaluated whenever form data changes. For conditions that need to update reactively when a field changes, use flat field names that exist directly on the form.
Recommended (reactive):
"visible": "Q(track_inventory__eq=true)"Not recommended for reactive use:
"visible": "Q(product__track_inventory__eq=true)"Why? Nested traversal (e.g., product__track_inventory) accesses cached nested objects that are snapshots from when the record was loaded. When you edit a related field like track_inventory, the flat field updates but the nested product object doesn’t refresh. This means:
- Initial load: Traversal works correctly
- After editing: Nested object has stale data, condition may not update
Best practices:
| Scenario | Approach |
|---|---|
| Check a field on the current form | Use flat field name: Q(status__eq='draft') |
Check a related field (has related_field attribute) | Use the flat field name: Q(track_inventory__eq=true) |
| Check a truly nested value (display only) | Traversal works for initial state, won’t be reactive |
| Need reactive behavior for nested computed field | Create a related field on your model to expose it |
Group-Based Visibility
Section titled “Group-Based Visibility”Restrict to specific groups:
{ "type": "field", "name": "internal_notes", "properties": { "widget": "TextArea", "groups": ["sales_manager", "admin"] }}Complete Example
Section titled “Complete Example”{ "data_type": "UiView", "identifier": "order_form_view", "type": "Form", "model": "Order", "arch": [ { "type": "statusbar", "name": "status", "properties": { "widget": "Badge", "variant": "dot", "colors": { "draft": "gray", "confirmed": "blue", "shipped": "orange", "delivered": "green", "cancelled": "red" } } }, { "type": "actionButton", "properties": { "label": "Confirm", "icon": "Check", "variant": "primary", "method": "confirm_order", "visible": "Q(status__eq='draft')" } }, { "type": "actionButton", "properties": { "label": "Ship", "icon": "Truck", "variant": "primary", "method": "ship_order", "visible": "Q(status__eq='confirmed')" } }, { "type": "actionButton", "properties": { "label": "Cancel", "icon": "X", "variant": "outline", "method": "cancel_order", "visible": "Q(status__in=['draft', 'confirmed'])" } }, { "type": "row", "content": [ { "type": "column", "span": 8, "content": [ { "type": "field", "name": "name", "properties": {"widget": "TextInput", "size": "lg", "nolabel": true} } ] }, { "type": "column", "span": 4, "content": [ { "type": "field", "name": "order_date", "properties": {"widget": "DatePickerInput"} } ] } ] }, { "type": "row", "content": [ { "type": "column", "span": 6, "content": [ {"type": "field", "name": "customer", "properties": {"widget": "DataCombo"}}, {"type": "field", "name": "shipping_address", "properties": {"widget": "DataCombo"}} ] }, { "type": "column", "span": 6, "content": [ {"type": "field", "name": "payment_terms", "properties": {"widget": "DataCombo"}}, {"type": "field", "name": "salesperson", "properties": {"widget": "DataCombo"}} ] } ] }, { "type": "tab", "title": "Order Lines", "content": [ { "type": "field", "name": "lines", "properties": { "widget": "List", "create": true, "delete": true, "view": "order_line_list_view" } } ] }, { "type": "tab", "title": "Notes", "content": [ {"type": "field", "name": "notes", "properties": {"widget": "RichTextEditor"}} ] }, { "type": "row", "content": [ {"type": "column", "span": 6, "content": []}, { "type": "column", "span": 6, "content": [ {"type": "field", "name": "subtotal", "properties": {"widget": "NumberInput", "readonly": true}}, {"type": "field", "name": "tax_amount", "properties": {"widget": "NumberInput", "readonly": true}}, {"type": "field", "name": "total", "properties": {"widget": "NumberInput", "size": "lg", "readonly": true}} ] } ] } ]}Next Steps
Section titled “Next Steps”- List Views - Table views
- Widgets - Available widgets
- View Inheritance - Extending views