How Widget Events Work in Odoo 19 OWL JavaScript Widgets
Widget events power component-to-component communication in Odoo 19's OWL framework. Rather than calling another component's methods directly, components share information through callback events — keeping each piece independent and reusable. This complete tutorial is a beginner guide and a step by step guide that explains exactly how to build event-driven OWL widgets. You will learn how to define callback props on a child component, emit events upward to a parent, bind handlers with the .bind suffix, pass structured data between components, react to changes with useState, and register your component so Odoo can render it. By the end of this complete tutorial you will understand the full callback-based architecture that keeps Odoo 19 dashboards, dialogs, and custom Kanban views clean, flexible, and maintainable.
What You'll Learn:
- What widget events are and why OWL uses callback props instead of direct method calls
- How the Odoo 19 parent-child component hierarchy passes callbacks down via props
- How to declare callback props on a child with static props
- How a child emits events upward by invoking
this.props.onSomething(...) - How to bind a parent handler using the critical .bind suffix in the template
- How to pass multiple values between components by sending a single data object
- How to use useState so OWL automatically re-renders when state changes
- How to register the component in the
actionsregistry and wire it to a client action
What Are Widget Events in Odoo 19 OWL?
Widget events enable one component to announce that something occurred to another component. Instead of a child reaching into its parent and calling the parent's methods directly — which tightly couples the two — the child simply invokes a callback function that the parent handed it through props. The parent decides what that callback does. This inversion of control is the foundation of reusable, decoupled OWL widgets.
You reach for widget events whenever: a child component needs to notify its parent that an action happened; multiple components must respond to the same user interaction; data has to be shared between sibling or nested components; or a component needs to be reusable across different contexts without knowing anything about who is using it. A button widget, for example, should not care whether it lives inside a dashboard, a dialog, or a Kanban card — it just fires its callback and lets the host decide.
Component Architecture: Parent and Child
Odoo 19 follows a hierarchical parent-child structure. A single Parent component can host several children (Parent → Child A, Child B). The parent passes callback methods to each child through props. When a child invokes one of those callbacks, the parent's corresponding method executes — in the parent's own context. The data flows down as props and events flow up as callback invocations, giving you a predictable one-way data model.
Callback Props
A parent passes a function to a child as a prop. The child calls it when an event happens, e.g. this.props.onRecordSelected(...). The child never knows or cares what the function does — it only triggers it. This keeps components loosely coupled and easy to reuse anywhere.
Parent-Child Communication
Data flows down through props; events flow up through callbacks. A child notifies its parent without ever importing or referencing the parent class. The parent binds its handler in the template using the .bind suffix so the method runs in the parent's context.
Reactive State (useState)
Wrap component state in useState inside setup(). When a callback mutates that state — for example storing the selected record ID — OWL automatically detects the change and re-renders the template. No manual DOM updates or re-render calls are ever required.
Component Registration
Register the parent component in the actions category of the OWL registry, then reference it from an ir.actions.client record. The XML tag value must exactly match the registration key so Odoo knows which component to mount.
Step by Step Guide: Building Event-Driven OWL Widgets
This step by step guide walks through the full lifecycle of a widget event — from setting up the OWL module, to defining and emitting callbacks on the child, to binding handlers and reacting to data on the parent, to finally registering the component so Odoo can render it.
Set Up the OWL Module
Start every OWL JavaScript file with the /** @odoo-module */ directive on the first line. This tells Odoo's asset bundler to treat the file as an ES module so you can use import and export. Import the building blocks you need — at minimum Component and, for reactive state, useState — from @odoo/owl. Without the directive, Odoo will not recognize the file as a module and your imports will silently fail.
Define Callback Props on the Child
Declare the callbacks your child component expects using static props. Each callback is typed as Function, for example onRecordSelected: Function. Explicitly declaring props makes the component's public interface clear, lets OWL validate that the parent actually supplied the callback, and documents exactly which events the component can emit. A child that lists onRecordSelected, onProductChanged, and onReloadParent is self-documenting.
Emit Events from the Child
Inside the child's methods, invoke the callback through props to notify the parent: this.props.onRecordSelected({ recordId: 10 }). Wire these methods to DOM events in the child template with directives like t-on-click. When the user clicks a button, the child's method runs, which in turn calls the parent's callback. The child stays completely unaware of what the parent does in response.
Bind Handlers on the Parent
In the parent template, pass the handler to the child using the .bind suffix: <ChildWidget onRecordSelected.bind="_onRecordSelected" />. The .bind suffix is critical — it guarantees the method executes within the parent's context so that this refers to the parent component. Also register the child in the parent's static components object so OWL knows how to resolve the <ChildWidget /> tag.
Pass Data Between Components
To send multiple values, have the child pass a single object to the callback — for example { productId: 45, quantity: 3, price: 120 } — and have the parent destructure or read the fields it needs. This keeps callback signatures stable as requirements grow. Follow the minimal-data principle: pass only what the parent needs (often just a record ID), not entire record objects, to keep the contract lean and avoid leaking internal structure.
Register the Component
Make the parent reachable by adding it to the OWL registry: registry.category("actions").add("my_module_action", ParentWidget). Then create an ir.actions.client XML record whose tag field equals the registration key (my_module_action). When a user triggers that client action — from a menu item, button, or URL — Odoo mounts your parent component and the whole event-driven tree comes alive.
Step 1: Set Up the OWL Module
Every OWL file begins with the @odoo-module directive, which enables Odoo to recognize the file as an ES module. Then import the OWL primitives you need. For an event-driven widget with reactive state, you need Component and useState.
/** @odoo-module */
import { Component, useState } from "@odoo/owl";
// The @odoo-module directive above MUST be the first line.
// It enables Odoo to recognize this file as an ES module so
// that import / export statements resolve correctly.
Step 2 & 3: The Child Component — Props and Emitting Events
The child declares the callbacks it expects with static props, then emits events by calling those callbacks through this.props. Notice the child never imports the parent — it only knows it has been handed some functions to call.
/** @odoo-module */
import { Component } from "@odoo/owl";
export class ChildWidget extends Component {
static template = "MyModule.ChildWidget";
// Explicitly declare every callback this child can emit.
static props = {
onRecordSelected: Function,
onProductChanged: Function,
onReloadParent: Function,
};
// Emit a simple event with one value.
_onSelectRecord() {
this.props.onRecordSelected({ recordId: 25 });
}
// Emit an event carrying multiple values inside one object.
_onChangeProduct() {
this.props.onProductChanged({
productId: 45,
quantity: 3,
price: 120,
});
}
// Emit an event with no payload (just a signal).
_onCreateRecord() {
this.props.onReloadParent({});
}
}
Wire those methods to DOM events in the child's XML template. The t-on-click directive connects each button to the matching method.
<t t-name="MyModule.ChildWidget">
<div>
<button class="btn btn-primary"
t-on-click="_onSelectRecord">
Select Record
</button>
<button class="btn btn-secondary"
t-on-click="_onChangeProduct">
Change Product
</button>
<button class="btn btn-success"
t-on-click="_onCreateRecord">
Create Record
</button>
</div>
</t>
Step 4: The Parent Component and the .bind Suffix
The parent registers the child in static components, defines the handler methods, and receives the child's events. Below, _onRecordSelected simply logs the incoming record ID — but it could update state, reload data, or open a dialog.
/** @odoo-module */
import { Component, useState } from "@odoo/owl";
import { ChildWidget } from "./child_widget";
export class ParentWidget extends Component {
static template = "MyModule.ParentWidget";
// Register the child so OWL can resolve <ChildWidget />.
static components = { ChildWidget };
setup() {
// Reactive state — see Step 6.
this.state = useState({ logs: [] });
}
// Handler that receives the child's onRecordSelected event.
_onRecordSelected(data) {
console.log(data.recordId);
}
}
Now bind the parent's handler to the child in the parent template. The .bind suffix ensures the method executes within the parent's context, so this inside _onRecordSelected correctly points to the parent instance.
<t t-name="MyModule.ParentWidget">
<div>
<!-- The .bind suffix binds the handler to the parent's context -->
<ChildWidget
onRecordSelected.bind="_onRecordSelected"
onProductChanged.bind="_onProductChanged"
onReloadParent.bind="_onReloadParent" />
</div>
</t>
Always Use the .bind Suffix on Callback Props
Forgetting the .bind suffix means the handler runs in the wrong context. Without it, this inside your parent method will be undefined, so any reference to this.state, this.reload(), or other parent members throws a runtime error. Writing onRecordSelected="_onRecordSelected" (no .bind) is one of the most common OWL mistakes — always write onRecordSelected.bind="_onRecordSelected".
Step 5: Passing Data Between Components
When an event needs to carry more than one value, the child sends a single data object and the parent reads the fields it cares about. This keeps the callback signature stable even as the payload grows. The parent simply destructures or accesses the object's properties.
// CHILD: send several values in one object
_onChangeProduct() {
this.props.onProductChanged({
productId: 45,
quantity: 3,
price: 120,
});
}
// PARENT: read the values from the data object
_onProductChanged(data) {
const productId = data.productId;
const quantity = data.quantity;
const price = data.price;
// ...use the values, e.g. update a line total
console.log(productId, quantity, price);
}
// Or destructure directly in the parameter list:
_onProductChangedShort({ productId, quantity, price }) {
console.log(productId, quantity, price);
}
DOM Events vs Component Events
It is important to distinguish the two kinds of events you work with in OWL. DOM events are fired by the browser when the user interacts with an element. Component events are how components talk to one another — and in OWL they are implemented with callback props rather than a separate event-bus.
| Aspect | DOM Events | Component Events |
|---|---|---|
| Triggered by | Browser actions on an element | One component signalling another |
| Typical use | User interactions (clicks, changes, input) | Component-to-component communication |
| Syntax / mechanism | t-on-click, t-on-change in the template |
Implemented via callback props (onXyz.bind) |
| Scope | A single DOM element and its listeners | Across the parent-child component tree |
The Parent Refresh Pattern
A very common requirement is letting a child trigger a refresh of the parent — for instance after creating a record in a dialog. The child fires a no-payload event; the parent's handler calls its own reload() method. This pattern is everywhere in Odoo 19 dialogs, dashboards, and Kanban views.
// CHILD: fire a no-payload signal after creating a record
_createRecord() {
// ...perform the create...
this.props.onReloadParent({});
}
// PARENT: respond by reloading itself
_onReloadParent() {
this.reload();
}
// Commonly used in dialogs, dashboards, and Kanban views to
// refresh the parent view after a child mutates data.
Step 6: Reactive State with useState
To make the UI respond to incoming events, store data in a reactive object created with useState inside setup(). When a callback mutates that state — for example saving the selected record ID — OWL automatically re-renders the template. You never call render manually.
setup() {
this.state = useState({
logs: [],
recordId: null,
});
}
_onRecordSelected(data) {
// Mutating reactive state triggers an automatic re-render.
this.state.recordId = data.recordId;
this.state.logs.push("Selected record " + data.recordId);
}
// When state changes, OWL automatically re-renders the template —
// no manual render() call is ever needed.
Registering the Component as a Client Action
Finally, expose your parent component to Odoo by adding it to the actions registry, then point an ir.actions.client record at it. The XML tag must match the registration key exactly.
/** @odoo-module */
import { registry } from "@web/core/registry";
import { ParentWidget } from "./parent_widget";
// The key "my_module_action" must match the XML tag below.
registry.category("actions").add("my_module_action", ParentWidget);
<record id="action_my_widget" model="ir.actions.client">
<field name="name">My Widget Demo</field>
<!-- tag MUST equal the registry key "my_module_action" -->
<field name="tag">my_module_action</field>
</record>
Best Practices for Widget Events
Following a few conventions keeps your event-driven widgets clean, predictable, and reusable across the entire Odoo 19 web client.
| Practice | Why It Matters |
|---|---|
| Use meaningful callback names | Descriptive names like onRecordSaved or onInvoiceConfirmed communicate intent far better than generic onEvent or onUpdate, making the component self-documenting. |
| Keep components independent | Never access parent methods directly. Communicating only through callback props lets the same child be reused in any parent, dialog, or view without modification. |
| Declare props explicitly | Using static props documents the component's public interface and lets OWL validate that every required callback was actually supplied by the parent. |
| Transfer minimal data | Pass only what the parent needs — often just a record ID — instead of whole record objects. This keeps the contract lean and avoids leaking internal structure between components. |
Key Insight: Callback Props Keep Components Independent and Reusable
The entire value of widget events comes from inversion of control. A child that emits onRecordSelected has no idea what happens next — it might update a dashboard count, open a dialog, or reload a Kanban column. Because the child only fires a callback and never references its parent, the exact same component can be dropped into any context and reused without changes. This callback-based architecture is what makes OWL widgets composable: each piece does one job, announces what happened, and lets the host decide how to react. Combine it with useState for automatic re-rendering and you have a clean, flexible foundation for dashboards, dialogs, and custom Kanban views.
Frequently Asked Questions
What does the .bind suffix do on a callback prop in OWL?
The .bind suffix binds the handler to the parent component's context so that this inside the method refers to the parent instance. Without it, this is undefined and any reference to this.state or other parent members throws an error. Always write onRecordSelected.bind="_onRecordSelected".
How is a child component event different from a DOM event in Odoo 19?
A DOM event is fired by the browser when a user interacts with an element and is captured with directives like t-on-click. A component event is how one component notifies another, and in OWL it is implemented with callback props rather than a native event. The child calls this.props.onXyz(...) and the parent's bound handler runs.
How do I pass multiple values from a child to a parent component?
Send a single object as the callback argument, for example this.props.onProductChanged({ productId: 45, quantity: 3, price: 120 }). The parent then reads or destructures the fields it needs. Using one object keeps the callback signature stable even when you later add more fields to the payload.
Why use useState instead of plain object properties in OWL?
Wrapping state in useState makes it reactive. When a callback mutates a property of the reactive object, OWL automatically re-renders the template to reflect the change. Plain properties are not tracked, so the UI would not update. Create the state in setup() with this.state = useState({ ... }).
Why must the client action tag match the registry key?
When you register a component with registry.category("actions").add("my_module_action", ParentWidget), Odoo looks up the component by that key when an ir.actions.client with the same tag is triggered. If the tag and key differ, Odoo cannot find the component and the action fails to render.
Need Help Building Custom Odoo 19 OWL Widgets?
Our certified Odoo developers design event-driven OWL components, custom dashboards, dialogs, and Kanban views that are clean, reusable, and production-ready. Let us accelerate your Odoo 19 frontend development.
About the author
Odoo Practice Lead, Braincuber Technologies
Leads the Odoo practice at Braincuber. Has delivered Odoo ERP implementations, NetSuite/Tally migrations, and Shopify–Odoo integrations for US mid-market and D2C brands. Owns scoping, data migration, and go-live for every Odoo engagement.
