第一章:Owl 组件

本章介绍 Owl framework,一个为 Odoo 量身定制的组件系统。 OWL 的主要构建模块是 componentstemplates

在 Owl 中,用户界面的每个部分都由组件管理:它们保存逻辑并定义用于呈现用户界面的模板。实际上,组件由 Component 类的子类的一个小型 JavaScript 类表示。

首先,您需要一个正在运行的 Odoo 服务器和一个开发环境设置。在开始练习之前,请确保您已遵循此 tutorial introduction 中描述的所有步骤。

小技巧

如果您使用 Chrome 作为网络浏览器,则可以安装 Owl Devtools 扩展程序。此扩展提供了许多功能来帮助您了解和分析任何 Owl 应用程序。

Video: How to use the DevTools

在本章中,我们使用 awesome_owl 插件,它提供了一个仅包含 Owl 和其他一些文件的简化环境。目标是学习 Owl 本身,而不依赖于 Odoo Web 客户端代码。

本章每个练习的解决方案都托管在 official Odoo tutorials repository 上。建议先尝试解决,不要看解决方案!

示例:Counter 组件

首先,让我们看一个简单的例子。下面显示的 Counter 组件是一个维护内部数值的组件,显示它,并在用户单击按钮时更新它。

import { Component, useState } from "@odoo/owl";

export class Counter extends Component {
    static template = "my_module.Counter";

    setup() {
        this.state = useState({ value: 0 });
    }

    increment() {
        this.state.value++;
    }
}

Counter 组件指定表示其 html 的模板的名称。它是使用 QWeb 语言以 XML 编写的:

<templates xml:space="preserve">
   <t t-name="my_module.Counter">
      <p>Counter: <t t-esc="state.value"/></p>
      <button class="btn btn-primary" t-on-click="increment">Increment</button>
   </t>
</templates>

1. 显示计数器

../../../_images/counter.png

作为第一个练习,让我们修改位于 awesome_owl/static/src/ 中的 Playground 组件,将其变成计数器。要查看结果,您可以使用浏览器转到 /awesome_owl 路线。

  1. 修改 playground.js ,使其充当计数器,如上例所示。保留 Playground 作为类名。您将需要使用 useState hook ,以便每当该组件读取的状态对象的任何部分被修改时,该组件都会重新呈现。

  2. 在同一组件中,创建一个 increment 方法。

  3. 修改 playground.xml 中的模板,使其显示您的计数器变量。使用 t-esc 输出数据。

  4. 在模板中添加一个按钮,并在按钮中指定 t-on-click 属性,以便在单击该按钮时触发 increment 方法。

小技巧

浏览器下载的 Odoo JavaScript 文件已缩小。出于调试目的,当文件未缩小时会更容易。切换到 debug mode with assets 这样文件就不会被缩小。

这个练习展示了 Owl 的一个重要特征:reactivity systemuseState 函数将一个值包装在代理中,以便 Owl 可以跟踪哪个组件需要状态的哪一部分,以便在值更改时可以更新它。尝试删除 useState 函数,看看会发生什么。

2. 提取子组件中的`Counter`

目前,我们在 Playground 组件中拥有计数器的逻辑,但它不可重用。让我们看看如何从中创建 sub-component

  1. 将计数器代码从 Playground 组件提取到新的 Counter 组件中。

  2. 您可以先在同一个文件中执行此操作,但完成后,请更新代码以将 Counter 移动到其自己的文件夹和文件中。从 ./counter/counter 相对导入。确保模板位于其自己的文件中,并且具有相同的名称。

  3. Playground 组件的模板中使用 <Counter/> 在 Playground 中添加两个计数器。

../../../_images/double_counter.png

小技巧

按照惯例,大多数组件代码、模板和 CSS 应该与组件具有相同的蛇形名称。例如,如果我们有一个 TodoList 组件,则其代码应位于 todo_list.jstodo_list.xml 中,如有必要,还应位于 todo_list.scss

3. 一个简单的 Card 组件

组件实际上是将复杂的用户界面划分为多个可重用部分的最自然的方式。但要使它们真正有用,就必须能够在它们之间传递一些信息。让我们看看父组件如何使用属性(最常见的是 props)向子组件提供信息。

本练习的目标是创建一个 Card 组件,它需要两个 props:titlecontent。例如,它的使用方法如下:

<Card title="'my title'" content="'some content'"/>

上面的示例应该使用 bootstrap 生成一些如下所示的 html:

<div class="card d-inline-block m-2" style="width: 18rem;">
    <div class="card-body">
        <h5 class="card-title">my title</h5>
        <p class="card-text">
         some content
        </p>
    </div>
</div>
  1. 创建 Card 组件

  2. 将其导入 Playground 并在其模板中显示一些卡片

../../../_images/simple_card.png

4.使用`markup`显示html

如果您在上一个练习中使用了 t-esc,那么您可能已经注意到 Owl 会自动转义其内容。例如,如果您尝试显示如下所示的某些 html:<Card title="'my title'" content="this.html"/>this.html = "<div>some content</div>"",则生成的输出将简单地将 html 显示为字符串。

