Owl components

Odoo的Javascript框架使用了一个名为Owl的自定义组件框架。它是一个声明式组件系统,受到Vue和React的启发。组件使用 QWeb模板 定义,并且使用一些Owl特定的指令进行增强。官方的 Owl文档<https://github.com/odoo/owl/blob/master/doc/readme.md> _包含了完整的参考和教程。

重要

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

注解

目前,所有的Odoo版本(从14版本开始)共享同一个Owl版本。

使用 Owl 组件

The Owl documentation already documents in detail the Owl framework, so this page will only provide Odoo specific information. But first, let us see how we can make a simple component in 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标准)。

我们在javascript代码中使用 xml 助手定义模板。然而,这只是为了入门。实际上,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

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

CheckBox

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

ColorList

可供选择的颜色列表

Dropdown

全功能下拉菜单

Notebook

一个使用选项卡导航页面的组件

Pager

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

SelectMenu

a dropdown component to choose between different options

TagsList

a list of tags displayed in rounded pills

动作轮播

位置

@web/core/action_swiper/action_swiper

描述

这是一个组件,可以在元素水平滑动时执行操作。Swiper将目标元素包装起来,以添加操作。一旦用户释放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)语言时,操作会被排列。

属性

名称

类型

描述

animationOnMove

Boolean

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

animationType

String

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

onLeftSwipe

Object

如果存在,则可以向左滑动 actionswiper

onRightSwipe

Object

如果存在,则可以向右滑动 actionswiper

swipeDistanceRatio

Number

可选的最小宽度比率,必须滑动才能执行操作

您可以同时使用 onLeftSwipeonRightSwipe 属性。

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

  • action, which is the callable Function serving as a callback. Once the swipe has been completed in the given direction, that action is performed.

  • icon 是要使用的图标类,通常用于表示动作。它必须是一个 string

  • bgColor 是背景颜色,用于装饰操作。可以是以下之一 bootstrap contextual color (danger, info, secondary, successwarning)。

这些值必须提供以定义 swiper 的行为和视觉效果。

示例:扩展现有组件

在下面的示例中,您可以使用 xpath 来包装现有元素到 ActionSwiper 组件中。在这里,一个 swiper 已经被添加到邮件中标记消息为已读。

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

属性

名称

类型

描述

value

boolean

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

disabled

boolean

如果为真,则复选框被禁用,否则它是启用的

颜色列表

位置

@web/core/colorlist/colorlist

描述

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

属性

名称

类型

描述

canToggle

boolean

可选。颜色列表是否可以在单击时展开列表

colors

array

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

forceExpanded

boolean

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

isExpanded

boolean

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

onColorSelected

function

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

selectedColor

number

可选。所选颜色的 id

颜色 id 的如下:

Id

颜色

0

No color

1

Red

2

Orange

3

Yellow

4

Light blue

5

Dark purple

6

Salmon pink

7

Medium blue

8

Dark blue

9

Fuchsia

12

Green

11

Purple

位置

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

描述

The Dropdown lets you show a menu with a list of items when a toggle is clicked on. They can be combined with DropdownItems to invoke callbacks and close the menu when items are selected.

Dropdowns are surprisingly complicated components, the list of features they provide is as follow:

  • 点击时切换项目列表

  • 点击外部关闭

  • Call a function when items are selected

  • 当选择一个项目时,可选择关闭项目列表

  • SIY:自己设计样式

  • 支持多级子菜单下拉框

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

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

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

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

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

要正确使用 <Dropdown/> 组件,您需要填充两个 OWL slots

  • default slot: it contains the toggle elements of your dropdown. By default, click events will be attached to this element to open and close the dropdown.

  • content slot: it contains the elements of the dropdown menu itself and is rendered inside a popover. Although it is not mandatory, you can put some DropdownItem inside this slot, the dropdown will automatically close when these items are selected.

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

Nested Dropdown

Dropdown can be nested, to do this simply put new Dropdown components inside other dropdown’s content slot. When the parent dropdown is open, child dropdowns will open automatically on hover.

By default, selecting a DropdownItem will close the whole Dropdown tree.

Example

This example shows how one could make a nested File dropdown menu, with submenus for the New sub elements.

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

In the example bellow, we recursively call a template to display a tree-like structure.

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

Controlled Dropdown

If needed, you can also open or close the dropdown using code. To do this you must use the useDropdownState hook along with the state prop. useDropdownState returns an object that has an open and a close method (as well as an isOpen getter). Give the object to the state prop of the dropdown you want to control and calling the respective functions should now open and close your dropdown.

You can also set manual to true if you don’t want the default click handlers to be added on the toggle.

Example

