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

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

CheckBox

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

ColorList

可供选择的颜色列表

Dropdown

功能完整的下拉菜单

Notebook

使用选项卡在页面之间导航的组件

Pager

用于处理分页的小型组件

SelectMenu

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

TagsList

以圆角胶囊形式显示的标签列表

ActionSwiper

库位

@web/core/action_swiper/action_swiper

说明

这是一个在元素被水平滑动时执行动作的组件。滑动器会包裹目标元素,为其添加动作。当用户松开滑动器时,如果滑动距离超过其宽度的一定比例,动作就会被执行。

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

使用该组件最简单的方式是像上面所示那样,直接在 xml 模板中把它包裹在目标元素周围。但有时你可能想扩展已有元素,又不希望复制模板。这完全可以做到。

如果你想扩展已有元素的行为,必须直接包裹该元素,把它放在里面。此外,你还可以按条件添加 props,以控制元素何时可滑动、其动画效果以及执行动作所需的最小滑动比例。

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

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)的语言时,动作顺序会对调。

属性(Props)

名称

类型

说明

animationOnMove

Boolean

可选布尔值,决定滑动过程中是否有平移效果

animationType

String

滑动结束后使用的可选动画(bounce 或 forwards)

onLeftSwipe

Object

若存在,则可将滑动器向左滑动

onRightSwipe

Object

若存在,则可将滑动器向右滑动

swipeDistanceRatio

数字

执行动作所需滑动的最小宽度比例(可选)

你可以同时使用 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)

名称

类型

说明

value

boolean

若为 true,则复选框处于勾选状态,否则为未勾选

disabled

boolean

若为 true,则复选框被禁用,否则为启用

ColorList

库位

@web/core/colorlist/colorlist

说明

ColorList 让你从预定义列表中选择一种颜色。默认情况下,该组件显示当前选中的颜色,并且在存在 canToggle props 之前不可展开。不同的 props 可以改变其行为:始终展开列表,或使其在被点击后充当切换器,显示可用颜色列表直到选中某个颜色。

属性(Props)

名称

类型

说明

canToggle

boolean

可选。点击时 colorlist 是否可以展开列表

colors

array

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

forceExpanded

boolean

可选。如果为 true,则列表始终处于展开状态

isExpanded

boolean

可选。如果为 true,则列表默认处于展开状态

onColorSelected

function

选择颜色后执行的回调

selectedColor

number

可选。所选颜色的 id

颜色 id 如下:

编号

颜色

0

无颜色

1

红色

2

橙色

3

黄色

4

浅蓝色

5

深紫色

6

鲑鱼粉

7

中蓝色

8

深蓝色

9

洋红色

12

绿色

11

紫色

库位

@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

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();
    }
  }
}

Notebook

库位

@web/core/notebook/notebook

说明

Notebook 用于在选项卡界面中显示多个页面。选项卡可以位于元素的顶部以水平显示,也可以位于左侧以垂直布局。

有两种方式可以定义要实例化的 Notebook 页面:使用 slot,或者传入专用的 props。

可以使用 isDisabled 属性禁用页面,该属性可直接设置在 slot 节点上;如果 Notebook 是通过作为 props 传入的 pages 使用的,也可以在页面声明中设置。一旦被禁用,相应的选项卡将变灰并同时设为非活动状态。

属性(Props)

名称

类型

说明

anchors

object

可选。允许锚点导航到选项卡内不可见的元素。

className

string

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

defaultPage

string

可选。默认显示的页面 id。

icons

array

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

orientation

string

可选。选项卡方向是 horizontal 还是 vertical。

onPageUpdate

function

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

pages

array

可选。包含要由模板填充的 page 列表。

示例

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

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

名称

类型

说明

offset

number

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

limit

number

页的大小。offset 与 limit 之和对应页中最后一个元素的索引。

total

number

页可以到达的元素总数。

onUpdate

function

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

isEditable

boolean

允许点击当前页码来编辑它(默认为 true)。

withAccessKey

boolean

在上一页按钮上绑定访问键 p,在下一页按钮上绑定 n,默认值为 true。

SelectMenu

库位

@web/core/select_menu/select_menu

说明

当你想做的比使用原生 select 元素更多时,可以使用此组件。你可以定义自己的选项模板,从而在选项之间进行搜索,或将它们分组到子区段中。

注解

请优先使用原生 HTML select 元素,因为它默认提供无障碍功能,并且在移动设备上具有更好的用户界面。此组件是为更复杂的用例而设计的,用于克服原生元素的限制。

属性(Props)

名称

类型

说明

choices

array

可选。要在下拉列表中显示的 choice 项列表。

class

string

可选。设置在 SelectMenu 组件根节点上的类名。

groups

array

可选。group 列表,其中包含要在下拉列表中显示的 choices。

multiSelect

boolean

可选。启用多重选择。启用多重选择后,所选值会以 标签 的形式显示在 SelectMenu 输入框中。

togglerClass

string

可选。设置在切换按钮上的类名。

required

boolean

可选。所选值是否可以被取消选择。

searchable

boolean

可选。下拉列表中是否显示搜索框。

searchPlaceholder

string

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

value

any

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

onSelect

function

可选。选择某个选项时执行的回调。

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 用法与自定义示例

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

SelectMenu 多重选择用法示例

对于更高级的用例,你可以使用 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>
SelectMenu 底部区域自定义示例

TagsList

库位

@web/core/tags_list/tags_list

说明

此组件可以以圆角胶囊的形式显示标签列表。这些标签既可以只是列出几个值,也可以是可编辑的,从而允许移除条目。可以使用 itemsVisible 属性限制显示条目的数量。如果列表长度超过该限制,则额外条目的数量会显示在最后一个标签旁边的圆圈中。

属性(Props)

名称

类型

说明

displayBadge

boolean

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

displayText

boolean

可选。标签是否带文本显示。

itemsVisible

number

可选。列表中可见标签的数量上限。

tags

array

传给该组件的 tag 元素列表。

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: () => {...}
    }];
  }
}

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

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