在这种情况下,由于 Card 组件可用于显示任何类型的内容,因此允许用户显示一些 html 是有意义的。这是通过 t-out directive 完成的。

然而,将任意内容显示为 html 是危险的,它可能被用来注入恶意代码,因此默认情况下,Owl 总是会转义字符串,除非它已使用 markup 函数明确标记为安全。

  1. 更新 Card 以使用 t-out

  2. 更新 Playground 以导入 markup,并将其用于某些 html 值

  3. 确保您看到普通字符串始终被转义,这与标记字符串不同。

注解

t-esc 指令仍然可以在 Owl 模板中使用。它比 t-out 稍快。

../../../_images/markup.png

5. 道具验证

Card 组件具有隐式 API。它期望在其 props 中接收两个字符串:titlecontent。让我们让这个 API 更加明确。我们可以添加一个 props 定义,让 Owl 在 dev mode 中执行验证步骤。您可以在 App configuration 中激活开发模式(但在 awesome_owl 游乐场上默认激活)。

对每个组件进行 props 验证是一个很好的做法。

  1. props validation 添加到 Card 组件中。

  2. title 道具重命名为游乐场模板中的其他名称,然后检查浏览器开发工具的 Console 选项卡,您可以看到错误。

6. 两个`Counter`之和

我们在之前的练习中看到,props 可用于从父组件向子组件提供信息。现在,让我们看看如何以相反的方向传达信息:在本练习中,我们要显示两个 Counter 组件,并在它们下面显示它们的值的总和。因此,只要 Counter 值之一发生更改,就需要通知父组件 (Playground)。

这可以通过使用 callback prop 来完成:这是一个要回调的函数。子组件可以选择使用任何参数调用该函数。在我们的例子中,我们将简单地添加一个可选的 onChange 属性,每当 Counter 组件递增时就会调用该属性。

  1. 将道具验证添加到 Counter 组件:它应该接受可选的 onChange 函数道具。

  2. 更新 Counter 组件以在每次递增时调用 onChange 属性(如果存在)。

  3. 修改 Playground 组件以维护本地状态值 (sum),最初设置为 2,并将其显示在其模板中

  4. Playground 中实现 incrementSum 方法

  5. 将该方法作为两个(或更多!)子 Counter 组件的支柱。

../../../_images/sum_counter.png

重要

回调属性有一个微妙之处:它们通常应该使用 .bind 后缀来定义。请参阅 documentation

7. 待办事项列表

现在让我们通过创建待办事项列表来发现 Owl 的各种功能。 我们需要两个组件:一个 TodoList 组件,它将显示 TodoItem 组件列表。待办事项列表是应由 TodoList 维护的状态。

对于本教程,todo 是一个包含三个值的对象:`id`(数字)、`description`(字符串)和标志 `isCompleted`(布尔值):

{ id: 3, description: "buy milk", isCompleted: false }
  1. 创建一个 TodoList 和一个 TodoItem 组件。

  2. TodoItem 组件应该接收 todo 作为 prop,并在 div 中显示其 iddescription

  3. 现在,对待办事项列表进行硬编码:

    // in TodoList
    this.todos = useState([{ id: 3, description: "buy milk", isCompleted: false }]);
    
  4. 使用 t-foreach 显示 TodoItem 中的每个待办事项。

  5. 在操场上显示 TodoList

  6. 将道具验证添加到 TodoItem

../../../_images/todo_list.png

小技巧

由于 TodoListTodoItem 组件紧密耦合,因此将它们放在同一文件夹中是有意义的。

注解

Owl 中的 t-foreach 指令与 QWeb python 实现并不完全相同:它需要 t-key 唯一值,以便 Owl 可以正确协调每个元素。

8.使用动态属性

目前,TodoItem 组件不会直观地显示 todo 是否已完成。让我们使用 dynamic attributes 来做到这一点。

  1. 如果已完成,请在 TodoItem 根元素上添加 Bootstrap 类 text-mutedtext-decoration-line-through

  2. 更改硬编码的 this.todos 值以检查其是否正确显示。

即使该指令被命名为 t-att (对于属性),它也可用于设置 class 值(以及 html 属性,例如输入的 value )。

../../../_images/muted_todo.png

小技巧

Owl 允许您将静态类值与动态值结合起来。以下示例将按预期工作:

<div class="a" t-att-class="someExpression"/>

另请参阅:Owl: Dynamic class attributes

9. 添加待办事项

到目前为止,我们列表中的待办事项都是硬编码的。让我们通过允许用户向列表添加待办事项来使其更有用。

  1. 删除 TodoList 组件中的硬编码值:

    this.todos = useState([]);
    
  2. 使用占位符“输入新任务”在任务列表上方添加输入。

  3. 在名为 addTodokeyup 事件上添加 event handler

  4. 实现 addTodo 来检查是否按下了 Enter(ev.keyCode === 13),在这种情况下,创建一个新的待办事项,以输入的当前内容作为描述,并清除所有内容的输入。

  5. 确保待办事项具有唯一的 ID。它可以只是一个在每次待办事项时递增的计数器。

  6. 加分点:如果输入为空,则不执行任何操作。

