frontend-js
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseOdoo Frontend JavaScript Patterns
Odoo前端JavaScript模式
Critical Rules
核心规则
- Website themes: Use framework ONLY — NOT Owl or vanilla JS
publicWidget - JS modules: Start every file with
/** @odoo-module **/ - No inline JS/CSS: Always separate files in and
static/src/js/static/src/scss/ - Bootstrap: v5.1.3 for Odoo 16+ (never Tailwind)
- Translations: Use at DEFINITION TIME for static JS labels
_t()
- 网站主题:仅使用框架 —— 不要用Owl或原生JS
publicWidget - JS模块:每个文件都以开头
/** @odoo-module **/ - 禁止内联JS/CSS:始终将文件拆分到和
static/src/js/目录下static/src/scss/ - Bootstrap:Odoo 16+使用v5.1.3(禁止使用Tailwind)
- 翻译:定义静态JS标签时就使用包裹
_t()
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 ( → v17), manifest version field, or config file.
odoo17/| 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+ |
可通过路径( → v17)、manifest版本字段或配置文件检测版本。
odoo17/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
核心要点
- ALWAYS check before animations/interactions
this.editableMode - makes widgets work in website builder
disabledInEditableMode: false - ALWAYS clean up event listeners in
destroy() - NEVER use Owl or vanilla JS for website themes
- Use namespaced events () for easy cleanup
.myWidget
- 执行动效/交互前必须检查
this.editableMode - 可让组件在网站构建器中正常运行
disabledInEditableMode: false - 必须在中清理事件监听器
destroy() - 网站主题绝对不能使用Owl或原生JS
- 使用带命名空间的事件()便于清理
.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/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 |
| 需要使用_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 4 | Bootstrap 5 |
|---|---|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| Bootstrap 4 | Bootstrap 5 |
|---|---|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
Data Attributes
数据属性
| Bootstrap 4 | Bootstrap 5 |
|---|---|
| |
| |
| |
| Bootstrap 4 | Bootstrap 5 |
|---|---|
| |
| |
| |
Removed Classes (find alternatives)
已移除类(请寻找替代方案)
- → Use grid/flex utilities
form-inline - → Recreate with utilities
jumbotron - → Use
mediawith flex utilitiesd-flex
- → 使用栅格/flex工具类
form-inline - → 用工具类自行实现
jumbotron - → 搭配flex工具类使用
mediad-flex
SCSS Bootstrap Overrides
SCSS Bootstrap覆盖配置
File:
Bundle:
static/src/scss/bootstrap_overridden.scssweb._assets_frontend_helpersscss
@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 flag on all overrides.
!default文件路径:
资源包:
static/src/scss/bootstrap_overridden.scssweb._assets_frontend_helpersscss
@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;所有覆盖配置都要加上标识。
!defaultVersion-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'in controllerstype='jsonrpc'
- Owl v2:静态template、props校验
- 必须配置代码片段分组
- 网站构建器:插件架构(Odoo 19)
- 不兼容改动:控制器中→
type='json'type='jsonrpc'