Owl 组件

Odoo JavaScript 框架使用一个名为 Owl 的自定义组件框架。它是一个声明式的组件系统,受 Vue 和 React 的启发。组件通过 QWeb 模板 定义,并添加了一些 Owl 特有的指令进行增强。官方 Owl 文档 包含了完整的参考文档和教程。

重要

尽管代码可以在 web 模块中找到,但它是从一个单独的 GitHub 仓库进行维护的。因此,对 Owl 的任何修改都应通过 https://github.com/odoo/owl 上的拉取请求进行。

注解

目前,所有 Odoo 版本(从版本 14 开始)都使用相同的 Owl 版本。

使用 Owl 组件

Owl 文档 已经详细介绍了 Owl 框架,因此本页面将仅提供与 Odoo 相关的信息。但首先,让我们看看如何在 Odoo 中创建一个简单的组件。

const { useState } = owl.hooks;
const { xml } = owl.tags;

class MyComponent extends Component {
    setup() {
        this.state = useState({ value: 1 });
    }

    increment() {
        this.state.value++;
    }
}
MyComponent.template = xml
    `<div t-on-click="increment">
        <t t-esc="state.value">
    </div>`;

此示例表明,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 中。

以下是该组件的定义方式:

const { useState } = owl.hooks;

class MyComponent extends Component {
    ...
}
MyComponent.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 网页客户端是使用 Owl 组件构建的。为了便于使用,Odoo JavaScript 框架提供了一套通用组件,可以在一些常见场景中重复使用,例如下拉框、复选框或日期选择器。本页面将解释如何使用这些通用组件。

技术名称

简要描述

ActionSwiper

一个用于在触摸滑动时执行操作的滑动组件

复选框

一个带有旁边标签的简单复选框组件

ColorList

可选择的颜色列表

下拉菜单

全功能下拉菜单

笔记本

一个用于通过选项卡在页面之间导航的组件

分页器

一个用于处理分页的小型组件

SelectMenu

一个用于在不同选项之间进行选择的下拉组件

标签列表

显示为圆角药丸样式的标签列表

动作滑块

库位

@web/core/action_swiper/action_swiper

描述

这是一个可在元素水平滑动时执行动作的组件。滑动器会包裹目标元素以向其添加动作。当用户释放滑动器并使其滑过其宽度的一部分时,将执行该动作。

<ActionSwiper onLeftSwipe="Object" onRightSwipe="Object">
  <SomeElement/>
</ActionSwiper>

使用组件最简单的方式是如上所示,在 XML 模板中直接将目标元素包裹在组件中。但有时,您可能希望扩展一个现有的元素,并且不希望重复模板。这是完全可以实现的。

如果您想扩展现有元素的行为,必须将该元素直接包裹在内部。此外,您还可以根据需要条件性地添加属性,以管理该元素是否可滑动、其动画效果以及执行操作所需的最小滑动比例。

你可以使用组件轻松地与记录、消息、列表中的条目等进行交互。

ActionSwiper 使用示例

以下示例创建了一个基本的 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)语言时,操作会进行反转。

属性

名称

类型

描述

移动时的动画

布尔值

可选布尔值,用于确定滑动时是否存在翻译效果

动画类型

字符串

