- The widget contract:
props.value,props.update(),props.readonly - Building a read-only widget (e.g., color swatch)
- Building an editable widget (e.g., star rating)
- Registering the widget and using it in a view XML
- Supporting list view columns
The Widget Contract
A field widget receives standardized props from the view framework. The key props are:
| Prop | Type | Description |
|---|---|---|
| value | any | The current field value (Python type mapped to JS) |
| update(newValue) | Function | Call this to update the field value |
| readonly | Boolean | True when the field is not editable |
| record | Object | The full record object — access other field values |
| name | String | The field name |
| type | String | The field type (char, integer, selection, etc.) |
Building a Read-Only Widget
A color swatch widget that renders a hex color value as a colored circle:
/** @odoo-module **/
import { Component } from '@odoo/owl';
import { registry } from '@web/core/registry';
import { standardFieldProps } from '@web/views/fields/standard_field_props';
export class ColorSwatchField extends Component {
static template = 'my_module.ColorSwatchField';
static props = {
...standardFieldProps, // includes value, update, readonly, etc.
};
get hexColor() {
return this.props.value || '#ffffff';
}
}
registry.category('fields').add('color_swatch', {
component: ColorSwatchField,
displayName: 'Color Swatch',
supportedTypes: ['char'],
});
<templates>
<t t-name="my_module.ColorSwatchField">
<div class="o_field_color_swatch">
<span t-attf-style="background-color: #{hexColor}; display: inline-block;
width: 20px; height: 20px; border-radius: 50%;
border: 1px solid #ccc;"/>
<span class="o_color_value" t-esc="hexColor"/>
</div>
</t>
</templates>
Building an Editable Widget
A star rating widget for an integer field (0-5 stars). Calls props.update() on click:
/** @odoo-module **/
import { Component } from '@odoo/owl';
import { registry } from '@web/core/registry';
import { standardFieldProps } from '@web/views/fields/standard_field_props';
export class StarRatingField extends Component {
static template = 'my_module.StarRatingField';
static props = {
...standardFieldProps,
maxStars: { type: Number, optional: true },
};
static defaultProps = { maxStars: 5 };
get stars() {
const max = this.props.maxStars;
const val = this.props.value || 0;
return Array.from({ length: max }, (_, i) => ({
filled: i < val,
index: i + 1,
}));
}
onClick(star) {
if (!this.props.readonly) {
this.props.update(star.index);
}
}
}
registry.category('fields').add('star_rating', {
component: StarRatingField,
displayName: 'Star Rating',
supportedTypes: ['integer'],
extractProps: ({ options }) => ({
maxStars: options.max_stars || 5,
}),
});
<t t-name="my_module.StarRatingField">
<div class="o_field_star_rating">
<t t-foreach="stars" t-as="star">
<span t-attf-class="fa #{star.filled ? 'fa-star' : 'fa-star-o'}
#{props.readonly ? '' : 'o_clickable'}"
t-on-click="() => onClick(star)"/>
</t>
</div>
</t>
Using the Widget in XML Views
Reference the widget by its registry key in your view XML:
<!-- Form view -->
<field name="rating" widget="star_rating" options="{'max_stars': 5}"/>
<field name="color" widget="color_swatch"/>
<!-- List view column -->
<field name="rating" widget="star_rating" optional="show"/>
The options attribute passes a JSON object to extractProps() in the registry definition. Use it to configure widget behavior per field usage.
Supporting List View Columns
For list view support, the widget must handle readonly=true gracefully (list view is always readonly). The same component works in both form and list views — just ensure the template renders a compact representation when props.readonly is true.
- Widgets receive
props.value(current value) and callprops.update(newVal)to save changes - Always check
props.readonlybefore enabling edit interactions - Register widgets in
registry.category('fields')withsupportedTypes - Use
extractPropsin the registry entry to map XMLoptionsto component props
Frequently Asked Questions
How do I make a widget that spans multiple fields?
Use props.record to access other field values: this.props.record.data.other_field. Your widget is still bound to one field (for the props.update() contract), but it can read any field from the record. To update multiple fields at once, use props.record.update({ field1: val1, field2: val2 }).
Can I use an existing widget as a base and extend it?
Yes — import the existing widget class and extend it: class MyCharField extends CharField. Override setup() or get displayValue() to modify behavior. Re-register under a new key in the registry. Don't re-register under the existing key unless you want to replace the widget for ALL usages of that type.
How do I handle async data loading in a widget?
Use useState() for loading state and onMounted() to fetch data after the widget mounts. For data that depends on props.value, also use onWillUpdateProps() to refetch when the value changes. Always show a loading indicator while async data is being fetched to avoid blank renders.