第 1 章:Owl 组件

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

在 Owl 中,用户界面的每个部分都由一个组件管理:组件持有逻辑,并定义用于渲染用户界面的模板。在实践中,组件表现为一个继承 Component 类的小型 JavaScript 类。

在开始练习之前,请确保您已完成本 教程简介 中描述的所有步骤。

本章各练习的答案托管在 官方 Odoo 教程仓库 中。建议您先尝试自己解题,不要看答案!

Tip

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

视频:如何使用 DevTools

在本章中,我们使用 owl_playground 插件模块,它提供了一个仅包含 Owl 和少量其他文件的简化环境。目标是学习 Owl 本身,而不依赖 Odoo Web 客户端代码。开始使用前,请在浏览器中打开 /owl_playground/playground 路由:应该会显示一个带有 hello world 文本的 Owl 组件。

示例:一个 Counter 组件

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

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

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

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

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

Counter 组件指定了要渲染的模板名称。该模板以 XML 编写,定义了界面的一部分:

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

您可能注意到了 owl="1" 这个临时属性,它让 Odoo 能够区分 Owl 模板与旧 JavaScript 框架的模板。注意 Owl 模板与 QWeb 模板并不相同:它们可以包含额外的指令,例如 t-on-click。

1. 显示计数器

第一个练习:在位于 owl_playground/static/src/ 的 Playground 组件中实现一个计数器。要查看结果,可以在浏览器中访问 /owl_playground/playground 路由。

Exercise

  1. 修改 playground.js,使其像上面的示例一样充当计数器。您需要使用 useState hook,以便每当该组件读取的 state 对象的任何部分被修改时,组件都会重新渲染。

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

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

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

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

Tip

浏览器下载的 Odoo JavaScript 文件是经过压缩的。出于调试目的,文件未经压缩时更便于使用。切换到 带资源的调试模式,使文件不被压缩。

2. 将计数器提取为组件

目前我们已经把计数器的逻辑放在 Playground 组件中,下面看看如何从中创建 子组件。

Exercise

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

  2. 您可以先在同一个文件中完成,但完成后,请更新代码,将 Counter 移到它自己的文件夹和文件中,并从 ./counter/counter 相对导入。确保模板位于同名的独立文件中。

Important

别忘了在您的 JavaScript 文件中写上 /** @odoo-module **/。更多信息请参阅 这里。

3. 待办组件

我们将在 owl_playground/static/src/ 中创建新组件来跟踪待办事项列表。这将通过多个练习循序渐进地完成,逐步引入各种概念。

Exercise

  1. 创建一个 Todo 组件,它在 props 中接收一个 todo 对象并显示出来。显示效果应类似 3. buy milk。

  2. 如果任务已完成,为该任务添加 Bootstrap 类 text-muted 和 text-decoration-line-through。为此,您可以使用 动态属性。

  3. 修改 owl_playground/static/src/playground.js 和 owl_playground/static/src/playground.xml,先通过一些硬编码的 props 显示您的新 Todo 组件以进行测试。

    Example

    setup() {
        ...
        this.todo = { id: 3, description: "buy milk", done: false };
    }
    
../../../_images/todo.png

更多内容

Owl:动态类属性

4. Props 校验

Todo 组件有一个隐式 API。它期望在 props 中以指定格式接收待办事项的描述:id、description 和 done。让我们把这个 API 表达得更明确。我们可以添加 props 定义,让 Owl 在 dev mode 中执行校验步骤。您可以在 App configuration 中启用开发者模式。

为每个组件进行 props 验证是一个良好的实践。

Exercise

  1. 为 Todo 组件添加 props 校验。

  2. 打开浏览器开发者工具的 Console 选项卡,确认 props 校验在开发者模式下通过;开发者模式默认已在 owl_playground 中启用。可通过修改 owl_playground/static/src/main.js 中 mount 函数的 config 参数里的 dev 属性来开启和关闭开发者模式。

  3. 从 props 中移除 done 并重新加载页面。校验应该会失败。

