frontend-js

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Odoo Frontend JavaScript Patterns

Odoo前端JavaScript模式

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
  1. 网站主题:仅使用
    publicWidget
    框架 —— 不要用Owl或原生JS
  2. JS模块:每个文件都以
    /** @odoo-module **/
    开头
  3. 禁止内联JS/CSS:始终将文件拆分到
    static/src/js/
    static/src/scss/
    目录下
  4. Bootstrap:Odoo 16+使用v5.1.3(禁止使用Tailwind)
  5. 翻译:定义静态JS标签时就使用
    _t()
    包裹

Version Detection

版本检测

OdooBootstrapOwlJavaScript
144.xES6+
154.xv1ES6+
165.1.3v1ES2020+
175.1.3v2ES2020+
18-195.1.3v2ES2020+
Detect from path (
odoo17/
→ v17), manifest version field, or config file.

Odoo版本Bootstrap版本Owl版本JavaScript版本
144.xES6+
154.xv1ES6+
165.1.3v1ES2020+
175.1.3v2ES2020+
18-195.1.3v2ES2020+
可通过路径(
odoo17/
→ v17)、manifest版本字段或配置文件检测版本。

publicWidget Pattern (REQUIRED for Themes)

publicWidget模式(主题必填)

Use for: Website interactions, theme functionality, animations, forms
javascript
/** @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;
适用场景:网站交互、主题功能、动效、表单
javascript
/** @odoo-module **/

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

publicWidget.registry.MyWidget = publicWidget.Widget.extend({
    selector: '.my-selector',
    disabledInEditableMode: false,  // 允许在网站构建器中运行

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

    /**
     * 核心注意点:检查editableMode保证和网站构建器兼容
     */
    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;
        // 处理逻辑
    },

    /**
     * 核心注意点:清理事件监听器避免内存泄漏
     */
    destroy: function () {
        $(window).off('.myWidget');  // 移除带命名空间的事件
        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
  1. 执行动效/交互前必须检查
    this.editableMode
  2. disabledInEditableMode: false
    可让组件在网站构建器中正常运行
  3. 必须在
    destroy()
    中清理
    事件监听器
  4. 网站主题绝对不能使用Owl或原生JS
  5. 使用带命名空间的事件
    .myWidget
    )便于清理

Include in Manifest

加入Manifest配置

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

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

Owl Component Pattern

Owl组件模式

Odoo 17 (Owl v1)

Odoo 17(Owl v1)

javascript
/** @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);
javascript
/** @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 18-19(Owl v2 —— 不兼容改动)

javascript
/** @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 });
    }
}
javascript
/** @odoo-module **/

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

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

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

XML Template

XML模板

xml
<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>

xml
<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

翻译(_t)最佳实践

CORRECT — Wrap at DEFINITION TIME

正确写法 —— 定义时就包裹

javascript
/** @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"),
};
javascript
/** @odoo-module **/
import { _t } from "@web/core/l10n/translation";

// 静态标签在定义处就包裹
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

错误写法 —— 运行时包裹无效

javascript
// 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
}
javascript
// 错误:定义时没有用_t()包裹字符串
const MONTHS = [{value: 1, label: "Jan"}]; // PO提取器无法识别!

// 错误:运行时才把变量传入_t()
translateLabel(key) {
    return _t(key);  // PO提取器无法识别字符串字面量
}

When to use _t()

_t()使用场景

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

需要使用_t()不要使用_t()
JS数组/对象中的静态标签XML模板中的静态文本(自动翻译)
JS常量中的错误提示信息运行时传入的动态变量
JS中定义的面向用户的字符串.xml文件中的硬编码字符串

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

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

Class Replacements

类名替换

Bootstrap 4Bootstrap 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
Bootstrap 4Bootstrap 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 4Bootstrap 5
data-toggle
data-bs-toggle
data-target
data-bs-target
data-dismiss
data-bs-dismiss
Bootstrap 4Bootstrap 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

  • form-inline
    → 使用栅格/flex工具类
  • jumbotron
    → 用工具类自行实现
  • media
    → 搭配flex工具类使用
    d-flex

SCSS Bootstrap Overrides

SCSS Bootstrap覆盖配置

File:
static/src/scss/bootstrap_overridden.scss
Bundle:
web._assets_frontend_helpers
scss
@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.

文件路径
static/src/scss/bootstrap_overridden.scss
资源包
web._assets_frontend_helpers
scss
@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;
所有覆盖配置都要加上
!default
标识。

Version-Specific Notes

版本特定注意事项

Odoo 17

Odoo 17

  • Owl v1: template as separate property
  • Snippet registration: simple XPath
  • Import:
    @web/legacy/js/public/public_widget
  • Owl v1:template作为独立属性定义
  • 代码片段注册:简单XPath即可
  • 导入路径:
    @web/legacy/js/public/public_widget

Odoo 18-19

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
  • Owl v2:静态template、props校验
  • 必须配置代码片段分组
  • 网站构建器:插件架构(Odoo 19)
  • 不兼容改动:控制器中
    type='json'
    type='jsonrpc'