Owl 组件

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

《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标准)。

我们在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" owl="1">
  <div t-on-click="increment">
    <t t-esc="state.value"/>
  </div>
</t>

</templates>

Odoo代码尚未完全转换为Owl,因此需要一种方法来区分Owl模板(新代码)和旧模板(用于组件)。为了以向后兼容的方式实现这一点,所有新模板都应该定义为 owl 属性设置为1。

注解

不要忘记在您的Owl模板中设置 owl="1"

注解

模板名称应遵循约定 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

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

动作轮播

位置

@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,它是可调用的`Function`,用作回调函数。一旦在给定方向上完成了滑动,就会执行该操作。

  • 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

描述

下拉菜单是非常复杂的组件。它们需要提供许多功能,例如:

  • 点击时切换项目列表

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

  • 点击外部关闭

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

  • 当项目被选中时调用一个函数

  • 支持多级子菜单下拉框

  • SIY:自己设计样式

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

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

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

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

为了一劳永逸地解决这些问题,Odoo 框架提供了一组两个组件:一个 Dropdown 组件(实际的下拉菜单)和 DropdownItem,用于列表中的每个元素。

<Dropdown>
  <t t-set-slot="toggler">
    <!-- "toggler" slot content is rendered inside a button -->
    Click me to toggle the dropdown menu !
  </t>
  <!-- "default" slot content is rendered inside a div -->
  <DropdownItem onSelected="selectItem1">Menu Item 1</DropdownItem>
  <DropdownItem onSelected="selectItem2">Menu Item 2</DropdownItem>
</Dropdown>

属性

一个 <Dropdown/> 组件就是一个 <div class="dropdown"/>,旁边有一个 <button class="dropdown-toggle"/>,以及菜单的 <div class="dropdown-menu"/>。这个按钮负责菜单是否在 DOM 中存在。

下拉菜单

类型

描述

startOpen

布尔值