The following example shows a dropdown that opens automatically when mounted and only has a 50% chance of closing when clicking on the button inside.

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

描述

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

有两种方法可以定义您的笔记本页面进行实例化,一种是使用 slot,另一种是通过传递专用的 props

A page can be disabled with the isDisabled attribute, set directly on the slot node, or in the page declaration, if the Notebook is used with the pages given as props. Once disabled, the corresponding tab is greyed out and set as inactive as well.

属性

名称

类型

描述

anchors

object

可选。允许在不可见选项卡内部的元素之间进行锚点导航。

className

string

可选。设置在组件根部的类名。

defaultPage

string

可选。默认显示的页面 id

icons

array

optional. List of icons used in the tabs.

orientation

string

可选。选项卡方向是 水平 还是 垂直

onPageUpdate

function

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

pages

array

可选。包含从模板填充的 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" />

属性

名称

类型

描述

offset

number

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

limit

number

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

total

number

页面可以达到的元素总数。

onUpdate

function

当分页器修改页面时调用的函数。此函数可以是异步的,当此函数执行时,分页器不能被编辑。

isEditable

boolean

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

withAccessKey

boolean

Binds access key p on the previous page button and n on the next page one (true by default).

SelectMenu

位置

@web/core/select_menu/select_menu

描述

This component can be used when you want to do more than using the native select element. You can define your own option template, allowing to search between your options, or group them in subsections.

注解

Prefer the native HTML select element, as it provides by default accessibility features, and has a better user interface on mobile devices. This component is designed to be used for more complex use cases, to overcome limitations of the native element.

属性

名称

类型

描述

choices

array

optional. List of choice’s to display in the dropdown.

class

string

optional. Classname set on the root of the SelectMenu component.

groups

array

optional. List of group’s, containing choices to display in the dropdown.

multiSelect

boolean

optional. Enable multiple selections. When multiple selection is enabled, selected values are displayed as tag’s in the SelectMenu input.

togglerClass

string

optional. classname set on the toggler button.

required

boolean

optional. Whether the selected value can be unselected.

searchable

boolean

optional. Whether a search box is visible in the dropdown.

searchPlaceholder

string

optional. Text displayed as the search box placeholder.

value

any

optional. Current selected value. It can be from any kind of type.

onSelect

function

optional. Callback executed when an option is chosen.

The shape of a choice is the following:

  • value is actual value of the choice. It is usually a technical string, but can be from any type.

  • label is the displayed text associated with the option. This one is usually a more friendly and translated string.

The shape of a group is the following:

  • choices is the list of choice’s to display for this group.

  • label is the displayed text associated with the group. This is a string displayed at the top of the group.

Example

In the following example, the SelectMenu will display four choices. One of them is displayed on top of the options, since no groups are associated with it, but the other ones are separated by the label of their group.

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'"
  />
`;

You can also customize the appearance of the toggler and set a custom template for the choices, using the appropriate component slot’s.

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>
`;
Example of SelectMenu usage and customization

When SelectMenu is used with multiple selection, the value props must be an Array containing the values of the selected choices.

Example of SelectMenu used with multiple selection

For more advanced use cases, you can customize the bottom area of the dropdown, using the bottomArea slot. Here, we choose to display a button with the corresponding value set in the search input.

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>
`;
Example of SelectMenu's bottom area customization

TagsList

位置

@web/core/tags_list/tags_list

描述

This component can display a list of tags in rounded pills. Those tags can either simply list a few values, or can be editable, allowing the removal of items. It can be possible to limit the number of displayed items using the itemsVisible props. If the list is longer than this limit, the number of additional items is shown in a circle next to the last tag.

属性

名称

类型

描述

displayBadge

boolean

optional. Whether the tag is displayed as a badge.

displayText

boolean

optional. Whether the tag is displayed with a text or not.

itemsVisible

number

optional. Limit of visible tags in the list.

tags

array

list of tag’s elements given to the component.

The shape of a tag is the following:

  • colorIndex is an optional color id.

  • icon is an optional icon displayed just before the displayed text.

  • id is a unique identifier for the tag.

  • img is an optional image displayed in a circle, just before the displayed text.

  • onClick is an optional callback that can be given to the element. This allows the parent element to handle any functionality depending on the tag clicked.

  • onDelete is an optional callback that can be given to the element. This makes the removal of the item from the list of tags possible, and must be handled by the parent element.

  • text is the displayed string associated with the tag.

Example

In the next example, a TagsList component is used to display multiple tags. It’s at the developer to handle from the parent what would happen when the tag is pressed, or when the delete button is clicked.

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" />`;

Depending the attributes given to each tag, their appearance and behavior will differ.

Examples of TagsList using different props and attributes