Advertisement
🦉 Core Concepts

OWL JS Event Handling – t-on Directives & Custom Events

Events are how users interact with your OWL components — clicking buttons, submitting forms, typing in inputs, pressing keyboard shortcuts. OWL handles all DOM events through the t-on-[eventname] directive, which attaches native browser event listeners in a declarative way. Beyond DOM events, OWL components can also fire custom events with this.trigger(), allowing a child to communicate upward without needing a callback prop. This lesson covers everything: inline handlers, method handlers, all modifier combinations, keyboard events, and custom event patterns.

⏱️ 20 min read 🎯 Beginner 📅 Updated 2026 👁️ Lesson 5 of 5

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.

XML – common t-on examples
<!-- 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.

JavaScript – method handlers
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:

XML – inline handlers
<!-- 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>
💡
Inline vs method: which to use?

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.

Advertisement

Event Modifiers

OWL supports modifiers appended with a dot after the event name. They are applied before your handler runs:

XML – event modifiers
<!-- .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>
ModifierWhat it doesCommon use case
.preventCalls event.preventDefault()Prevent form submission page reload, prevent link navigation
.stopCalls event.stopPropagation()Stop click from bubbling to parent overlay
.selfOnly fires if event.target === event.currentTargetModal 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.

JavaScript – keyboard event handling
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.

JavaScript – custom event with trigger()
// ── 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;
  }
}
ℹ️
trigger() vs callback props — when to use each

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:

XML – event handlers in loops
<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:

JavaScript – using the event object
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

JavaScript – common event 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 */ }
SyntaxDoes
t-on-click="fn"call fn on click
t-on-click="() => fn(id)"pass an argument inline
t-on-submit.preventauto preventDefault()
t-on-click.stopauto 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:

  1. 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.
  2. Each tag has an ✕ button. Clicking it removes that specific tag (use inline arrow in the loop).
  3. Use t-on-keydown with ev.key to handle Enter and Backspace.
  4. Prevent form submission if the component is inside a <form> by using t-on-keydown.prevent only on Enter (hint: check the key first, then call preventDefault manually in the handler).
  5. Fire a custom "tags-changed" event via this.trigger() whenever the tag list changes. In a parent App, 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 Event object 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 with t-on-event-name="handler".
  • Keyboard events: use ev.key (e.g. "Enter", "Escape", "ArrowDown") rather than ev.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?

FAQ

Why doesn't my method get the event object when I use a callback prop? +

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.

Can I add event listeners to window or document in OWL? +

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.

What is the difference between t-on-input and t-on-change? +

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.

Is this.trigger() the same as the browser's CustomEvent? +

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.