Using Interactions to Serve JavaScript in Odoo 19.0

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:

  1. Targets a CSS selector in the rendered DOM
  2. Runs automatically when that element is present on the page
  3. 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 publicWidget system from web.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

ScenarioUse
Enhance an existing XML-rendered template✅ Interaction
Build a fully reactive, stateful UI widgetOWL Component
Listen to clicks or form inputs on a static page✅ Interaction
Need slots, child components, or complex renderingOWL 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 selector to target your DOM element
  • Use setup() for initialization and destroy() for cleanup
  • Use dynamicContent for reactive event binding and attribute updates
  • Declare static services to inject Odoo services like rpc
  • 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.

Book a Free Consultation

Haven’t found the right solution yet?
Tell us about your needs, and let’s discuss the best solution together.

Project description.

Built a private marketplace research and purchase planning platform for Shopee. The system tracks product performance over time, estimates sales velocity and gross revenue, analyzes demand across product variants, and turns the research into actionable purchasing plans. It also calculates supplier costs, shipping, platform fees, margins, planned investment, and estimated earnings to support data-driven sourcing decisions.

Skills and deliverables

Full-Stack Development, Web Application Development, Data Analysis, Business Process Automation, Business Intelligence

Shopee Marketplace Research & Purchase Planning Dashboard

We built this application to solve a common problem in marketplace sourcing: finding products is easy, but deciding what is actually worth buying, which variant to prioritize, and how much inventory to purchase requires much more analysis. Instead of relying on a single product snapshot, the system organizes products into research collections and tracks their marketplace performance over time.

Product research & tracking

Products can be added individually or collected from marketplace stores and organized into dedicated research collections. The system keeps historical observations so users can compare changes between captures instead of relying only on the latest marketplace data. This makes it possible to estimate metrics such as: – Sales velocity – Estimated daily gross revenue – Price changes – Product movement over time – Variant-level demand

Variant demand analysis

One of the most important parts of the system is identifying which variants are actually driving sales. Instead of treating a product as a single item, the application analyzes sales movement at variant level and shows each variant’s contribution to recent demand. This helps identify which sizes, models, or variations should be prioritized when purchasing inventory.

Purchase planning

Research data can then be converted directly into a purchasing plan. For every product and variant, the user can enter supplier pricing, shipping costs, purchase quantities, exchange rates, and other operational costs. The system automatically calculates: – Planned inventory investment – Shipping cost – Marketplace/platform fees – Net margin per unit – Net margin percentage – Estimated earnings – Total batch investment This allows purchasing decisions to be evaluated before committing capital.

Why We built it

The goal was to replace a workflow that would normally require multiple spreadsheets, manual marketplace checks, and repeated calculations. The resulting workflow became: Discover products → Track performance → Analyze demand → Select variants → Plan purchasing → Estimate margins Everything is kept in one application, making the research and sourcing process easier to review, repeat, and scale.

Result

The platform turns raw marketplace observations into actionable purchasing information. Instead of choosing products based only on intuition or current sales numbers, users can evaluate sales movement, variant demand, sourcing cost, expected margin, and required investment before making a purchasing decision. Built as an end-to-end internal tool, from data collection and analysis to the final purchasing workflow.

Project description.

Built a browser-based tool that converts Shopee/SPX order exports into clean, print-ready shipping labels. It supports multiple Excel/CSV files, combines orders into one preview, and automatically generates QR codes, barcodes, tracking numbers, and product information for 68 mm thermal printers. All order data is processed locally in the browser for better privacy, eliminating repetitive manual label formatting.

Skills and deliverables

Laravel, Business Automation, CSV, Microsoft Excel

Shopee SPX Order Label Maker for Thermal Printing

This tool was built to simplify the shipping label preparation process for Shopee SPX orders. The existing workflow required order data to be exported from Shopee and manually formatted before it could be printed on a thermal printer. This became repetitive and time-consuming, especially when processing multiple orders at once.

We built a browser-based label generator that reads Shopee Excel/CSV exports and automatically converts the order data into clean, print-ready SPX labels. The tool automatically handles: – Tracking numbers – QR codes – Barcodes – Product names and variants – Order quantities – Multiple order files at once – 68 mm thermal printer formatting Users can review all generated labels before printing, without having to manually copy or reorganize the order information.

The order files are processed directly inside the browser instead of being uploaded to a server. This keeps customer and shipment data private while also making the tool fast and simple to use.