Skip to content

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:

<victor>
  <!-- <view>, <action>, <menu> and seed records go here -->
</victor>

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:

<field name="email" widget="email"/>
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:

  1. The widget name you gave, if it is a registered widget.
  2. Otherwise, the default widget for the field's data type.
  3. 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:

<filter name="by_company" string="Company" context="{'group_by': 'is_company'}"/>

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:

<view id="view_graph" model="workflow" type="graph"/>

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