5. 待办事项列表

现在,让我们显示待办事项列表,而不是只显示一个待办事项。目前仍可硬编码该列表。

Exercise

  1. 修改代码以显示待办事项列表而不是单个待办事项。创建一个新的 TodoList 组件来容纳 Todo 组件,并在其模板中使用 t-foreach。

  2. 思考应如何使用 t-key 指令来设置键值。

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

6. 添加待办事项

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

Exercise

  1. 在任务列表上方添加一个输入框,其占位符为 Enter a new task。

  2. 在 keyup 事件上添加一个名为 addTodo 的 事件处理器。

  3. 实现 addTodo,检查是否按下了回车键(ev.keyCode === 13),如果是,则创建一个新的待办事项,其描述为输入框的当前内容,并清空输入框的所有内容。

  4. 请确保待办事项具有唯一的 id。它可以只是一个每添加一个待办事项就递增的计数器。

  5. 将待办事项列表包装在 useState 钩子中,让 Owl 知道在列表被修改时应更新界面。

  6. 加分项:如果输入框为空,则不做任何操作。

    this.todos = useState([]);
    
../../../_images/create_todo.png

更多内容

Owl:响应式

7. 聚焦输入框

我们来看看如何通过 t-ref 和 useRef 来访问 DOM。

Exercise

  1. 当仪表板 mounted 时,聚焦上一练习中的 input。这应在 TodoList 组件中完成。

  2. 加分项:将代码提取为一个专用的 钩子 useAutofocus,放入新的 owl_playground/utils.js 文件。

8. 切换待办事项状态

现在,让我们添加新功能:将待办事项标记为已完成。这一点其实比想象中更棘手。状态的所有者与显示该状态的组件并不相同。因此,Todo 组件需要向其父组件传达待办事项的状态需要切换。一种经典做法是使用 callback prop toggleState。

Exercise

  1. 在任务的 id 之前添加带有属性 type="checkbox" 的输入框,当状态 done 为真时必须勾选。

    Tip

    如果 t-att 指令的计算结果为假值,QWeb 不会创建对应的属性。

  2. 添加回调 props toggleState。

  3. 在 Todo 组件的输入框上添加 click 事件处理程序,并确保它以待办事项的 id 调用 toggleState 函数。

  4. 让它跑起来!

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

9. 删除待办事项

最后一笔是让用户能够删除待办事项。

Exercise

  1. 添加新的回调 prop removeTodo。

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

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

    Tip

    如果您使用数组来存储待办事项列表,可以使用 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

10. 带插槽的通用卡片

Owl 拥有强大的 插槽 系统,让您可以编写通用组件。这有助于将界面不同部分之间的通用布局抽象复用。

Exercise

  1. 在 Counter 和 Todolist 组件之间插入一个新的 Card 组件。对该卡片使用以下 Bootstrap HTML 结构:

    <div class="card" style="width: 18rem;">
        <img src="..." class="card-img-top" alt="..." />
        <div class="card-body">
            <h5 class="card-title">Card title</h5>
            <p class="card-text">
                Some quick example text to build on the card title and make up the bulk
                of the card's content.
            </p>
            <a href="#" class="btn btn-primary">Go somewhere</a>
        </div>
    </div>
    
  2. 该组件应有两个插槽:一个用于标题,一个用于内容(默认插槽)。应可以按如下方式使用 Card 组件:

    <Card>
        <t t-set-slot="title">Card title</t>
        <p class="card-text">Some quick example text...</p>
        <a href="#" class="btn btn-primary">Go somewhere</a>
    </Card>
    
  3. 加分项:如果未提供 title 插槽,则 h5 元素应完全不渲染。

../../../_images/card1.png

11. 全面的 props 校验

Exercise

  1. 为 Card 组件添加 prop 校验。

  2. 尝试在 props 校验系统中表达:它需要一个 default 插槽和一个可选的 title 插槽。