可选的动画,在滑动结束后使用(bounceforwards

左滑动

对象

如果存在,可以通过向左滑动来触发操作切换器

向右滑动时触发

对象

如果存在,可以通过向右滑动来操作 actionswiper

滑动距离比例

数字

可选的最小宽度比例,必须滑动该比例以执行动作

你可以同时使用 onLeftSwipeonRightSwipe 属性。

用于左/右滑动的 Object 必须包含:

  • action,即作为回调的可调用 Function。一旦在给定方向上完成滑动,就会执行该动作。

  • icon 是要使用的图标类,通常用于表示动作。它必须是 字符串

  • bgColor 是背景颜色,用于装饰动作。可以是以下 Bootstrap 上下文颜色 中的一种(dangerinfosecondarysuccesswarning)。

这些值必须提供以定义滑动器的行为和视觉外观。

示例:扩展现有组件

在下面的示例中,你可以使用 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>

复选框

库位

@web/core/checkbox/checkbox

描述

这是一个带有旁边标签的简单复选框组件。复选框与标签相关联:当点击标签时,复选框会切换状态。

<CheckBox value="boolean" disabled="boolean" t-on-change="onValueChange">
  Some Text
</CheckBox>

属性

名称

类型

描述

布尔值

如果为 true,则复选框被选中,否则未被选中

禁用

布尔值

如果为 true,则复选框被禁用,否则为启用。

颜色列表

库位

@web/core/colorlist/colorlist

描述

颜色列表允许您从预定义的颜色列表中进行选择。默认情况下,该组件会显示当前选中的颜色,并且在 canToggle 属性存在之前,该组件是不可展开的。不同的属性可以改变其行为,例如始终展开列表,或在点击后使其作为切换器使用,以显示可用颜色列表,直到选择一个颜色为止。

属性

名称

类型

描述

可切换

布尔值

可选。是否可以在点击时扩展颜色列表

颜色

数组

组件中要显示的颜色列表。每个颜色都有一个唯一的 id

强制展开

布尔值

可选。如果为 true,则列表始终展开

已展开

布尔值

可选。如果为真,列表默认展开

onColorSelected

函数

选择一种颜色后执行的回调函数

选定的颜色

编号

可选。所选颜色的 id

颜色 id 的如下:

ID

颜色

0

无颜色

1

红色

2

橙色

3

黄色

4

浅蓝色

5

深紫色

6

“鲑鱼粉红色”

7

媒介 蓝色

8

深蓝色

9

福石榴色

12

绿色

11

紫色

库位

@web/core/dropdown/dropdown@web/core/dropdown/dropdown_item

描述

下拉菜单在点击切换按钮时会显示一个包含多个项目的菜单。它们可以与下拉项(DropdownItems)结合使用,以便在选择项目时调用回调函数并关闭菜单。

下拉菜单是出人意料的复杂组件,它们提供的功能列表如下:

  • 点击以切换项目列表

  • 点击外部区域关闭

  • 当选择项目时调用函数

  • 可选地,在选择一项时关闭项目列表

  • SIY:自行设计样式

  • 支持子下拉菜单,层级不限

  • 可配置的快捷键,用于打开/关闭下拉菜单或选择下拉项

  • 键盘导航(箭头键、Tab、Shift+Tab、Home、End、Enter 和 Escape)

  • 在页面滚动或调整大小时重新定位自身

  • 智能选择其应打开的方向(右到左方向会自动处理)。

  • 直接兄弟下拉菜单:当一个打开时,在悬停时切换其他下拉菜单

要正确使用 <Dropdown/> 组件,你需要填充两个 OWL 插槽 :

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

嵌套下拉菜单

下拉菜单可以嵌套,只需将新的下拉菜单组件放在其他下拉菜单的内容插槽中即可。当父级下拉菜单打开时,子级下拉菜单会在悬停时自动打开。

默认情况下,选择一个 DropdownItem 会关闭整个 Dropdown 树。

Example

此示例展示了如何创建一个嵌套的文件下拉菜单,包含用于新建子元素的子菜单。

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

受控下拉框

如果需要,你也可以通过代码打开或关闭下拉菜单。要做到这一点,你必须使用 useDropdownState 钩子以及 state 属性。useDropdownState 返回一个包含 openclose 方法的对象(以及一个 isOpen 获取器)。将该对象传递给你要控制的下拉菜单的 state 属性,然后调用相应的方法应该就可以打开和关闭你的下拉菜单了。

你可以将 manual 设置为 true,如果你不希望默认的点击处理程序被添加到切换按钮上。

Example

以下示例显示了一个在挂载时会自动打开的下拉菜单,并且当点击内部按钮时,只有 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();
    }
  }
}