初始下拉菜单打开状态(默认为 false

menuClass

字符串

应用于下拉菜单 <div class="dropdown-menu"/> 的额外 CSS 类

togglerClass

字符串

应用于toggler `<button class=”dropdown-toggle”/>`的额外CSS类

hotkey

字符串

通过键盘切换打开的热键

tooltip

字符串

在切换器上添加工具提示

beforeOpen

函数

在打开之前执行逻辑的钩子。可能是异步的。

manualOnly

布尔值

如果为真,则只在单击按钮时切换下拉菜单(默认为 false

title

字符串

title attribute content for the <button class="dropdown-toggle"/> (default: none)

position

字符串

定义所需的菜单打开位置。RTL 方向会自动应用。应为有效的 usePosition hook 位置。(默认值: bottom-start

toggler

"parent"undefined

当设置为 "parent" 时, <button class="dropdown-toggle"/> 不会被渲染(因此 toggler 插槽被忽略),切换功能由父节点处理(例如用例:数据透视表单元格)。 (默认值: undefined

一个 <DropdownItem/> 简单地是一个 span (<span class="dropdown-item"/>)。当一个 <DropdownItem/> 被选中时,它调用它的 onSelected 属性。如果这个属性是一个方法,请确保它被绑定,如果该方法需要使用 this 值。

下拉菜单项

类型

描述

onSelected

函数

当下拉菜单项被选中时将调用的函数。

parentClosingMode

none | closest | all

当项目被选中时,控制哪个父级下拉菜单将关闭:none,closest或all(默认 = all

hotkey

字符串

可选热键以选择该项

href

字符串

如果提供,则DropdownItem将变为 <a href="value" class="dropdown-item"/> 而不是 <span class="dropdown-item"/>。 (默认值:未提供)

title

字符串

可选的标题属性,将传递给DropdownItem的根节点。(默认值:未提供)

dataset

对象

可选对象,包含应添加到根元素数据集的值。这可以使元素在编程中更容易找到,例如在测试或导览中。

技术说明

渲染后的 DOM 结构如下:

<div class="dropdown">
    <button class="dropdown-toggle">Click me !</button>
    <!-- following <div/> will or won't appear in the DOM depending on the state controlled by the preceding button -->
    <div class="dropdown-menu">
        <span class="dropdown-item">Menu Item 1</span>
        <span class="dropdown-item">Menu Item 2</span>
    </div>
</div>

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

  • toggler 插槽:它包含您的下拉菜单的 toggler 元素,并且会在下拉菜单的 button 内部呈现(除非将 toggler 属性设置为 parent),

  • default 插槽:它包含下拉菜单本身的 元素,并在 <div class="dropdown-menu"/> 内呈现。虽然不是强制的,但通常在 menu 插槽中至少有一个 DropdownItem

当多个下拉菜单共享DOM中的同一父元素时,它们被视为一组,并且将相互通知其状态更改。这意味着当其中一个下拉菜单打开时,其他下拉菜单将在鼠标悬停时自动打开,无需点击。

示例:直接兄弟下拉菜单

当点击一个下拉切换器(文件编辑**或**关于),其他的将在悬停时打开。

<div>
  <Dropdown>
    <t t-set-slot="toggler">File</t>
    <DropdownItem onSelected="() => this.onItemSelected('file-open')">Open</DropdownItem>
    <DropdownItem onSelected="() => this.onItemSelected('file-new-document')">New Document</DropdownItem>
    <DropdownItem onSelected="() => this.onItemSelected('file-new-spreadsheet')">New Spreadsheet</DropdownItem>
  </Dropdown>
  <Dropdown>
    <t t-set-slot="toggler">Edit</t>
    <DropdownItem onSelected="() => this.onItemSelected('edit-undo')">Undo</DropdownItem>
    <DropdownItem onSelected="() => this.onItemSelected('edit-redo')">Redo</DropdownItem>
    <DropdownItem onSelected="() => this.onItemSelected('edit-find')">Search</DropdownItem>
  </Dropdown>
  <Dropdown>
    <t t-set-slot="toggler">About</t>
    <DropdownItem onSelected="() => this.onItemSelected('about-help')">Help</DropdownItem>
    <DropdownItem onSelected="() => this.onItemSelected('about-update')">Check update</DropdownItem>
  </Dropdown>
</div>

示例:多级下拉菜单(使用 t-call

这个例子展示了如何创建一个带有子菜单的 文件 下拉菜单,包括 新建另存为... 子元素。

<t t-name="addon.Dropdown.File" owl="1">
  <Dropdown>
    <t t-set-slot="toggler">File</t>
    <DropdownItem onSelected="() => this.onItemSelected('file-open')">Open</DropdownItem>
    <t t-call="addon.Dropdown.File.New"/>
    <DropdownItem onSelected="() => this.onItemSelected('file-save')">Save</DropdownItem>
    <t t-call="addon.Dropdown.File.Save.As"/>
  </Dropdown>
</t>

<t t-name="addon.Dropdown.File.New" owl="1">
  <Dropdown>
    <t t-set-slot="toggler">New</t>
    <DropdownItem onSelected="() => this.onItemSelected('file-new-document')">Document</DropdownItem>
    <DropdownItem onSelected="() => this.onItemSelected('file-new-spreadsheet')">Spreadsheet</DropdownItem>
  </Dropdown>
</t>

<t t-name="addon.Dropdown.File.Save.As" owl="1">
  <Dropdown>
    <t t-set-slot="toggler">Save as...</t>
    <DropdownItem onSelected="() => this.onItemSelected('file-save-as-csv')">CSV</DropdownItem>
    <DropdownItem onSelected="() => this.onItemSelected('file-save-as-pdf')">PDF</DropdownItem>
  </Dropdown>
</t>

示例:多级下拉菜单(嵌套)

<Dropdown>
  <t t-set-slot="toggler">File</t>
  <DropdownItem onSelected="() => this.onItemSelected('file-open')">Open</DropdownItem>
  <Dropdown>
    <t t-set-slot="toggler">New</t>
    <DropdownItem onSelected="() => this.onItemSelected('file-new-document')">Document</DropdownItem>
    <DropdownItem onSelected="() => this.onItemSelected('file-new-spreadsheet')">Spreadsheet</DropdownItem>
  </Dropdown>
  <DropdownItem onSelected="() => this.onItemSelected('file-save')">Save</DropdownItem>
  <Dropdown>
    <t t-set-slot="toggler">Save as...</t>
    <DropdownItem onSelected="() => this.onItemSelected('file-save-as-csv')">CSV</DropdownItem>
    <DropdownItem onSelected="() => this.onItemSelected('file-save-as-pdf')">PDF</DropdownItem>
  </Dropdown>
</Dropdown>

示例:递归多级下拉菜单

在这个例子中,我们递归调用一个模板来显示类似树形结构的内容。

<t t-name="addon.MainTemplate" owl="1">
  <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" owl="1">
  <Dropdown>
    <t t-set-slot="toggler"><t t-esc="name"/></t>
      <t t-foreach="items" t-as="item" t-key="item.id">

        <!-- If this item has no child: make it a <DropdownItem/> -->
        <t t-if="!item.childrenTree.length">
          <DropdownItem onSelected="() => this.onItemSelected(item)" t-esc="item.name"/>
        </t>
        <!-- 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>

笔记本电脑

位置

@web/core/notebook/notebook

描述

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

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

属性

名称

类型

描述

anchors

object

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

className

string

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

defaultPage

string

可选。默认显示的页面 id

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

将访问键 p 绑定到上一页按钮,将访问键 n 绑定到下一页按钮(默认为 true)。