Part 2. Components · lesson 8 of 40
Events: how a child talks back to a parent
After this lesson you can: Handle DOM events, pass callbacks upward correctly, and recognise OWL 1 event code that no longer works.
Written against Odoo 19.0 and 18.0. Last checked 2026-10-09.
Props go down. Something has to come back up. In OWL 2 that something is a function.
DOM events
<button t-on-click="onSave">Save</button>
<input t-on-input="onType"/>
<form t-on-submit.prevent="onSubmit">...</form>
<div t-on-click.stop="onClick">...</div>
The handler receives the DOM event:
onType(ev) {
this.state.query = ev.target.value;
}
this is bound for you when the handler is a method name.
Modifiers
| Modifier | Effect |
|---|---|
.prevent | event.preventDefault() |
.stop | event.stopPropagation() |
.capture | Listen during capture rather than bubble |
.self | Only when the event started on this element, not a child |
.synthetic | Attach at the root rather than the element |
They chain: t-on-click.stop.prevent="handler".
.prevent on a form submit is the common one. Without it the browser navigates
and your single page application reloads itself.
Passing arguments
<button t-on-click="() => this.remove(invoice.id)">Remove</button>
Inline arrow functions in templates are normal and expected here. If you need the event as well:
<button t-on-click="(ev) => this.remove(ev, invoice.id)">Remove</button>
Component events: the thing that changed
If you find a tutorial doing this, it is OWL 1, and it does not work:
// OWL 1. Does not exist any more.
this.trigger("invoice-selected", { id: 42 });
<!-- OWL 1. Does not exist any more. -->
<InvoiceCard t-on-invoice-selected="onInvoiceSelected"/>
OWL 2 removed it deliberately. The reasons are worth understanding because they explain the replacement.
A bubbling custom event is invisible in the code. Reading a child, you cannot tell who listens. Reading a parent, you cannot tell what might arrive. It also crosses component boundaries it has no business crossing, so a component three levels up can quietly depend on a grandchildโs internals.
Callback props, the replacement
The parent passes a function. The child calls it.
// Parent
export class InvoiceList extends Component {
static template = "owl_course.InvoiceList";
static components = { InvoiceCard };
setup() {
this.state = useState({ selectedId: null });
}
onSelect(id) {
this.state.selectedId = id;
}
}
<t t-name="owl_course.InvoiceList">
<div>
<InvoiceCard
t-foreach="state.invoices" t-as="invoice" t-key="invoice.id"
invoice="invoice"
onSelect="(id) => this.onSelect(id)"
/>
</div>
</t>
// Child
export class InvoiceCard extends Component {
static template = "owl_course.InvoiceCard";
static props = {
invoice: Object,
onSelect: { type: Function, optional: true },
};
select() {
this.props.onSelect?.(this.props.invoice.id);
}
}
Note ?. on the call. The prop is optional, so a parent that does not care
simply does not pass it, and the child does not need to know.
The naming convention across Odooโs own code is onSomething. Follow it.
Why this is better
You can read a componentโs static props and know everything it can do to the
outside world. You can read a template and see exactly what a child will call.
Nothing travels through layers that did not opt in.
The cost is that passing a callback through four levels is tedious. That is a real cost, and when you hit it, it is usually a signal that the state belongs in a service rather than in a component four levels up. Part 5.
Listening outside the component
Sometimes you need an event on document or window, for a keyboard shortcut
or an outside click.
import { Component, onMounted, onWillUnmount } from "@odoo/owl";
export class Dropdown extends Component {
setup() {
this.onDocClick = (ev) => {
if (!this.rootRef.el?.contains(ev.target)) {
this.state.open = false;
}
};
onMounted(() => document.addEventListener("click", this.onDocClick));
onWillUnmount(() => document.removeEventListener("click", this.onDocClick));
}
}
Every listener added by hand must be removed. Not removing it is a genuine leak: the component is destroyed, the closure survives, and it keeps a reference to a dead component. In a backend session where a user opens hundreds of records, these accumulate.
Keep the same function reference for both calls. addEventListener with an
inline arrow and removeEventListener with a different inline arrow removes
nothing.
For keyboard shortcuts specifically, Odoo has a hotkey service that handles
registration and cleanup. Prefer it. Part 5.
What is next
Slots: letting a parent put its own markup inside your component.