第1章:Owl组件¶
本章介绍了 Owl 框架 <https://github.com/odoo/owl>
_,一个专为 Odoo 定制的组件系统。OWL 的主要构建模块是 components <https://github.com/odoo/owl/blob/master/doc/reference/component.md>
_ 和 templates <https://github.com/odoo/owl/blob/master/doc/reference/templates.md>
_。
在Owl中,用户界面的每个部分都由组件管理:它们保存逻辑并定义用于呈现用户界面的模板。实际上,组件由一个小的JavaScript类表示,该类是 Component
类的子类。
要开始使用,您需要一个正在运行的 Odoo 服务器和一个开发环境设置。在进行练习之前,请确保您已按照此 教程介绍 中描述的所有步骤操作。
小技巧
如果您使用 Chrome 作为您的网络浏览器,您可以安装 Owl Devtools
扩展。这个扩展提供了许多功能,帮助您理解和分析任何 Owl 应用程序。
在本章中,我们使用 awesome_owl
插件,它提供了一个简化的环境,仅包含 Owl 和其他一些文件。目的是学习 Owl 本身,而不依赖于 Odoo 网页客户端代码。
本章每个练习的解决方案都托管在 官方 Odoo 教程仓库 上。建议先尝试自己解决,不要查看解决方案!
示例:一个 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. 显示计数器¶
作为第一个练习,让我们修改位于 awesome_owl/static/src/
的 Playground
组件,将其变成一个计数器。要查看结果,你可以通过浏览器访问 /awesome_owl
路由。
修改
playground.js
,使其像上面的示例一样充当计数器。保持Playground
作为类名。你需要使用 useState 钩子 ,以便在该组件读取的状态对象的任何部分被修改时重新渲染组件。在同一组件中创建一个
increment
方法。修改
playground.xml
中的模板,以便显示您的计数变量。使用 t-esc 输出数据。在模板中添加一个按钮,并在按钮中指定一个 t-on-click 属性,以便在按钮被点击时触发
increment
方法。
小技巧
浏览器下载的Odoo JavaScript文件是经过压缩的。为了调试目的,文件未压缩时更为方便。切换到 带资源的调试模式 ,这样文件就不会被压缩。
这个练习展示了 Owl 的一个重要特性:reactivity system。useState
函数将一个值包装在代理中,以便 Owl 可以跟踪哪个组件需要状态的哪一部分,从而在值更改时进行更新。尝试移除 useState
函数,看看会发生什么。
2. Extract Counter
in a sub component¶
目前我们在 Playground
组件中实现了一个计数器的逻辑,但它不可重用。让我们看看如何从中创建一个 子组件:
从
Playground
组件中提取计数器代码到一个新的Counter
组件中。你可以先在同一个文件中完成,但是一旦完成后,请更新你的代码,将
Counter
移动到它自己的文件夹和文件中。从./counter/counter
相对导入它。确保模板在它自己的文件中,文件名相同。在
Playground
组件的模板中使用<Counter/>
来添加两个计数器到你的 playground 中。
小技巧
按照惯例,大多数组件的代码、模板和 CSS 文件应使用与组件相同的蛇形命名法。例如,如果我们有一个 TodoList
组件,其代码应位于 todo_list.js
、todo_list.xml
中,如果需要,还应位于 todo_list.scss
中。
3. A simple Card
component¶
组件确实是将复杂的用户界面划分为多个可重用部分的最自然方式。但要使它们真正有用,必须能够在它们之间传递一些信息。让我们看看父组件如何通过使用属性(通常称为 props)向子组件提供信息。
本练习的目标是创建一个 Card
组件,它接收两个属性:title
和 content
。例如,以下是如何使用它的示例:
<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>
创建一个
Card
组件在
Playground
中导入并在其模板中显示几张卡片
4. Using markup
to display html¶
如果你在前面的练习中使用了 t-esc
,那么你可能已经注意到 Owl 会自动转义其内容。例如,如果你尝试显示一些像这样的 html:<Card title="'my title'" content="this.html"/>
并且 this.html = "<div>some content</div>""
,那么结果输出将简单地将 html 显示为字符串。
在这种情况下,由于 Card
组件可用于显示任何类型的内容,允许用户显示一些 html 是有意义的。这是通过 t-out 指令 实现的。
然而,将任意内容显示为 HTML 是危险的,因为它可能被用来注入恶意代码,因此默认情况下,Owl 将始终对字符串进行转义,除非它已明确使用 markup
函数标记为安全。
更新
Card
以使用t-out
更新
Playground
以导入markup
,并在某些 html 值上使用它确保你看到普通字符串总是被转义的,与标记字符串不同。
注解
t-esc
指令仍可在 Owl 模板中使用。它比 t-out
稍快一些。
5. Props validation¶
Card
组件有一个隐式的 API。它期望在其 props 中接收两个字符串:title
和 content
。让我们使这个 API 更加明确。我们可以添加一个 props 定义,这将让 Owl 在 dev mode 中执行一个验证步骤。你可以在 App configuration 中激活开发模式(但在 awesome_owl
游乐场中默认已激活)。
对于每个组件进行属性验证是一个好的实践。
向
Card
组件添加 props 验证。将
title
属性重命名为其他名称在 playground 模板中,然后在浏览器的开发者工具的 Console 标签页中检查是否可以看到错误。
6. The sum of two Counter
¶
我们在之前的练习中看到,props
可以用于从父组件向子组件传递信息。现在,让我们看看如何以相反的方向传递信息:在这个练习中,我们想要显示两个 Counter
组件,并在它们下方显示它们的值的总和。因此,每当其中一个 Counter
的值发生变化时,父组件(Playground
)需要被通知。
这可以通过使用 callback prop 来实现:一个旨在被回调的函数 prop。子组件可以选择用任何参数调用该函数。在我们的例子中,我们将简单地添加一个可选的 onChange
prop,每当 Counter
组件增加时,它就会被调用。
为
Counter
组件添加 prop 验证:它应接受一个可选的onChange
函数 prop。更新
Counter
组件,使其在每次递增时调用onChange
属性(如果存在)。修改
Playground
组件以维护一个本地状态值 (sum
),初始设置为 2,并在其模板中显示它在
Playground
中实现一个incrementSum
方法将该方法作为属性传递给两个(或更多!)子
Counter
组件。
重要
回调属性有一个微妙之处:通常应该使用 .bind
后缀来定义它们。请参阅 文档。
7. A todo list¶
现在让我们通过创建一个待办事项列表来探索 Owl 的各种功能。我们需要两个组件:一个 TodoList
组件,它将显示一系列 TodoItem
组件。待办事项列表是一个状态,应由 TodoList
维护。
在本教程中,todo
是一个包含三个值的对象:一个 `id`(数字)、一个 `description`(字符串)和一个标志 `isCompleted`(布尔值):
{ id: 3, description: "buy milk", isCompleted: false }
创建
TodoList
和TodoItem
组件。TodoItem
组件应接收一个todo
作为 prop,并在div
中显示其id
和description
。目前,硬编码待办事项列表:
// in TodoList this.todos = useState([{ id: 3, description: "buy milk", isCompleted: false }]);
使用 t-foreach 在
TodoItem
中显示每个待办事项。在 playground 中显示一个
TodoList
。为
TodoItem
添加 props 验证。
小技巧
由于 TodoList
和 TodoItem
组件紧密耦合,将它们放在同一个文件夹中是合理的。
注解
t-foreach
指令在 Owl 中与 QWeb Python 实现并不完全相同:它需要一个唯一的 t-key
值,以便 Owl 能够正确地协调每个元素。
8. Use dynamic attributes¶
目前,TodoItem
组件在视觉上并未显示 todo
是否已完成。让我们通过使用 动态属性 来实现这一点。
如果
TodoItem
根元素已完成,则在其上添加 Bootstrap 类text-muted
和text-decoration-line-through
。更改硬编码的
this.todos
值以检查其是否正确显示。
尽管指令名为 t-att`(用于属性),但它也可用于设置 `class
值(以及诸如输入框的 value
等 HTML 属性)。
9. Adding a todo¶
到目前为止,我们列表中的待办事项是硬编码的。让我们通过允许用户向列表中添加待办事项使其更加有用。
移除
TodoList
组件中的硬编码值:this.todos = useState([]);
在任务列表上方添加一个输入框,占位符为 输入新任务。
添加一个 event handler 在
keyup
事件上命名为addTodo
。实现
addTodo
函数来检查是否按下了回车键 (ev.keyCode === 13
),如果是的话,使用输入框当前的内容创建一个新的待办事项,并清空输入框中的所有内容。确保todo具有唯一的id。它可以只是一个计数器,在每个todo上递增。
奖励分:如果输入为空,则不执行任何操作。
另请参阅
理论:组件生命周期与钩子¶
到目前为止,我们已经看到了一个钩子函数的例子:useState
。一个 hook 是一个特殊的函数,它 钩入 组件的内部。在 useState
的情况下,它会生成一个与当前组件链接的代理对象。这就是为什么钩子函数必须在 setup
方法中调用,而不能更晚!
一个 Owl 组件会经历很多阶段:它可以被实例化、渲染、挂载、更新、分离、销毁……这就是 组件生命周期. 上图展示了一个组件生命周期中最重要的阶段(钩子以紫色显示)。简而言之,一个组件被创建,然后被更新(可能多次),最后被销毁。
Owl 提供了多种内置的 hooks 函数。所有这些函数都必须在 setup
函数中调用。例如,如果您想在组件挂载时执行一些代码,可以使用 onMounted
钩子:
setup() {
onMounted(() => {
// do something here
});
}
小技巧
所有钩子函数都以 use
或 on
开头。例如:useState
或 onMounted
。
10. Focusing the input¶
让我们看看如何使用 t-ref 和 useRef 访问 DOM。主要思路是需要在组件模板中使用 t-ref
标记目标元素:
<div t-ref="some_name">hello</div>
然后,你可以通过 useRef hook 在 JS 中访问它。然而,如果你仔细想想,这里有一个问题:组件的实际 HTML 元素在组件创建时并不存在。它只有在组件挂载时才存在。但钩子必须在 setup
方法中被调用。因此,useRef
返回一个包含 `el`(表示元素)键的对象,该键仅在组件挂载时定义。
setup() {
this.myRef = useRef('some_name');
onMounted(() => {
console.log(this.myRef.el);
});
}
聚焦上一个练习中的
input
。这应该从TodoList
组件中完成(注意,input
HTML 元素上有一个focus
方法)。加分项:将代码提取到一个专门的 hook
useAutofocus
中,并放入一个新的awesome_owl/utils.js
文件中。
小技巧
Refs 通常以 Ref
作为后缀,以明确表示它们是特殊的对象:
this.inputRef = useRef('input');
11. Toggling todos¶
现在,让我们添加一个新功能:将待办事项标记为已完成。这实际上比人们想象的要复杂一些。状态的拥有者与显示它的组件不同。因此,TodoItem
组件需要通知其父组件切换待办事项的状态。实现这一点的经典方法之一是添加一个 callback prop toggleState
。
在任务ID前添加一个带有属性
type="checkbox"
的输入框,如果状态isCompleted
为真,则必须勾选该输入框。小技巧
如果
t-att
指令计算出的值为假值,Owl 不会创建该属性。向
TodoItem
添加回调属性toggleState
。在
TodoItem
组件的输入上添加一个change
事件处理程序,并确保它调用带有待办事项 id 的toggleState
函数。让它工作起来!
12. Deleting todos¶
最后一步是让用户删除待办事项。
在
TodoItem
中添加一个新的回调属性removeTodo
。在
TodoItem
组件的模板中插入<span class="fa fa-remove"/>
。每当用户点击它时,它应该调用
removeTodo
方法。让它工作起来!
小技巧
如果你正在使用数组来存储待办事项清单,你可以使用 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);
}
13. Generic Card
with slots¶
在 之前的练习 中,我们构建了一个简单的 Card
组件。但说实话,它的功能相当有限。如果我们想在卡片内显示一些任意内容,比如一个子组件,该怎么办呢?嗯,这行不通,因为卡片的内容是用字符串描述的。然而,如果我们能够将内容描述为模板的一部分,那将会非常方便。
这正是 Owl 的 slot 系统的设计目的:允许编写通用组件。
让我们修改 Card
组件以使用插槽:
移除
content
属性。使用默认插槽来定义主体。
插入一些带有任意内容的卡片,例如一个
Counter
组件。(bonus) Add prop validation.
14. Minimizing card content¶
最后,让我们为 Card
组件添加一个功能,使其更有趣:我们想要一个按钮来切换其内容(显示或隐藏)
向
Card
组件添加一个状态以跟踪它是否打开(默认)或关闭在模板中添加
t-if
以有条件地渲染内容在标题中添加一个按钮,并修改代码以在点击按钮时切换状态