笔记本

库位

@web/core/notebook/notebook

描述

笔记本用于以选项卡界面显示多个页面。选项卡可以位于元素顶部以水平方式显示,或位于左侧以实现垂直布局。

有两种方式可以定义你的 Notebook 页面以进行实例化,可以通过使用 slot,或者通过传递专用的 props

一个页面可以通过 isDisabled 属性禁用,该属性可以直接设置在插槽节点上,或者在使用 Notebook 时,如果将 pages 作为 props 传递,则可以在页面声明中设置。一旦被禁用,对应的标签页将变灰,并且也会被设置为未激活状态。

属性

名称

类型

描述

锚点

对象

可选。允许导航到当前不可见标签页内的元素。

类名

字符串

可选。组件根元素上的类名。

默认页面

字符串

可选。默认显示的页面 id

图标

数组

可选。选项卡中使用的图标列表。

方向

字符串

可选。选项卡的方向是 `horizontal`(水平)还是 `vertical`(垂直)。

` onPageUpdate `

函数

可选。页面更改后执行的回调函数。

页面

数组

可选。包含从模板中填充的 page 列表。

Example

第一种方法是将页面设置在组件的插槽中。

<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 { Notebook } from "@web/core/notebook/notebook";

class MyTemplateComponent extends owl.Component {
  static template = owl.tags.xml`
    <h1 t-esc="props.title" />
    <p t-esc="props.text" />
  `;
}

class MyComponent extends owl.Component {
  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!",
        },
      },
    ]
  }
}
MyComponent.template = owl.tags.xml`
  <Notebook defaultPage="'page_2'" pages="pages" />
`;

此处展示了两个示例:

垂直和水平布局示例

分页器

库位

@web/core/pager/pager

描述

分页器是一个用于处理分页的小型组件。一页由一个 offset`(偏移量)和一个 `limit`(每页大小)定义。它会显示当前页码和 `total`(总元素数),例如,“9-12 / 20”。在前面的例子中,`offset 是 8,limit 是 4,total 是 20。它包含两个按钮(“上一页”和“下一页”),用于在不同页面之间导航。

注解

分页器可以在任何地方使用,但其主要用途是在控制面板中。有关操作控制面板分页器的信息,请参阅 usePager 钩子。

<Pager offset="0" limit="80" total="50" onUpdate="doSomething" />

属性

名称

类型

描述

偏移量

编号

当前页面第一个元素的索引。它从 0 开始,但分页器显示 offset + 1

限制

编号

页面的大小。offsetlimit 的和对应于页面中最后一个元素的索引。

总计

编号

该页面可以访问的元素总数。

onUpdate

函数

当分页器修改页面时调用的函数。此函数可以是异步的,在此函数执行期间,分页器无法被编辑。

可编辑

布尔值

允许单击当前页面以进行编辑(默认为 true)。

使用访问密钥

布尔值

绑定上一页按钮的访问键 p 和下一页按钮的访问键 n`(默认为 `true)。

选择菜单

库位

@web/core/select_menu/select_menu

描述

当您需要比使用原生的 select 元素更复杂的功能时,可以使用此组件。您可以自定义选项模板,从而在选项中进行搜索,或将其分组为子部分。

注解

优先使用原生的 HTML <select> 元素,因为它默认提供了可访问性功能,并且在移动设备上具有更好的用户界面。此组件旨在用于更复杂的使用场景,以克服原生元素的限制。

属性

名称

类型

描述

选项

数组

可选。在下拉菜单中显示的 choice 列表。

字符串

可选。设置在 SelectMenu 组件根元素上的类名。

数组

可选。包含 group 的列表,用于在下拉菜单中显示 choices

多选

布尔值

可选。启用多选功能。当启用多选时,所选值会以 标签 的形式显示在 SelectMenu 输入框中。

togglerClass

字符串

