Part 6. Extending Odoo itself · lesson 27 of 40
Writing a custom field widget
After this lesson you can: Write, register and use a custom field widget, and extend a standard one.
Written against Odoo 19.0 and 18.0. Last checked 2026-10-09.
A field widget controls how one field is displayed and edited. It is the most common piece of custom OWL in real modules, and the most contained: it affects only the fields that ask for it.
<field name="progress" widget="my_progress"/>
A complete one
// static/src/fields/progress_field.js
import { Component } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { standardFieldProps } from "@web/views/fields/standard_field_props";
export class ProgressField extends Component {
static template = "owl_course.ProgressField";
static props = { ...standardFieldProps };
get value() {
return this.props.record.data[this.props.name] || 0;
}
get barClass() {
if (this.value >= 100) return "bg-success";
if (this.value >= 50) return "bg-info";
return "bg-warning";
}
async setValue(next) {
const clamped = Math.min(100, Math.max(0, next));
await this.props.record.update({ [this.props.name]: clamped });
}
}
export const progressField = {
component: ProgressField,
displayName: "Progress Bar",
supportedTypes: ["float", "integer"],
extractProps: ({ options }) => ({ showValue: options.show_value ?? true }),
};
registry.category("fields").add("my_progress", progressField);
<t t-name="owl_course.ProgressField">
<div class="d-flex align-items-center gap-2" style="min-width: 120px;">
<div class="progress flex-grow-1" style="height: 14px;">
<div class="progress-bar" t-att-class="barClass"
t-attf-style="width: {{ value }}%"/>
</div>
<span t-if="!props.readonly" class="small text-muted">
<input type="number" class="form-control form-control-sm"
style="width: 70px;"
t-att-value="value"
t-on-change="(ev) => this.setValue(parseFloat(ev.target.value))"/>
</span>
<span t-else="" class="small text-muted"><t t-esc="value"/>%</span>
</div>
</t>
standardFieldProps
Every field widget receives the same set of props, and spreading
standardFieldProps declares them all:
| Prop | What it is |
|---|---|
record | The record. record.data[name] is your value |
name | The field name |
readonly | Whether the field is currently read only |
id | DOM id for label association |
Do not invent your own version of these. Views pass them, and a widget that declares different props will fail validation.
Reading and writing
Read through this.props.record.data[this.props.name]. Not from a copy
taken in setup, which will not follow changes.
Write through record.update(), which is async and returns a promise:
await this.props.record.update({ [this.props.name]: value });
update is what triggers onchanges, recomputes and dirty tracking. Never
assign to record.data directly. It changes the object, the view does not
notice, and the value is not saved.
In Odoo 17 and later, pass an object keyed by field name as shown. Older examples pass the bare value.
Registering
export const progressField = {
component: ProgressField,
displayName: "Progress Bar", // shown in Studio and the field list
supportedTypes: ["float", "integer"],
supportedOptions: [{
label: "Show value", name: "show_value", type: "boolean",
}],
extractProps: ({ attrs, options, string }) => ({
showValue: options.show_value ?? true,
}),
fieldDependencies: [{ name: "state", type: "selection" }],
isEmpty: () => false,
};
supportedTypes is checked, so a widget declared for float used on a char
gives a clear error rather than a strange render.
extractProps turns view attributes into props. Options come from the view:
<field name="progress" widget="my_progress" options="{'show_value': false}"/>
fieldDependencies asks the view to fetch extra fields your widget needs but
which are not on the view. Without it, record.data.state is simply undefined.
isEmpty controls whether the field is treated as empty, which affects
required styling and the โno contentโ display.
Extending a standard widget
Often cheaper and more upgrade proof than starting from nothing.
import { CharField, charField } from "@web/views/fields/char/char_field";
import { registry } from "@web/core/registry";
export class UpperCharField extends CharField {
async onInput(ev) {
ev.target.value = ev.target.value.toUpperCase();
return super.onInput(ev);
}
}
registry.category("fields").add("upper_char", {
...charField,
component: UpperCharField,
});
Spreading charField keeps its supportedTypes, extractProps and the rest,
so you inherit everything and change one method.
Standard widgets worth knowing as bases: char, text, integer, float,
monetary, date, datetime, selection, many2one, many2many_tags,
one2many, boolean, badge, image, binary, html.
Using it
<field name="progress" widget="my_progress"/>
<field name="progress" widget="my_progress" options="{'show_value': true}"/>
Works in form, list and kanban views. In a list your widget renders in a table cell, so keep it compact and give it a sensible minimum width, or the column collapses.
Readonly matters
Honour props.readonly. A widget that renders an editable input in a readonly
context lets a user type into something that will never be saved, which is worse
than showing nothing.
The same applies in a list view where only one row is in edit mode: your widget renders for every row, and all but one are readonly.
Finding how core does it
addons/web/static/src/views/fields/
char/char_field.js
many2one/many2one_field.js
progress_bar/progress_bar_field.js
Before writing a widget, read the nearest standard one. They are short, they are the reference implementation of every convention above, and a good half of the widgets people write already exist.
What is next
A field widget with its own editing interface, where the value is not a simple input.