Odoo 19 introduces the Interaction API as the modern way to attach JavaScript behavior to your XML templates. If you’ve previously written OWL components or used legacy publicWidget, learning Interactions will significantly simplify how you structure frontend code in custom modules.
This guide explains what Interactions are, why they exist, and how to use them step by step.
What Is an Interaction?
An Interaction is a JavaScript class that:
- Targets a CSS selector in the rendered DOM
- Runs automatically when that element is present on the page
- Can listen to events, manipulate the DOM, or call RPC methods
Think of it as a controller that wakes up when its element appears, and cleans up when it disappears.
Why Interactions?
Before Odoo 19, attaching JavaScript to a rendered page required either:
- Writing a full OWL component, or
- Using the older
publicWidgetsystem fromweb.public.widget
Both approaches had friction. OWL components need a mount point and a full reactive setup. publicWidget still works but is considered legacy.
Interactions offer a middle path: a lightweight, declarative way to bind JavaScript logic directly to DOM elements rendered by your XML templates without the overhead of a full component.
File Structure
For a custom module named floo_smart_address, a typical Interaction setup looks like this:
floo_smart_address/
├── static/
│ └── src/
│ └── interactions/
│ └── address_form.js
├── views/
│ └── address_template.xml
└── __manifest__.py
You also need to declare your JS file as an asset in __manifest__.py:
"assets": {
"web.assets_frontend": [
"floo_smart_address/static/src/interactions/address_form.js",
],
},
Writing Your First Interaction
Here is a minimal Interaction that targets a <div class="address-form"> element and logs a message when found:
import { Interaction } from "@web/public/interaction";
import { registry } from "@web/core/registry";
class AddressFormInteraction extends Interaction {
static selector = ".address-form";
setup() {
console.log("AddressFormInteraction is running!", this.el);
}
}
registry.category("public.interactions").add(
"floo_smart_address.AddressFormInteraction",
AddressFormInteraction
);
Breaking this down:
static selector: the CSS selector that activates this Interaction. Odoo scans the page for matching elements and runs one instance per match.setup(): runs once when the element is found. This is where you initialize your logic.registry.category("public.interactions").add(...): registers the Interaction so Odoo’s frontend service discovers it automatically.
Listening to DOM Events
Interactions support a dynamicContent property for declaratively binding event listeners:
import { Interaction } from "@web/public/interaction";
import { registry } from "@web/core/registry";
class AddressFormInteraction extends Interaction {
static selector = ".address-form";
dynamicContent = {
".btn-submit": {
"t-on-click": this.onSubmitClick,
},
};
onSubmitClick(event) {
event.preventDefault();
console.log("Submit button clicked!");
}
}
registry.category("public.interactions").add(
"floo_smart_address.AddressFormInteraction",
AddressFormInteraction
);
- dynamicContent maps CSS selectors inside your element to event handlers.
The key format follows the t-on-{eventname} convention familiar from OWL templates.
Updating the DOM Reactively
If you need to show or hide elements based on state, you can combine dynamicContent with t-att-class or t-if-style patterns:
import { Interaction } from "@web/public/interaction";
import { registry } from "@web/core/registry";
import { reactive } from "@odoo/owl";
class AddressFormInteraction extends Interaction {
static selector = ".address-form";
setup() {
this.state = reactive({ isLoading: false });
}
dynamicContent = {
".loading-spinner": {
"t-att-class": () => ({ "d-none": !this.state.isLoading }),
},
".btn-submit": {
"t-on-click": this.onSubmitClick,
},
};
onSubmitClick(event) {
event.preventDefault();
this.state.isLoading = true;
setTimeout(() => {
this.state.isLoading = false;
}, 2000);
}
}
registry.category("public.interactions").add(
"floo_smart_address.AddressFormInteraction",
AddressFormInteraction
);
When this.state.isLoading changes, dynamicContent re-evaluates automatically without any manual DOM manipulation required.
Making RPC Calls
To fetch data from your Odoo backend, use the rpc service available through this.services:
import { Interaction } from "@web/public/interaction";
import { registry } from "@web/core/registry";
class AddressFormInteraction extends Interaction {
static selector = ".address-form";
static services = ["rpc"];
async setup() {
const result = await this.services.rpc(
"/web/dataset/call_kw",
{
model: "res.partner",
method: "search_read",
args: [[["is_company", "=", true]]],
kwargs: { fields: ["name"], limit: 5 },
}
);
console.log("Partners:", result);
}
}
registry.category("public.interactions").add(
"floo_smart_address.AddressFormInteraction",
AddressFormInteraction
);
Declare any services your Interaction needs in static services and Odoo will inject them automatically into this.services.
Cleanup with destroy()
If your Interaction sets up intervals, external listeners, or third-party libraries, clean them up in destroy():
setup() {
this.interval = setInterval(() => {
console.log("Polling...");
}, 3000);
}
destroy() {
clearInterval(this.interval);
}
Odoo calls destroy() automatically when the element leaves the DOM.
Interactions vs. OWL Components Know When to Use Which
| Scenario | Use |
|---|---|
| Enhance an existing XML-rendered template | ✅ Interaction |
| Build a fully reactive, stateful UI widget | OWL Component |
| Listen to clicks or form inputs on a static page | ✅ Interaction |
| Need slots, child components, or complex rendering | OWL Component |
| RPC calls triggered by user actions | ✅ Interaction |
| Portal or website frontend pages | ✅ Interaction |
For most website and portal module customizations, Interactions are the right tool.
Common Mistakes to Avoid
Forgetting to register the asset. If your JS file is not listed under web.assets_frontend in __manifest__.py, the Interaction will never load. Always check the browser console for 404s on your JS files.
Using web.assets_backend for frontend pages. Portal and website pages use web.assets_frontend. Backend views use web.assets_backend. Mixing these is a common source of confusion.
Not upgrading the module after adding new files. Odoo caches assets. After adding a new JS file, always upgrade your module (or use -u your_module with --dev=all) so the asset bundle is rebuilt.
Summary
Interactions in Odoo 19 give you a clean, structured way to attach JavaScript to your templates without writing full OWL components. Key points:
- Define
static selectorto target your DOM element - Use
setup()for initialization anddestroy()for cleanup - Use
dynamicContentfor reactive event binding and attribute updates - Declare
static servicesto inject Odoo services likerpc - Register with
registry.category("public.interactions")
Once you understand this pattern, you’ll find it applies consistently across website pages, portal views, and any frontend template in your custom modules.