可选。切换按钮的类名。

必填

布尔值

可选。指定所选值是否可以取消选择。

可搜索的

布尔值

可选。下拉菜单中是否显示搜索框。

搜索占位符

字符串

可选。作为搜索框占位符显示的文本。

任意

可选。当前选中的值。它可以是任何类型的值。

onSelect

函数

可选。当选择一个选项时执行的回调函数。

choice 的形状如下:

  • value 是选项的实际值。它通常是一个技术字符串,但也可以是 any 类型。

  • label 是与选项相关联的显示文本。通常,这个值是一个更友好且已翻译的 string

一个 group 的结构如下:

  • choices 是此用户组要显示的 choice 列表。

  • label 是与用户组关联的显示文本。这是一个显示在用户组顶部的 字符串

Example

在下面的例子中,SelectMenu 将显示四个选项。其中有一个选项显示在选项上方,因为它没有关联任何用户组,而其他选项则根据其所属用户组的标签进行分组显示。

import { SelectMenu } from "@web/core/select_menu/select_menu";

class MyComponent extends owl.Component {
  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"
              }
          ]
      }
    ]
  }
}
MyComponent.template = owl.tags.xml`
  <SelectMenu
    choices="choices"
    groups="groups"
    value="'value_2'"
  />
`;

你可以通过使用相应的组件 slot 自定义切换器的外观,并为选项设置自定义模板。

MyComponent.template = owl.tags.xml`
  <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 使用和自定义示例

当 SelectMenu 用于多选时,value 属性必须是一个包含所选选项值的 Array

使用多选的 SelectMenu 示例

对于更高级的用例,您可以自定义下拉菜单的底部区域,使用 bottomArea 插槽。在这里,我们选择显示一个按钮,其对应的值设置在搜索输入中。

MyComponent.template = owl.tags.xml`
  <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>
`;
SelectMenu 底部区域自定义示例

标签列表

库位

@web/core/tags_list/tags_list

描述

此组件可以以圆角药丸的形式显示一组标签。这些标签可以简单地列出几个值,也可以是可编辑的,允许删除项目。可以使用 itemsVisible 属性来限制显示的项目数量。如果列表长度超过此限制,则会在最后一个标签旁边显示一个圆圈,表示额外的项目数量。

属性

名称

类型

描述

显示徽章

布尔值

可选。指定标签是否以徽章形式显示。

显示文本

布尔值

可选。指定标签是否显示文本。

可见项

编号

可选。列表中可见标签的限制。

标签

数组

组件接收到的 tag 元素列表。

tag 的形状如下:

  • colorIndex 是一个可选的颜色 ID。

  • icon 是一个可选图标,在显示的文本之前显示。

  • id 是标签的唯一标识符。

  • img 是一个可选的图片,以圆形显示在显示文本的前面。

  • onClick 是一个可选的回调函数,可以传递给该元素。这允许父元素根据点击的标签来处理任何功能。

  • onDelete 是一个可选的回调函数,可以传递给元素。这使得从标签列表中移除该项目成为可能,并必须由父元素进行处理。

  • text 是与标签相关联的显示 string

Example

在下一个示例中,使用了 TagsList 组件来显示多个标签。当点击标签或删除按钮时会发生什么,由开发者负责处理。

import { TagsList } from "@web/core/tags_list/tags_list";

class Parent extends Component {
  setup() {
    this.tags = [{
        id: "tag1",
        text: "Earth"
    }, {
        colorIndex: 1,
        id: "tag2",
        text: "Wind",
        onDelete: () => {...}
    }, {
        colorIndex: 2,
        id: "tag3",
        text: "Fire",
        onClick: () => {...},
        onDelete: () => {...}
    }];
  }
}
Parent.components = { TagsList };
Parent.template = xml`<TagsList tags="tags" />`;

根据每个标签所赋予的属性,它们的外观和行为将会有所不同。

使用不同 props 和属性的 TagsList 示例