Views¶
A view describes how one model is rendered on screen — as a table, a record editor, a board, a card grid, and so on. You never build these screens as bespoke UI: you declare them in view XML, and Victor's generic framework renders them for you. Point a menu or action at a view, and the framework does the rest.
This page covers the view XML DSL: the file structure, the six view types, the <field> element and its widgets, form layout tags, search filters, record-action buttons, and view inheritance. It assumes you already have a model to render — see Models & fields for defining one.
The view file¶
Every view file has a single root element, <victor>, which wraps all of the view, action, menu, and seed declarations for a module:
View files are activated by listing them under the data: key of your module's module.yaml:
name: contact
label: Contacts
version: 0.1.0
requires: [base]
models: [models]
data: [views/contact.xml, data/contact.xml]
Note
Files under data: load in order. If a view references another view (for example, an action that opens a form), make sure the referenced view is declared first.
Declaring a view¶
Each <view> element declares one view for one model:
<view id="view_list" model="contact" type="list">
<field name="display_name"/>
<field name="email"/>
</view>
| Attribute | Purpose |
|---|---|
id |
Unique reference within the module. Actions and menus point at a view by this id (e.g. contact.view_form). |
model |
The target model name the view renders. |
type |
One of list, form, kanban, search, card, graph. |
A model may have one view per type — a list, a form, a kanban, a search, and so on. The framework picks the right one for the context (a collection view uses list/kanban/card; opening a record uses form; the search bar uses search).
View types at a glance¶
| Type | What it renders |
|---|---|
list |
A sortable, paginated table. <field> children are columns. |
form |
A single-record editor with layout tags and editable field rows. |
kanban |
A column board grouped by a field; records are draggable cards. |
search |
The filter chips and Group By options for a model's collection views. |
card |
A responsive card grid (as used by the Module Manager). |
graph |
A specialized node-graph canvas for record graphs such as a workflow's steps. |
Fields in views¶
<field> is the core element. It places a model field into a view — a column in a list, kanban, or card, and an editable row in a form:
| Attribute | Applies to | Meaning |
|---|---|---|
name |
all | Required. The model field name to render. |
widget |
all | Optional widget-name override (see Widgets). |
readonly="1" |
form | Render the field read-only. |
required="0" |
form | Suppress the required * marker. |
invisible |
form | "1" to always hide, or an expression evaluated against the current form values. |
Widget resolution¶
When you don't specify a widget, the framework resolves one in this order:
- The
widgetname you gave, if it is a registered widget. - Otherwise, the default widget for the field's data type.
- Otherwise, a plain string.
Tip
Prefer letting the field's data type pick the widget. Only add an explicit widget when you want a different presentation than the type's default (for example, a String field rendered as an email link, or a Boolean rendered as a toggle).
Conditional visibility¶
In a form, invisible can be a simple flag or an expression compared against the current record's values:
<view id="view_form" model="contact" type="form">
<group string="Details">
<field name="is_company"/>
<field name="company_name" invisible="is_company != 'true'"/>
<field name="birthday" invisible="is_company == 'true'"/>
</group>
</view>
Supported expression forms are field == 'value' and field != 'value'.
Widgets¶
Data-type defaults¶
If a <field> has no widget, its data type selects the widget:
| Field type | Default rendering |
|---|---|
String |
Single-line text |
Text |
Multiline text |
Integer |
Number input |
Float |
Number input |
Boolean |
Checkbox (check cell in collections) |
Date |
Localized date / date picker |
Datetime |
Date and time |
Reference |
Many-to-one autocomplete with quick-create |
Selection |
Dropdown, shown as a labelled badge |
CheckboxSet |
Stacked multi-checkbox group |
Many2many |
Tag chips with autocomplete |
One2many |
Inline editable child-row grid (full form width) |
Name overrides¶
Set widget="…" to override the default. The registered widget names are:
| Widget | Renders |
|---|---|
email |
mailto: link (read) / email input (edit) |
url |
Clickable link (read) / URL input (edit) |
phone |
tel: link (read) / tel input (edit) |
tags |
Many-to-many as removable tag chips with type-ahead and create-on-the-spot |
radio |
A Selection as a vertical radio group instead of a dropdown |
checkbox-inline |
A CheckboxSet as a wrapping inline row of checkboxes |
textarea |
Forces a multiline text input |
toggle |
A boolean as an on/off switch |
badge |
The value as a pill/badge |
monetary |
A number formatted with grouping and 2 decimals |
date |
Localized date (read) / date picker (edit) |
Warning
Only the widget names above are registered. If you set widget to an unregistered name, the field silently falls back to its data-type default. Stick to this list.
Form layout¶
Forms arrange field rows in a two-column label/value grid. Use these layout tags to structure them:
| Tag | Purpose |
|---|---|
<group string="…"> |
Groups fields into the two-column grid; the optional string is a section heading. |
<notebook> |
A tabbed container that holds <page> children. |
<page string="…"> |
One tab inside a <notebook>; string is the tab label. |
<sheet> |
An optional container that lays its <field> children out in the standard grid. |
Plain field content without a layout tag lays out the same way as inside a <group>.
<view id="view_form" model="contact" type="form">
<group string="Details">
<field name="display_name"/>
<field name="email" widget="email"/>
<field name="is_company"/>
<field name="stage"/>
<field name="parent_id"/>
<field name="tag_ids" widget="tags"/>
</group>
<notebook>
<page string="Notes">
<field name="email"/>
</page>
</notebook>
</view>
<button> children in a form render in the form toolbar as record actions — see View actions.
List views¶
A list is a sortable, paginated table. Its <field> children are the columns, rendered left-to-right. Clicking a column header toggles the sort between ascending, descending, and none. Add <button> children for per-row record actions.
<view id="view_list" model="contact" type="list">
<field name="display_name"/>
<field name="email"/>
<field name="is_company"/>
<field name="stage"/>
<field name="tag_ids" widget="tags"/>
</view>
Kanban views¶
A kanban renders records as draggable cards, grouped into columns. Put the card's fields inside the <kanban> body — the first <field> becomes the bold card title, the rest render as card lines.
<view id="view_kanban" model="contact" type="kanban">
<kanban default_group_by="is_company" records_draggable="1">
<field name="display_name"/>
<field name="email"/>
</kanban>
</view>
<kanban> attribute |
Meaning |
|---|---|
default_group_by |
The field the board columns are grouped by. |
records_draggable |
"0" disables dragging cards between columns; drag is enabled by default. |
Card views¶
A card view is a responsive card grid. It takes the same <field> and <button> children as a list: the first <field> is the card title, and the rest render as label/value rows.
<view id="view_module_card" model="module" type="card">
<field name="shortdesc"/> <!-- first field = card title -->
<field name="latest_version"/>
<field name="author"/>
<field name="state"/>
<button action="install" string="Install" invisible="state == 'installed'"/>
<button action="uninstall" string="Uninstall" variant="destructive" invisible="can_uninstall != 'true'"/>
</view>
Search views and filters¶
A search view defines the filter chips and Group By options for a model's collection views. Its <search> body holds <filter> children.
<view id="view_search" model="contact" type="search">
<search>
<filter name="companies" string="Companies" domain='[["is_company", "=", true]]' group="kind"/>
<filter name="people" string="People" domain='[["is_company", "=", false]]' group="kind"/>
<filter name="by_company" string="Company" context="{'group_by': 'is_company'}"/>
</search>
</view>
Domain filters¶
A <filter> with a domain is a toggleable chip. When active, its domain is ANDed into the record query.
<filter> attribute |
Meaning |
|---|---|
name |
Unique key for the filter. |
string |
Chip label; defaults to name. |
domain |
A JSON list of [field, op, value] triples. |
group |
Optional. Combines related filters. |
A domain is a list of triples, each [field, operator, value]. In the example above, [["is_company", "=", true]] keeps only records where is_company is true.
Filters that share the same group are combined together. Several same-field equality filters in one group become an OR (an in match) rather than a contradictory AND — so companies and people above behave as a single toggle set.
Group By options¶
A <filter> that carries a group_by context becomes a Group By menu option instead of a domain chip:
Buttons¶
<button> surfaces a registered server action — as a column in list/card views, or as a toolbar button in a form. Its attributes include action, string, variant, confirm, preview, invisible, show_on_new, and run_on_save. See View actions for the full semantics.
<button action="uninstall" string="Uninstall" variant="destructive" invisible="can_uninstall != 'true'"/>
Graph views¶
A graph view is a specialized node-graph canvas for record graphs — for example, the steps of a workflow. You declare it with type="graph"; the graph is derived from the record's own data rather than from <field> columns, so it has no column children:
Graph views are used by workflow-style features; their authoring surface is documented alongside those features.
Extending views¶
To change a view defined by another module (or to keep your customizations separate from a base view), use <extend> instead of editing the original file. Target a base view by its ref, then apply ops that locate a node and modify it.
<victor>
<extend id="contact_form_lead" view="contact.view_form">
<after field="email"><field name="lead_count"/></after>
</extend>
</victor>
<extend> attribute |
Meaning |
|---|---|
id |
Unique reference for this extension. |
view |
The base view ref to modify (e.g. contact.view_form). |
Inheritance ops¶
Inside <extend>, each op locates a target node and changes the arch:
| Op | Effect |
|---|---|
<after> |
Insert its children as siblings after the target. |
<before> |
Insert its children as siblings before the target. |
<inside> |
Append its children into the target. |
<replace> |
Swap the target for the op's children. |
<remove> |
Delete the target node. |
<set> |
Set attributes on the target node. |
Each op picks its target with one of these selectors:
| Selector | Locates by |
|---|---|
field="…" |
A <field> with that name. |
id="…" |
A node with that id. |
select="…" |
A path of tag[@attr='value'] steps. |
<victor>
<extend id="contact_form_tweaks" view="contact.view_form">
<!-- add a field after email -->
<after field="email"><field name="lead_count"/></after>
<!-- make the stage field read-only -->
<set field="stage" readonly="1"/>
<!-- drop a field entirely -->
<remove field="parent_id"/>
</extend>
</victor>
Tip
Extending is the clean way to adapt built-in apps. To add a lead count to the built-in contact form, ship an <extend> from your own module rather than forking the contact app's view file.
See also¶
- Models & fields — define the model a view renders.
- View actions — the
<button>element and server actions.