Chapter 1: Owl components¶
本章介绍了 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
类的子类。
To get started, you need a running Odoo server and a development environment setup. Before getting into the exercises, make sure you have followed all the steps described in this tutorial introduction.
小技巧
如果您使用 Chrome 作为您的网络浏览器,您可以安装 Owl Devtools
扩展。这个扩展提供了许多功能,帮助您理解和分析任何 Owl 应用程序。
In this chapter, we use the awesome_owl
addon, which provides a simplified environment that
only contains Owl and a few other files. The goal is to learn Owl itself, without relying on Odoo
web client code.
The solutions for each exercise of the chapter are hosted on the official Odoo tutorials repository. It is recommended to try to solve them first without looking at the solution!
示例:一个 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++;
}
}
The Counter
component specifies the name of a template that represents its html. It is written in XML
using the QWeb language:
<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. 显示计数器¶
As a first exercise, let us modify the Playground
component located in
awesome_owl/static/src/
to turn it into a counter. To see the result, you can go to the
/awesome_owl
route with your browser.
Modify
playground.js
so that it acts as a counter like in the example above. KeepPlayground
for the class name. You will need to use the useState hook so that the component is re-rendered whenever any part of the state object that has been read by this component is modified.在同一组件中创建一个
increment
方法。修改
playground.xml
中的模板,以便显示您的计数变量。使用 t-esc 输出数据。在模板中添加一个按钮,并在按钮中指定一个 t-on-click 属性,以便在按钮被点击时触发
increment
方法。
小技巧
The Odoo JavaScript files downloaded by the browser are minified. For debugging purpose, it’s easier when the files are not minified. Switch to debug mode with assets so that the files are not minified.
This exercise showcases an important feature of Owl: the reactivity system.
The useState
function wraps a value in a proxy so Owl can keep track of which component
needs which part of the state, so it can be updated whenever a value has been changed. Try
removing the useState
function and see what happens.
2. Extract Counter
in a sub component¶
For now we have the logic of a counter in the Playground
component, but it is not reusable. Let us
see how to create a sub-component from it:
从
Playground
组件中提取计数器代码到一个新的Counter
组件中。你可以先在同一个文件中完成,但是一旦完成后,请更新你的代码,将
Counter
移动到它自己的文件夹和文件中。从./counter/counter
相对导入它。确保模板在它自己的文件中,文件名相同。Use
<Counter/>
in the template of thePlayground
component to add two counters in your playground.
小技巧
By convention, most components code, template and css should have the same snake-cased name
as the component. For example, if we have a TodoList
component, its code should be in
todo_list.js
, todo_list.xml
and if necessary, todo_list.scss
3. A simple Card
component¶
Components are really the most natural way to divide a complicated user interface into multiple reusable pieces. But to make them truly useful, it is necessary to be able to communicate some information between them. Let us see how a parent component can provide information to a sub component by using attributes (most commonly known as props).
The goal of this exercise is to create a Card
component, that takes two props: title
and content
.
For example, here is how it could be used:
<Card title="'my title'" content="'some content'"/>
The above example should produce some html using bootstrap that look like this:
<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>
Create a
Card
componentImport it in
Playground
and display a few cards in its template
4. Using markup
to display html¶
If you used t-esc
in the previous exercise, then you may have noticed that Owl automatically escapes
its content. For example, if you try to display some html like this: <Card title="'my title'" content="this.html"/>
with this.html = "<div>some content</div>""
,
the resulting output will simply display the html as a string.
In this case, since the Card
component may be used to display any kind of content, it makes sense
to allow the user to display some html. This is done with the
t-out directive.
However, displaying arbitrary content as html is dangerous, it could be used to inject malicious code, so
by default, Owl will always escape a string unless it has been explicitely marked as safe with the markup
function.
Update
Card
to uset-out
Update
Playground
to importmarkup
, and use it on some html valuesMake sure that you see that normal strings are always escaped, unlike markuped strings.
注解
The t-esc
directive can still be used in Owl templates. It is slightly faster than t-out
.
5. Props validation¶
The Card
component has an implicit API. It expects to receive two strings in its props: the title
and the content
. Let us make that API more
explicit. We can add a props definition that will let Owl perform a validation step in dev mode. You can activate the dev mode in the App
configuration (but it is activated by default
on the awesome_owl
playground).
对于每个组件进行属性验证是一个好的实践。
Add props validation to the
Card
component.Rename the
title
props into something else in the playground template, then check in the Console tab of your browser’s dev tools that you can see an error.
6. The sum of two Counter
¶
We saw in a previous exercise that props
can be used to provide information from a parent
to a child component. Now, let us see how we can communicate information in the opposite
direction: in this exercise, we want to display two Counter
components, and below them, the sum of
their values. So, the parent component (Playground
) need to be informed whenever one of
the Counter
value is changed.
This can be done by using a callback prop:
a prop that is a function meant to be called back. The child component can choose to call
that function with any argument. In our case, we will simply add an optional onChange
prop that will
be called whenever the Counter
component is incremented.
Add prop validation to the
Counter
component: it should accept an optionalonChange
function prop.Update the
Counter
component to call theonChange
prop (if it exists) whenever it is incremented.Modify the
Playground
component to maintain a local state value (sum
), initially set to 2, and display it in its templateImplement an
incrementSum
method inPlayground
Give that method as a prop to two (or more!) sub
Counter
components.
重要
There is a subtlety with callback props: they usually should be defined with the .bind
suffix. See the documentation.
7. A todo list¶
Let us now discover various features of Owl by creating a todo list. We need two components: a
TodoList
component that will display a list of TodoItem
components. The list of todos is a
state that should be maintained by the TodoList
.
For this tutorial, a todo
is an object that contains three values: an id
(number), a description
(string) and a flag isCompleted
(boolean):
{ id: 3, description: "buy milk", isCompleted: false }
Create a
TodoList
and aTodoItem
components.The
TodoItem
component should receive atodo
as a prop, and display itsid
anddescription
in adiv
.For now, hardcode the list of todos:
// in TodoList this.todos = useState([{ id: 3, description: "buy milk", isCompleted: false }]);
Use t-foreach to display each todo in a
TodoItem
.Display a
TodoList
in the playground.Add props validation to
TodoItem
.
小技巧
Since the TodoList
and TodoItem
components are so tightly coupled, it makes
sense to put them in the same folder.
注解
The t-foreach
directive is not exactly the same in Owl as the QWeb python implementation: it
requires a t-key
unique value, so that Owl can properly reconcile each element.
8. Use dynamic attributes¶
For now, the TodoItem
component does not visually show if the todo
is completed. Let us do that by
using a dynamic attributes.
Add the Bootstrap classes
text-muted
andtext-decoration-line-through
on theTodoItem
root element if it is completed.Change the hardcoded
this.todos
value to check that it is properly displayed.
Even though the directive is named t-att
(for attribute), it can be used to set a class
value (and
html properties such as the value
of an input).
小技巧
Owl let you combine static class values with dynamic values. The following example will work as expected:
<div class="a" t-att-class="someExpression"/>
See also: Owl: Dynamic class attributes
9. Adding a todo¶
到目前为止,我们列表中的待办事项是硬编码的。让我们通过允许用户向列表中添加待办事项使其更加有用。
Remove the hardcoded values in the
TodoList
component:this.todos = useState([]);
在任务列表上方添加一个输入框,占位符为 输入新任务。
添加一个 event handler 在
keyup
事件上命名为addTodo
。实现
addTodo
函数来检查是否按下了回车键 (ev.keyCode === 13
),如果是的话,使用输入框当前的内容创建一个新的待办事项,并清空输入框中的所有内容。确保todo具有唯一的id。它可以只是一个计数器,在每个todo上递增。
奖励分:如果输入为空,则不执行任何操作。
另请参阅
Theory: Component lifecycle and hooks¶
So far, we have seen one example of a hook function: useState
. A hook
is a special function that hook into the internals of the component. In the case of
useState
, it generates a proxy object linked to the current component. This is why
hook functions have to be called in the setup
method, and no later!
An Owl component goes through a lot of phases: it can be instantiated, rendered, mounted, updated, detached, destroyed… This is the component lifecycle. The figure above show the most important events in the life of a component (hooks are shown in purple). Roughly speaking, a component is created, then updated (potentially many times), then is destroyed.
Owl provides a variety of built-in hooks functions. All of them have to be called in
the setup
function. For example, if you want to execute some code when your component is mounted, you can use the onMounted
hook:
setup() {
onMounted(() => {
// do something here
});
}
小技巧
All hook functions start with use
or on
. For example: useState
or onMounted
.
10. Focusing the input¶
Let’s see how we can access the DOM with t-ref and useRef. The main idea is that you need to mark
the target element in the component template with a t-ref
:
<div t-ref="some_name">hello</div>
Then you can access it in the JS with the useRef hook.
However, there is a problem if you think about it: the actual html element for a
component does not exist when the component is created. It only exists when the
component is mounted. But hooks have to be called in the setup
method. So, useRef
return an object that contains a el
(for element) key that is only defined when the
component is mounted.
setup() {
this.myRef = useRef('some_name');
onMounted(() => {
console.log(this.myRef.el);
});
}
Focus the
input
from the previous exercise. This this should be done from theTodoList
component (note that there is afocus
method on the input html element).Bonus point: extract the code into a specialized hook
useAutofocus
in a newawesome_owl/utils.js
file.
小技巧
Refs are usually suffixed by Ref
to make it obvious that they are special objects:
this.inputRef = useRef('input');
11. Toggling todos¶
Now, let’s add a new feature: mark a todo as completed. This is actually trickier than one might
think. The owner of the state is not the same as the component that displays it. So, the TodoItem
component needs to communicate to its parent that the todo state needs to be toggled. One classic
way to do this is by adding a callback prop toggleState
.
Add an input with the attribute
type="checkbox"
before the id of the task, which must be checked if the stateisCompleted
is true.小技巧
Owl does not create attributes computed with the
t-att
directive if it evaluates to a falsy value.Add a callback props
toggleState
toTodoItem
.Add a
change
event handler on the input in theTodoItem
component and make sure it calls thetoggleState
function with the todo id.让它工作起来!
12. Deleting todos¶
最后一步是让用户删除待办事项。
Add a new callback prop
removeTodo
inTodoItem
.Insert
<span class="fa fa-remove"/>
in the template of theTodoItem
component.每当用户点击它时,它应该调用
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¶
In a previous exercise, we built
a simple Card
component. But it is honestly quite limited. What if we want
to display some arbitrary content inside a card, such as a sub-component? Well,
it does not work, since the content of the card is described by a string. It would
however be very convenient if we could describe the content as a piece of template.
This is exactly what Owl’s slot system is designed for: allowing to write generic components.
Let us modify the Card
component to use slots:
Remove the
content
prop.Use the default slot to define the body.
Insert a few cards with arbitrary content, such as a
Counter
component.(bonus) Add prop validation.
14. Minimizing card content¶
Finally, let’s add a feature to the Card
component, to make it more interesting: we
want a button to toggle its content (show it or hide it)
Add a state to the
Card
component to track if it is open (the default) or notAdd a
t-if
in the template to conditionally render the contentAdd a button in the header, and modify the code to flip the state when the button is clicked