frontend-js

Installation
SKILL.md

Odoo Frontend JavaScript Patterns

Critical Rules

  1. Website themes: Use publicWidget framework ONLY — NOT Owl or vanilla JS
  2. JS modules: Start every file with /** @odoo-module **/
  3. No inline JS/CSS: Always separate files in static/src/js/ and static/src/scss/
  4. Bootstrap: v5.1.3 for Odoo 16+ (never Tailwind)
  5. Translations: Use _t() at DEFINITION TIME for static JS labels

Version Detection

Odoo Bootstrap Owl JavaScript
14 4.x ES6+
15 4.x v1 ES6+
16 5.1.3 v1 ES2020+
17 5.1.3 v2 ES2020+
18-19 5.1.3 v2 ES2020+

Detect from path (odoo17/ → v17), manifest version field, or config file.


publicWidget Pattern (REQUIRED for Themes)

Use for: Website interactions, theme functionality, animations, forms

/** @odoo-module **/

import publicWidget from "@web/legacy/js/public/public_widget";

publicWidget.registry.MyWidget = publicWidget.Widget.extend({
    selector: '.my-selector',
    disabledInEditableMode: false,  // Allow in website builder

    events: {
        'click .button': '_onClick',
        'change input': '_onChange',
        'submit form': '_onSubmit',
    },

    /**
     * CRITICAL: Check editableMode for website builder compatibility
     */
    start: function () {
        if (!this.editableMode) {
            this._initializeAnimation();
            this._bindExternalEvents();
        }
        return this._super.apply(this, arguments);
    },

    _initializeAnimation: function () {
        this.$el.addClass('animated');
    },

    _bindExternalEvents: function () {
        $(window).on('scroll.myWidget', this._onScroll.bind(this));
        $(window).on('resize.myWidget', this._onResize.bind(this));
    },

    _onClick: function (ev) {
        ev.preventDefault();
        if (this.editableMode) return;
        // Handler logic
    },

    /**
     * CRITICAL: Clean up event listeners to prevent memory leaks
     */
    destroy: function () {
        $(window).off('.myWidget');  // Remove namespaced events
        this._super.apply(this, arguments);
    },
});

export default publicWidget.registry.MyWidget;

Key Points

  1. ALWAYS check this.editableMode before animations/interactions
  2. disabledInEditableMode: false makes widgets work in website builder
  3. ALWAYS clean up event listeners in destroy()
  4. NEVER use Owl or vanilla JS for website themes
  5. Use namespaced events (.myWidget) for easy cleanup

Include in Manifest

'assets': {
    'web.assets_frontend': [
        'module_name/static/src/js/my_widget.js',
    ],
}

Owl Component Pattern

Odoo 17 (Owl v1)

/** @odoo-module **/

import { Component, useState } from "@odoo/owl";
import { registry } from "@web/core/registry";

class MyComponent extends Component {
    setup() {
        this.state = useState({ items: [], loading: false });
    }

    async willStart() {
        await this.loadData();
    }
}

MyComponent.template = "module_name.MyComponentTemplate";
registry.category("public_components").add("MyComponent", MyComponent);

Odoo 18-19 (Owl v2 — Breaking Changes)

/** @odoo-module **/

import { Component, useState } from "@odoo/owl";

class MyComponent extends Component {
    static template = "module_name.MyComponentTemplate";  // Static property
    static props = {
        title: { type: String, optional: true },
        items: { type: Array },
    };

    setup() {
        this.state = useState({ selectedId: null });
    }
}

XML Template

<template id="MyComponentTemplate" name="My Component">
    <div class="my-component">
        <h3 t-if="props.title"><t t-esc="props.title"/></h3>
        <ul>
            <li t-foreach="props.items" t-as="item" t-key="item.id">
                <t t-esc="item.name"/>
            </li>
        </ul>
    </div>
</template>

Translation (_t) Best Practices

CORRECT — Wrap at DEFINITION TIME

/** @odoo-module **/
import { _t } from "@web/core/l10n/translation";

// Static labels wrapped where defined
const MONTHS = [
    {value: 1, short: _t("Jan"), full: _t("January")},
    {value: 2, short: _t("Feb"), full: _t("February")},
    // ...
];

const STATUS_LABELS = {
    draft: _t("Draft"),
    pending: _t("Pending"),
    approved: _t("Approved"),
};

WRONG — Runtime wrappers DON'T WORK

// WRONG: Strings without _t() at definition
const MONTHS = [{value: 1, label: "Jan"}]; // NOT found by PO extractor!

// WRONG: Variable passed to _t() at runtime
translateLabel(key) {
    return _t(key);  // PO extractor can't find string literals
}

When to use _t()

Use _t() Don't use _t()
Static labels in JS arrays/objects Static text in XML templates (auto-translated)
Error messages in JS constants Dynamic variables passed at runtime
User-facing strings defined in JS Hardcoded strings in .xml files

Bootstrap 4 → 5 Migration (Odoo 14/15 → 16+)

Class Replacements

Bootstrap 4 Bootstrap 5
ml-* ms-* (margin-start)
mr-* me-* (margin-end)
pl-* ps-* (padding-start)
pr-* pe-* (padding-end)
text-left text-start
text-right text-end
float-left float-start
float-right float-end
form-group mb-3
custom-select form-select
close btn-close
badge-* bg-*
font-weight-bold fw-bold
sr-only visually-hidden
no-gutters g-0

Data Attributes

Bootstrap 4 Bootstrap 5
data-toggle data-bs-toggle
data-target data-bs-target
data-dismiss data-bs-dismiss

Removed Classes (find alternatives)

  • form-inline → Use grid/flex utilities
  • jumbotron → Recreate with utilities
  • media → Use d-flex with flex utilities

SCSS Bootstrap Overrides

File: static/src/scss/bootstrap_overridden.scss Bundle: web._assets_frontend_helpers

@import "~bootstrap/scss/functions";
@import "~bootstrap/scss/variables";

$spacer: 1rem !default;
$border-radius: 0.25rem !default;
$border-radius-lg: 0.5rem !default;
$box-shadow: 0 .5rem 1rem rgba(0, 0, 0, .15) !default;

Use !default flag on all overrides.


Version-Specific Notes

Odoo 17

  • Owl v1: template as separate property
  • Snippet registration: simple XPath
  • Import: @web/legacy/js/public/public_widget

Odoo 18-19

  • Owl v2: static template, props validation
  • Snippet groups required
  • Website builder: plugin architecture (Odoo 19)
  • Breaking: type='json'type='jsonrpc' in controllers
Weekly Installs
7
GitHub Stars
34
First Seen
13 days ago
Installed on
opencode7
deepagents7
antigravity7
claude-code7
github-copilot7
codex7