t-on-[event] Basics
The t-on- prefix followed by any browser event name attaches a listener to that element. The attribute value is a JavaScript expression evaluated in the component's scope.
<!-- Mouse events -->
<button t-on-click="handleClick">Click me</button>
<div t-on-mouseenter="onHover" t-on-mouseleave="onLeave"></div>
<div t-on-dblclick="handleDoubleClick"></div>
<!-- Form events -->
<form t-on-submit="handleSubmit"></form>
<input t-on-change="handleChange"/>
<input t-on-input="handleInput"/>
<input t-on-focus="handleFocus" t-on-blur="handleBlur"/>
<!-- Keyboard events -->
<input t-on-keydown="handleKeyDown"/>
<input t-on-keyup="handleKeyUp"/>
<!-- Window / document events (on an element) -->
<div t-on-scroll="handleScroll"></div>
Method Handlers
The most common pattern: pass the name of a class method. OWL calls it with the native Event object as the first argument.
class SearchBox extends Component {
state = useState({ query: "", results: [] });
static template = xml`
<div>
<input
t-model="state.query"
t-on-keydown="onKeyDown"
placeholder="Search…"
/>
<button t-on-click="search">Search</button>
</div>
`;
// Receives the native KeyboardEvent as argument
onKeyDown(ev) {
if (ev.key === "Enter") {
this.search();
}
if (ev.key === "Escape") {
this.state.query = "";
}
}
search() {
// this.state.query is available — no event arg needed here
console.log("Searching for:", this.state.query);
}
}
Inline Handlers
For simple one-liners, use an inline arrow function in the template attribute. The event object is available as ev:
<!-- Toggle a boolean -->
<button t-on-click="() => state.isOpen = !state.isOpen">Toggle</button>
<!-- Pass data to a method (useful in loops) -->
<button t-on-click="() => deleteItem(item.id)">Delete</button>
<!-- Inline with event object -->
<input t-on-keydown="(ev) => ev.key === 'Enter' && submitForm()"/>
<!-- Set state directly -->
<button t-on-click="() => state.filter = 'active'">Active</button>
Use inline for simple, obvious one-liners (toggle a flag, set a filter value). Use a method when the handler has more than one statement, needs to be tested, is used in multiple places, or involves async logic. Keep templates readable — if an inline expression requires mental parsing, move it to a named method.
Event Modifiers
OWL supports modifiers appended with a dot after the event name. They are applied before your handler runs:
<!-- .prevent — calls event.preventDefault() before the handler -->
<form t-on-submit.prevent="handleSubmit"></form>
<a t-on-click.prevent="navigate" href="/foo">Link</a>
<!-- .stop — calls event.stopPropagation() before the handler -->
<div t-on-click="handleParentClick">
<button t-on-click.stop="handleChildClick">Inner button</button>
</div>
<!-- .self — handler only fires if the event target IS this element (not a child) -->
<div class="modal-overlay" t-on-click.self="closeModal">
<div class="modal-content">…</div> <!-- clicking inside won't close -->
</div>
<!-- Combine modifiers -->
<form t-on-submit.prevent.stop="handleSubmit"></form>
| Modifier | What it does | Common use case |
|---|---|---|
.prevent | Calls event.preventDefault() | Prevent form submission page reload, prevent link navigation |
.stop | Calls event.stopPropagation() | Stop click from bubbling to parent overlay |
.self | Only fires if event.target === event.currentTarget | Modal backdrop click-to-close without closing when clicking the modal content |
Keyboard Event Patterns
Keyboard events (keydown, keyup, keypress) give you ev.key (readable name like "Enter", "Escape", "ArrowDown") and ev.code (physical key like "Space"). Always use ev.key for logic.
class CommandPalette extends Component {
state = useState({ isOpen: false, query: "" });
static template = xml`
<div>
<input
t-if="state.isOpen"
t-model="state.query"
t-on-keydown="handleKey"
placeholder="Type a command…"
/>
</div>
`;
handleKey(ev) {
switch (ev.key) {
case "Enter":
this.executeCommand(this.state.query);
break;
case "Escape":
this.state.isOpen = false;
this.state.query = "";
break;
case "ArrowDown":
ev.preventDefault(); // prevent page scroll
this.selectNext();
break;
case "ArrowUp":
ev.preventDefault();
this.selectPrev();
break;
}
}
// Check modifier keys
handleGlobalKey(ev) {
// Ctrl/Cmd + K: open command palette
if ((ev.ctrlKey || ev.metaKey) && ev.key === "k") {
ev.preventDefault();
this.state.isOpen = !this.state.isOpen;
}
}
}
Custom Events with trigger()
A child component can dispatch a custom event up the DOM tree using this.trigger(eventName, detail). A parent component can listen to it in the template with t-on-[event-name]. This is an alternative to callback props for child-to-parent communication — especially useful when the child is deeply nested.
// ── Child: fires a custom event ───────────────────────────────────
class ColorPicker extends Component {
static template = xml`
<div class="color-picker">
<button t-on-click="() => pick('red')" style="background:red"></button>
<button t-on-click="() => pick('green')" style="background:green"></button>
<button t-on-click="() => pick('blue')" style="background:blue"></button>
</div>
`;
pick(color) {
// Dispatch a custom "color-selected" event with a detail payload
this.trigger("color-selected", { color });
}
}
// ── Parent: listens to the custom event ───────────────────────────
class App extends Component {
static components = { ColorPicker };
state = useState({ selectedColor: "none" });
static template = xml`
<div>
<p>Selected: <t t-esc="state.selectedColor"/></p>
<!-- Listen with t-on-[event-name] -->
<ColorPicker t-on-color-selected="onColorSelected"/>
</div>
`;
onColorSelected(ev) {
// ev.detail contains the payload from trigger()
this.state.selectedColor = ev.detail.color;
}
}
Callback props are explicit and type-checkable. Use them when the parent directly uses the child and the interface should be clear. trigger() is better when: (1) the child is deeply nested and threading callback props is cumbersome, (2) multiple different parents might listen to the same event, or (3) you want to decouple the child from knowing its parent's API. In Odoo modules, trigger() is common because views often insert widgets without knowing all parent listeners.
Events in Loops
When handling events inside t-foreach, use inline arrow functions to capture the loop variable:
<ul>
<li t-foreach="state.items" t-as="item" t-key="item.id">
<span t-esc="item.name"/>
<!-- Arrow function captures 'item' from the loop scope -->
<button t-on-click="() => deleteItem(item.id)">Delete</button>
<button t-on-click="() => editItem(item)">Edit</button>
<!-- Named method doesn't work without capturing: avoid this -->
<!-- <button t-on-click="deleteItem">❌ no item.id passed</button> -->
</li>
</ul>
The Native Event Object
OWL passes the native browser Event (or its subclass) to your handler as the first argument. All standard event properties are available:
class FileDropZone extends Component {
static template = xml`
<div
class="drop-zone"
t-on-dragover.prevent="() => {}"
t-on-drop.prevent="handleDrop"
>
Drop files here
</div>
`;
handleDrop(ev) {
const files = Array.from(ev.dataTransfer.files); // native DragEvent API
console.log("Dropped files:", files.map(f => f.name));
}
}
class InputWithValidation extends Component {
static template = xml`
<input
type="email"
t-on-input="validateEmail"
t-on-blur="validateEmail"
/>
`;
validateEmail(ev) {
const value = ev.target.value; // ev.target is the input element
this.state.isValid = value.includes("@") && value.includes(".");
}
}
Common Real-World Patterns
class UIPatterns extends Component {
state = useState({
isMenuOpen: false,
selected: null,
confirmingDelete: false,
});
static template = xml`
<div>
<!-- 1. Toggle on click -->
<button t-on-click="() => state.isMenuOpen = !state.isMenuOpen">
Menu
</button>
<!-- 2. Close on outside click (modal overlay) -->
<div t-if="state.isMenuOpen"
class="overlay"
t-on-click.self="() => state.isMenuOpen = false">
<div class="menu">…</div>
</div>
<!-- 3. Confirm before delete -->
<button t-if="!state.confirmingDelete"
t-on-click="() => state.confirmingDelete = true">
Delete
</button>
<span t-if="state.confirmingDelete">
Are you sure?
<button t-on-click="confirmDelete">Yes</button>
<button t-on-click="() => state.confirmingDelete = false">No</button>
</span>
</div>
`;
confirmDelete() {
// perform delete…
this.state.confirmingDelete = false;
}
}
Handling Events with t-on
OWL wires DOM events in the template with the t-on- directive: t-on-eventname points at a method on your component. No addEventListener, no manual cleanup — OWL attaches and removes handlers with the component.
static template = xml`
<button t-on-click="onClick">Click</button>
<input t-on-input="onType"/>
<form t-on-submit.prevent="onSave">...</form>`;
onClick(ev) { console.log("clicked", ev); }
onType(ev) { this.state.text = ev.target.value; }
onSave() { /* .prevent already blocked page reload */ }
| Syntax | Does |
|---|---|
t-on-click="fn" | call fn on click |
t-on-click="() => fn(id)" | pass an argument inline |
t-on-submit.prevent | auto preventDefault() |
t-on-click.stop | auto stopPropagation() |
Child → parent communication: a child never reaches into its parent. Instead the parent passes a callback down as a prop, and the child calls it: this.props.onDelete(this.props.id). Data flows down as props, notifications flow up as function calls — the same one-way pattern React uses.
🏋️ Practical Exercise
Build a TagInput component that demonstrates advanced event handling:
- An
<input>where the user types a tag name. Pressing Enter adds the tag to a list. Pressing Backspace when the input is empty removes the last tag. - Each tag has an ✕ button. Clicking it removes that specific tag (use inline arrow in the loop).
- Use
t-on-keydownwithev.keyto handle Enter and Backspace. - Prevent form submission if the component is inside a
<form>by usingt-on-keydown.preventonly on Enter (hint: check the key first, then call preventDefault manually in the handler). - Fire a custom
"tags-changed"event viathis.trigger()whenever the tag list changes. In a parentApp, listen to it and display the current tag array.
🔥 Challenge Exercise
Wire up events: a button t-on-click calling a setup method, an input handler updating state, and a custom event bubbled to a parent. Explain OWL’s t-on- syntax, event modifiers, and how child-to-parent communication works (props callback vs events).
📋 Summary
t-on-[eventname]attaches a native browser event listener to any DOM element.- Handlers can be method names (
t-on-click="handleClick") or inline arrow functions (t-on-click="() => state.isOpen = true"). - The native
Eventobject is passed as the first argument to every handler method. - Modifiers:
.prevent(preventDefault),.stop(stopPropagation),.self(target must be the element itself). Combine with dots. - In loops, use inline arrows to capture the loop variable:
() => deleteItem(item.id). this.trigger("event-name", detail)dispatches a custom DOM event that bubbles up. Parent listens witht-on-event-name="handler".- Keyboard events: use
ev.key(e.g."Enter","Escape","ArrowDown") rather thanev.keyCode(deprecated).
Interview Questions
- How do you handle events in OWL?
- What is the
t-on-directive? - What are event modifiers?
- How does a child notify its parent?
- How do you pass data to an event handler?
Related Topics
FAQ
When the parent passes onSave.bind="save", the child calls props.onSave(data) passing its own argument — not a DOM event. The parent's save method receives whatever the child passes. This is by design — callback props carry business data (like form values), not raw DOM events. If you need the event itself, use trigger() instead, which dispatches an actual DOM CustomEvent.
Yes, but you cannot do it with t-on-* since that requires a DOM element in the template. Use onMounted to add the listener and onWillUnmount to remove it: window.addEventListener('keydown', this.boundHandler) in onMounted, window.removeEventListener('keydown', this.boundHandler) in onWillUnmount. Store the bound handler as a property so you can remove the exact same reference.
input fires on every keystroke (immediately as the value changes). change fires when the element loses focus OR when a selection-type input (checkbox, radio, select) changes. Use t-on-input for live search or real-time validation. Use t-on-change for select dropdowns or when you only want to react when the user finishes typing and moves away. For most controlled inputs in OWL, t-model uses input internally.
Yes — this.trigger(name, detail) is a thin wrapper that creates a native CustomEvent with bubbles: true and composed: true and dispatches it on the component's root DOM element. It bubbles normally through the DOM. The parent's t-on-[name] catches it when it reaches the parent's DOM element. You can also listen to it with plain element.addEventListener() if needed.

