Owl 组件¶
Odoo Javascript 框架使用一个名为 Owl 的自定义组件框架。它是一个声明式组件系统,灵感松散地来自 Vue 和 React。组件使用 QWeb 模板 定义,并辅以一些 Owl 特有的指令。官方 Owl 文档 包含完整的参考和教程。
重要
尽管代码可以在 web 模块中找到,但它维护于一个独立的 GitHub 仓库。因此,对 Owl 的任何修改都应通过 https://github.com/odoo/owl 上的拉取请求进行。
注解
目前,所有 Odoo 版本(从 14 版开始)都共用同一个 Owl 版本。
使用 Owl 组件¶
Owl documentation 已经详细记录了 Owl 框架,因此本页只提供 Odoo 特有的信息。但首先,让我们看看如何在 Odoo 中创建一个简单的组件。
import { Component, xml, useState } from "@odoo/owl";
class MyComponent extends Component {
static template = xml`
<div t-on-click="increment">
<t t-esc="state.value">
</div>
`;
setup() {
this.state = useState({ value: 1 });
}
increment() {
this.state.value++;
}
}
此示例表明 Owl 作为库存在于全局命名空间的 owl 中:它可以像 Odoo 中的大多数库一样直接使用。请注意,我们在这里把模板定义为静态属性,但没有使用 static 关键字,因为该关键字在某些浏览器中不可用(Odoo 的 javascript 代码应遵循 Ecmascript 2019 规范)。
这里我们借助 xml 辅助函数在 javascript 代码中定义模板。不过,这只适合入门时使用。在实践中,Odoo 中的模板应定义在 xml 文件中,以便能够翻译。在这种情况下,组件只应定义模板名称。
在实践中,大多数组件应定义 2 或 3 个文件,且位于同一目录:一个 javascript 文件(my_component.js)、一个模板文件(my_component.xml),以及可选的 scss(或 css)文件(my_component.scss)。然后应将这些文件添加到某个资源包中。Web 框架会负责加载 javascript/css 文件,并把模板加载到 Owl 中。
上面这个组件的定义方式如下:
import { Component, useState } from "@odoo/owl";
class MyComponent extends Component {
static template = 'myaddon.MyComponent';
...
}
而模板现在位于对应的 xml 文件中:
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="myaddon.MyComponent">
<div t-on-click="increment">
<t t-esc="state.value"/>
</div>
</t>
</templates>
注解
模板名称应遵循 addon_name.ComponentName 的约定。
更多内容
最佳实践¶
首先,组件是类,因此它们有构造函数。但构造函数在 javascript 中是特殊方法,无法以任何方式重写。由于这在 Odoo 中是一个偶尔有用的模式,我们需要确保 Odoo 中没有任何组件直接使用构造函数方法。相反,组件应使用 setup 方法:
// correct:
class MyComponent extends Component {
setup() {
// initialize component here
}
}
// incorrect. Do not do that!
class IncorrectComponent extends Component {
constructor(parent, props) {
// initialize component here
}
}
另一项良好实践是为模板名称使用一致的约定:addon_name.ComponentName。这样可以避免 Odoo 插件模块之间的名称冲突。
参考列表¶
Odoo Web 客户端由 Owl 组件构建。为便于使用,Odoo javascript 框架提供了一套通用组件,可在下拉菜单、复选框或日期选择器这类常见场景中复用。本页介绍如何使用这些通用组件。
技术名称 |
简短说明 |
|---|---|
用于在触摸滑动时执行动作的滑动组件 |
|
一个旁边带标签的简单复选框组件 |
|
可供选择的颜色列表 |
|
功能完整的下拉菜单 |
|
使用选项卡在页面之间导航的组件 |
|
用于处理分页的小型组件 |
|
用于在不同选项之间进行选择的下拉菜单组件 |
|
以圆角胶囊形式显示的标签列表 |
ActionSwiper¶
库位¶
@web/core/action_swiper/action_swiper
说明¶
这是一个在元素被水平滑动时执行动作的组件。滑动器会包裹目标元素,为其添加动作。当用户松开滑动器时,如果滑动距离超过其宽度的一定比例,动作就会被执行。
<ActionSwiper onLeftSwipe="Object" onRightSwipe="Object">
<SomeElement/>
</ActionSwiper>
使用该组件最简单的方式是像上面所示那样,直接在 xml 模板中把它包裹在目标元素周围。但有时你可能想扩展已有元素,又不希望复制模板。这完全可以做到。
如果你想扩展已有元素的行为,必须直接包裹该元素,把它放在里面。此外,你还可以按条件添加 props,以控制元素何时可滑动、其动画效果以及执行动作所需的最小滑动比例。
你可以使用该组件轻松地与记录、消息、列表中的项目等进行交互。
下面的示例创建了一个基本的 ActionSwiper 组件。此处两个方向的滑动都已启用。
<ActionSwiper
onRightSwipe="
{
action: '() => Delete item',
icon: 'fa-delete',
bgColor: 'bg-danger',
}"
onLeftSwipe="
{
action: '() => Star item',
icon: 'fa-star',
bgColor: 'bg-warning',
}"
>
<div>
Swipable item
</div>
</ActionSwiper>
注解
使用从右到左(RTL)的语言时,动作顺序会对调。
属性(Props)¶
名称 |
类型 |
说明 |
|---|---|---|
|
|
可选布尔值,决定滑动过程中是否有平移效果 |
|
|
滑动结束后使用的可选动画( |
|
|
若存在,则可将滑动器向左滑动 |
|
|
若存在,则可将滑动器向右滑动 |
|
|
执行动作所需滑动的最小宽度比例(可选) |
你可以同时使用 onLeftSwipe 和 onRightSwipe 两个 props。
用于左/右滑动的 Object 必须包含以下内容:
action,即作为回调的可调用Function。一旦在给定方向上完成滑动,就会执行该动作。
icon是要使用的图标类,通常用于表示该动作。它必须是string。
bgColor是背景颜色,用于装饰该动作。它可以是以下 bootstrap 情景颜色 之一(danger、info、secondary、success或warning)。必须提供这些值,以定义滑动器的行为和视觉外观。
示例:扩展现有组件¶
在下面的示例中,你可以使用 xpath 把已有元素包裹在 ActionSwiper 组件中。此处添加了一个滑动器,用于在邮件中把消息标记为已读。
<xpath expr="//*[hasclass('o_Message')]" position="after">
<ActionSwiper
onRightSwipe="messaging.device.isMobile and messageView.message.isNeedaction ?
{
action: () => messageView.message.markAsRead(),
icon: 'fa-check-circle',
bgColor: 'bg-success',
} : undefined"
/>
</xpath>
<xpath expr="//ActionSwiper" position="inside">
<xpath expr="//*[hasclass('o_Message')]" position="move"/>
</xpath>
CheckBox¶
库位¶
@web/core/checkbox/checkbox
说明¶
这是一个带标签的简单复选框组件。复选框与标签相关联:只要点击标签,复选框就会切换状态。
<CheckBox value="boolean" disabled="boolean" t-on-change="onValueChange">
Some Text
</CheckBox>
属性(Props)¶
名称 |
类型 |
说明 |
|---|---|---|
|
|
若为 true,则复选框处于勾选状态,否则为未勾选 |
|
|
若为 true,则复选框被禁用,否则为启用 |
ColorList¶
库位¶
@web/core/colorlist/colorlist
说明¶
ColorList 让你从预定义列表中选择一种颜色。默认情况下,该组件显示当前选中的颜色,并且在存在 canToggle props 之前不可展开。不同的 props 可以改变其行为:始终展开列表,或使其在被点击后充当切换器,显示可用颜色列表直到选中某个颜色。
属性(Props)¶
名称 |
类型 |
说明 |
|---|---|---|
|
|
可选。点击时 colorlist 是否可以展开列表 |
|
|
组件中要显示的颜色列表。每种颜色都有一个唯一的 |
|
|
可选。如果为 true,则列表始终处于展开状态 |
|
|
可选。如果为 true,则列表默认处于展开状态 |
|
|
选择颜色后执行的回调 |
|
|
可选。所选颜色的 |
颜色 id 如下:
编号 |
颜色 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Dropdown¶
库位¶
@web/core/dropdown/dropdown 与 @web/core/dropdown/dropdown_item
说明¶
Dropdown 允许你在点击切换按钮时显示一个包含项目列表的菜单。它们可以与 DropdownItem 组合使用,以便在选中项目时调用回调并关闭菜单。
Dropdown 是出奇复杂的组件,它们提供的功能列表如下:
点击时切换项目列表
点击外部时关闭
选中项目时调用函数
可选:选中项目时关闭项目列表
SIY:自行设置样式
支持子下拉菜单,层级不限
可配置的快捷键,用于打开或关闭下拉菜单,或选择下拉菜单项
键盘导航(方向键、tab、shift+tab、home、end、enter 和 escape)
页面滚动或调整大小时自动重新定位
智能选择打开方向(自动处理从右到左的方向)。
同级下拉菜单:当其中一个打开时,悬停即可切换其他下拉菜单
要正确使用 <Dropdown/> 组件,需要填充两个 OWL slots:
default槽位:其中包含下拉菜单的 切换按钮 元素。默认情况下,点击事件会附加到该元素上,以打开和关闭下拉菜单。content槽位:其中包含下拉菜单本身的 元素,并在弹出层中渲染。虽然这不是必需的,但你可以在该槽位中放置一些DropdownItem,当这些项目被选中时,下拉菜单会自动关闭。
<Dropdown>
<!-- The content of the "default" slot is the component's toggle -->
<button class="my-btn" type="button">
Click me to toggle the dropdown menu!
</button>
<!-- The "content" slot is rendered inside the menu that pops up next to the toggle -->
<t t-set-slot="content">
<DropdownItem onSelected="selectItem1">Menu Item 1</DropdownItem>
<DropdownItem onSelected="selectItem2">Menu Item 2</DropdownItem>
</t>
</Dropdown>
Dropdown 属性¶
名称 |
类型 |
说明 |
|---|---|---|
|
|
可选,添加到下拉菜单上的类名 |
|
|
可选。如果为 true,则禁用下拉菜单,用户将无法再打开它。(默认值: |
|
|
可选的项目列表,将作为 DropdownItem 显示在下拉菜单中 |
|
|
可选,用于定义期望的菜单打开位置。会自动应用 RTL 方向。应为有效的 usePosition hook 位置。(默认值: |
|
|
可选,在打开之前调用的函数。可以是异步的。 |
|
|
可选,在打开之后调用的函数。 |
|
|
可选,在打开或关闭之后调用的函数(以单个布尔值作为参数,表示下拉菜单是否已打开)。 |
|
|
可选的对象,包含 |
|
|
可选。为 true 时,Dropdown 组件不会向切换按钮添加点击事件监听器。这样可以更好地控制何时打开下拉菜单。(应与 |
|
|
可选,覆盖下拉菜单的导航选项(参见 |
|
|
可选。如果为 true,则在鼠标悬停时保持 Dropdown 菜单的位置不变,从而在菜单内容变化时提供更好的用户体验。 |
|
|
可选,允许获取下拉菜单的 ref(期望传入 |
DropdownItem 属性¶
名称 |
类型 |
说明 |
|---|---|---|
|
|
可选,添加到根 span 的类名中的值(同时支持字符串和 OWL classname object notation)。 |
|
|
可选,在下拉菜单项被选中时调用的函数。 |
|
|
可选,控制选中项目时应关闭哪个父级下拉菜单: |
|
|
可选,表示添加到根元素的属性的对象。 |
重要
为组件编写自定义 css 时,不要忘记菜单元素并不在切换按钮旁边,而是位于文档底部的覆盖层容器中。因此,请使用 menuClass 和 class 属性,以便更轻松地编写选择器。(这种 DOM 技巧让我们避免了许多 z-index 问题。)
嵌套 Dropdown¶
Dropdown 可以嵌套,只需将新的 Dropdown 组件放入其他下拉菜单的 content 槽位中即可。当父级下拉菜单打开时,子下拉菜单会在悬停时自动打开。
默认情况下,选中一个 DropdownItem 会关闭整个 Dropdown 树。
示例
此示例展示了如何构建一个嵌套的 File 下拉菜单,并为 New 子元素提供子菜单。
<Dropdown>
<button>File</button>
<t t-set-slot="content">
<DropdownItem onSelected="() => this.onItemSelected('file-save')">Save</DropdownItem>
<DropdownItem onSelected="() => this.onItemSelected('file-open')">Open</DropdownItem>
<Dropdown>
<button>New</button>
<t t-set-slot="content">
<DropdownItem onSelected="() => this.onItemSelected('file-new-document')">Document</DropdownItem>
<DropdownItem onSelected="() => this.onItemSelected('file-new-spreadsheet')">Spreadsheet</DropdownItem>
</t>
</Dropdown>
</t>
</Dropdown>
在下面的示例中,我们递归调用模板来显示树状结构。
<t t-name="addon.MainTemplate">
<div>
<t t-call="addon.RecursiveDropdown">
<t t-set="name" t-value="'Main Menu'" />
<t t-set="items" t-value="state.menuItems" />
</t>
</div>
</t>
<t t-name="addon.RecursiveDropdown">
<Dropdown>
<button t-esc="name"></button>
<t t-set-slot="content">
<t t-foreach="items" t-as="item" t-key="item.id">
<!-- If this item has no child: make it a <DropdownItem/> -->
<DropdownItem t-if="!item.childrenTree.length" onSelected="() => this.onItemSelected(item)" t-esc="item.name"/>
<!-- Else: recursively call the current dropdown template. -->
<t t-else="" t-call="addon.RecursiveDropdown">
<t t-set="name" t-value="item.name" />
<t t-set="items" t-value="item.childrenTree" />
</t>
</t>
</t>
</Dropdown>
</t>
受控 Dropdown¶
如果需要,你也可以通过代码打开或关闭下拉菜单。为此,必须将 useDropdownState hook 与 state 属性配合使用。useDropdownState 返回一个对象,它具有 open 和 close 方法(以及 isOpen getter)。将该对象传给要控制的下拉菜单的 state 属性,然后调用相应函数即可打开和关闭下拉菜单。
如果不想在切换按钮上添加默认的点击处理程序,也可以将 manual 设为 true。
示例
下面的示例展示了一个挂载时自动打开的下拉菜单,并且在点击内部按钮时只有 50% 的概率会关闭。
import { Component, onMounted } from "@odoo/owl";
import { Dropdown } from "@web/core/dropdown/dropdown";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
import { useDropdownState } from "@web/core/dropdown/dropdown_hooks";
class MyComponent extends Component {
static components = { Dropdown, DropdownItem };
static template = xml`
<Dropdown state="this.dropdown">
<div>My Dropdown</div>
<t t-set-slot="content">
<button t-on-click="() => this.mightClose()">Close It!<button>
</t>
</Dropdown>
`;
setup() {
this.dropdown = useDropdownState();
onMounted(() => {
this.dropdown.open();
});
}
mightClose() {
if (Math.random() > 0.5) {
this.dropdown.close();
}
}
}
DropdownGroup¶
位置: @web/core/dropdown/dropdown_group
你可以使用 DropdownGroup 组件让多个 Dropdown 共享同一个组,这意味着当其中一个 Dropdown 打开时,其他 Dropdown 会在鼠标悬停时自动打开,无需点击。
为此,可以用单个 DropdownGroup 包裹所有 Dropdown,也可以用具有相同 group 键的多个 DropdownGroup 分别包裹它们。
示例
在下面的示例中,该代码片段中的所有 dropdown 都将共享同一个组:
<DropdownGroup>
<Dropdown>...</Dropdown>
<Dropdown>...</Dropdown>
<Dropdown>...</Dropdown>
</DropdownGroup>
而在下面的代码片段中,只有第一个、第二个和第四个 dropdown 共享同一个组:
<DropdownGroup group="'my-group'">
<Dropdown>...</Dropdown>
<Dropdown>...</Dropdown>
</DropdownGroup>
<DropdownGroup group="'my-other-group'">
<Dropdown>...</Dropdown>
</DropdownGroup>
<DropdownGroup group="'my-group'">
<Dropdown>...</Dropdown>
</DropdownGroup>
Notebook¶
库位¶
@web/core/notebook/notebook
说明¶
Notebook 用于在选项卡界面中显示多个页面。选项卡可以位于元素的顶部以水平显示,也可以位于左侧以垂直布局。
有两种方式可以定义要实例化的 Notebook 页面:使用 slot,或者传入专用的 props。
可以使用 isDisabled 属性禁用页面,该属性可直接设置在 slot 节点上;如果 Notebook 是通过作为 props 传入的 pages 使用的,也可以在页面声明中设置。一旦被禁用,相应的选项卡将变灰并同时设为非活动状态。
属性(Props)¶
名称 |
类型 |
说明 |
|---|---|---|
|
|
可选。允许锚点导航到选项卡内不可见的元素。 |
|
|
可选。设置在组件根元素上的类名。 |
|
|
可选。默认显示的页面 |
|
|
可选。选项卡中使用的图标列表。 |
|
|
可选。选项卡方向是 |
|
|
可选。页面更改后执行的回调。 |
|
|
可选。包含要由模板填充的 |
示例
第一种方法是在组件的槽位中设置页面。
<Notebook orientation="'vertical'"> <t t-set-slot="page_1" title="'Page 1'" isVisible="true"> <h1>My First Page</h1> <p>It's time to build Owl components. Did you read the documentation?</p> </t> <t t-set-slot="page_2" title="'2nd page'" isVisible="true"> <p>Wise owl's silent flight. Through the moonlit forest deep, guides my path to code</p> </t> </Notebook>另一种定义页面的方式是传入 props。如果多个页面共享相同的结构,这种方式会很有用。请先为可能使用的每个页面模板创建一个组件。
import { Component, xml } from "@odoo/owl"; import { Notebook } from "@web/core/notebook/notebook"; class MyTemplateComponent extends Component { static template = xml` <h1 t-esc="props.title" /> <p t-esc="props.text" /> `; } class MyComponent extends Component { static template = xml` <Notebook defaultPage="'page_2'" pages="pages" /> `; get pages() { return [ { Component: MyTemplateComponent, title: "Page 1", props: { title: "My First Page", text: "This page is not visible", }, }, { Component: MyTemplateComponent, id: "page_2", title: "Page 2", props: { title: "My second page", text: "You're at the right place!", }, }, ] } }
此处展示了这两个示例:
Pager¶
库位¶
@web/core/pager/pager
说明¶
Pager 是一个处理分页的小型组件。一页由 offset 和 limit 定义,其中 limit 即页的大小。它显示当前页和元素的总数 total,例如“9-12 / 20”。在上面的示例中,offset 为 8,limit 为 4,total 为 20。它带有两个按钮(“Previous”和“Next”),用于在页之间导航。
注解
Pager 可用于任何地方,但其主要用途是在控制面板中。要操作控制面板的分页器,请参阅 usePager 钩子。
<Pager offset="0" limit="80" total="50" onUpdate="doSomething" />
属性(Props)¶
名称 |
类型 |
说明 |
|---|---|---|
|
|
页中第一个元素的索引。它从 0 开始,但分页器显示的是 |
|
|
页的大小。 |
|
|
页可以到达的元素总数。 |
|
|
当分页器修改页码时调用的函数。该函数可以是异步的,在此函数执行期间无法编辑分页器。 |
|
|
允许点击当前页码来编辑它(默认为 |
|
|
在上一页按钮上绑定访问键 |
库位¶
@web/core/select_menu/select_menu
说明¶
当你想做的比使用原生 select 元素更多时,可以使用此组件。你可以定义自己的选项模板,从而在选项之间进行搜索,或将它们分组到子区段中。
注解
请优先使用原生 HTML select 元素,因为它默认提供无障碍功能,并且在移动设备上具有更好的用户界面。此组件是为更复杂的用例而设计的,用于克服原生元素的限制。
属性(Props)¶
名称 |
类型 |
说明 |
|---|---|---|
|
|
可选。要在下拉列表中显示的 |
|
|
可选。设置在 SelectMenu 组件根节点上的类名。 |
|
|
可选。 |
|
|
可选。启用多重选择。启用多重选择后,所选值会以 标签 的形式显示在 SelectMenu 输入框中。 |
|
|
可选。设置在切换按钮上的类名。 |
|
|
可选。所选值是否可以被取消选择。 |
|
|
可选。下拉列表中是否显示搜索框。 |
|
|
可选。作为搜索框占位符显示的文本。 |
|
|
可选。当前选中的值。它可以是任意类型。 |
|
|
可选。选择某个选项时执行的回调。 |
choice 的结构如下:
value是该选项的实际值。它通常是技术性字符串,但也可以是any类型。
label是与该选项关联的显示文本。它通常是更友好且已翻译的string。
group 的结构如下:
choices是要为该组显示的choice列表。
label是与该组关联的显示文本。它是显示在组顶部的string。
示例
在下面的示例中,SelectMenu 将显示四个选项。其中一个显示在所有选项的顶部,因为它未关联任何组,而其余选项则由各自组的标签分隔。
import { Component, xml } from "@odoo/owl";
import { SelectMenu } from "@web/core/select_menu/select_menu";
class MyComponent extends Component {
static template = xml`
<SelectMenu
choices="choices"
groups="groups"
value="'value_2'"
/>
`;
get choices() {
return [
{
value: "value_1",
label: "First value"
}
]
}
get groups() {
return [
{
label: "Group A",
choices: [
{
value: "value_2",
label: "Second value"
},
{
value: "value_3",
label: "Third value"
}
]
},
{
label: "Group B",
choices: [
{
value: "value_4",
label: "Fourth value"
}
]
}
]
}
}
你还可以自定义切换器的外观,并使用合适的组件 slot 为选项设置自定义模板。
<SelectMenu
choices="choices"
groups="groups"
value="'value_2'"
>
Make a choice!
<t t-set-slot="choice" t-slot-scope="choice">
<span class="coolClass" t-esc="'👉 ' + choice.data.label + ' 👈'" />
</t>
</SelectMenu>
当 SelectMenu 用于多重选择时,value 属性必须是包含所选选项值的 Array。
对于更高级的用例,你可以使用 bottomArea 插槽自定义下拉列表的底部区域。这里,我们选择显示一个按钮,该按钮携带搜索输入框中设置的值。
<SelectMenu
choices="choices"
>
<span class="select_menu_test">Select something</span>
<t t-set-slot="bottomArea" t-slot-scope="select">
<div t-if="select.data.searchValue">
<button class="btn text-primary" t-on-click="() => this.onCreate(select.data.searchValue)">
Create this article "<i t-esc="select.data.searchValue" />"
</button>
</div>
</t>
</SelectMenu>
库位¶
@web/core/tags_list/tags_list
说明¶
此组件可以以圆角胶囊的形式显示标签列表。这些标签既可以只是列出几个值,也可以是可编辑的,从而允许移除条目。可以使用 itemsVisible 属性限制显示条目的数量。如果列表长度超过该限制,则额外条目的数量会显示在最后一个标签旁边的圆圈中。
属性(Props)¶
名称 |
类型 |
说明 |
|---|---|---|
|
|
可选。标签是否以徽章形式显示。 |
|
|
可选。标签是否带文本显示。 |
|
|
可选。列表中可见标签的数量上限。 |
|
|
传给该组件的 |
tag 的结构如下:
colorIndex是可选的配色 ID。
icon是可选图标,显示在显示文本的正前方。
id是该标签的唯一标识符。
img是可选图片,以圆形显示在显示文本的正前方。
onClick是可以传递给该元素的可选回调。它允许父元素根据被点击的标签来处理相应功能。
onDelete是可以传递给该元素的可选回调。它使从标签列表中移除条目成为可能,并且必须由父元素处理。
text是与该标签关联的显示string。
示例
在下一个示例中,使用 TagsList 组件显示多个标签。当标签被按下或删除按钮被点击时会发生什么,需由开发者在父组件中处理。
import { Component, xml } from "@odoo/owl";
import { TagsList } from "@web/core/tags_list/tags_list";
class Parent extends Component {
static template = xml`<TagsList tags="tags" />`;
static components = { TagsList };
setup() {
this.tags = [{
id: "tag1",
text: "Earth"
}, {
colorIndex: 1,
id: "tag2",
text: "Wind",
onDelete: () => {...}
}, {
colorIndex: 2,
id: "tag3",
text: "Fire",
onClick: () => {...},
onDelete: () => {...}
}];
}
}
根据赋予每个标签的属性,其外观和行为会有所不同。