../../../_images/create_todo.png

其他资料

Owl: Reactivity

理论:组件生命周期和钩子

到目前为止,我们已经看到了一个钩子函数的示例:useStatehook 是一个特殊函数,它*挂钩*组件的内部。对于 useState ,它生成链接到当前组件的代理对象。这就是为什么必须在 setup 方法中调用钩子函数,而且不能稍后!

../../../_images/component_lifecycle.svg

一个 Owl 组件会经历很多阶段:它可以被实例化、渲染、安装、更新、分离、销毁……这就是 component lifecycle。上图显示了组件生命周期中最重要的事件(挂钩以紫色显示)。粗略地说,一个组件被创建,然后更新(可能很多次),然后被销毁。

Owl提供了多种内置的`hooks functions <https://github.com/odoo/owl/blob/master/doc/reference/hooks.md>`_。所有这些都必须在 setup 函数中调用。例如,如果您想在安装组件时执行一些代码,您可以使用 onMounted 钩子:

setup() {
  onMounted(() => {
    // do something here
  });
}

小技巧

所有钩子函数都以 useon 开头。例如:useStateonMounted

10. 聚焦输入

让我们看看如何使用 t-refuseRef 访问 DOM。主要思想是您需要使用 t-ref 标记组件模板中的目标元素:

<div t-ref="some_name">hello</div>

然后你可以在JS中使用`useRef hook <https://github.com/odoo/owl/blob/master/doc/reference/hooks.md#useref>`_来访问它。但是,仔细想想就会发现一个问题:创建组件时,组件的实际 html 元素并不存在。它仅在安装组件时存在。但必须在 setup 方法中调用钩子。因此, useRef 返回一个包含 el (元素)键的对象,该键仅在安装组件时定义。

setup() {
   this.myRef = useRef('some_name');
   onMounted(() => {
      console.log(this.myRef.el);
   });
}
  1. 关注上一个练习中的 input。这应该从 TodoList 组件完成(请注意,输入 html 元素上有一个 focus 方法)。

  2. 加分点:将代码提取到新的 awesome_owl/utils.js 文件中的专用 hook useAutofocus 中。

../../../_images/autofocus.png

小技巧

引用通常以 Ref 为后缀,以表明它们是特殊对象:

this.inputRef = useRef('input');

11. 切换待办事项

现在,让我们添加一个新功能:将待办事项标记为已完成。这实际上比人们想象的要棘手。状态的所有者与显示状态的组件不同。因此, TodoItem 组件需要与其父组件进行通信,告知需要切换待办事项状态。一种经典的方法是添加 callback prop toggleState

  1. 在任务 id 之前添加带有属性 type="checkbox" 的输入,必须检查状态 isCompleted 是否为 true。

    小技巧

    如果计算结果为假值,Owl 不会创建使用 t-att 指令计算的属性。

  2. 将回调道具 toggleState 添加到 TodoItem

  3. TodoItem 组件的输入上添加 change 事件处理程序,并确保它使用 todo id 调用 toggleState 函数。

  4. 让它发挥作用!

../../../_images/toggle_todo.png

12.删除待办事项

最后一步是让用户删除待办事项。

  1. TodoItem 中添加新的回调属性 removeTodo

  2. TodoItem 组件的模板中插入 <span class="fa fa-remove"/>

  3. 每当用户单击它时,它都应该调用 removeTodo 方法。

  4. 让它发挥作用!

    小技巧

    如果您使用数组来存储待办事项列表,则可以使用 JavaScript splice 函数从中删除待办事项。

// find the index of the element to delete
const index = list.findIndex((elem) => elem.id === elemId);
if (index >= 0) {
      // remove the element at index from list
      list.splice(index, 1);
}
../../../_images/delete_todo.png

13. 带槽的通用 Card

previous exercise 中,我们构建了一个简单的 Card 组件。但老实说,它是相当有限的。如果我们想在卡片内显示一些任意内容(例如子组件)怎么办?好吧,它不起作用,因为卡片的内容是由字符串描述的。然而,如果我们可以将内容描述为一个模板,那就非常方便了。

这正是 Owl 的 slot 系统的设计目的:允许编写通用组件。

让我们修改 Card 组件以使用插槽:

  1. 删除 content 道具。

  2. 使用默认槽来定义主体。

  3. 插入一些具有任意内容的卡片,例如 Counter 组件。

  4. (奖励)添加道具验证。

../../../_images/generic_card.png

14. 尽量减少卡片内容

最后,让我们向 Card 组件添加一个功能,使其更有趣:我们想要一个按钮来切换其内容(显示或隐藏它)

  1. Card 组件添加一个状态以跟踪它是否打开(默认)

  2. 在模板中添加 t-if 以有条件地渲染内容

  3. 在头部添加一个按钮,修改代码,实现点击按钮时翻转状态

../../../_images/toggle_card.png