Chương 1: Thành phần cú¶
Chương này giới thiệu ``Owl framework <https://github.com/odoo/owl>`_, một hệ thống thành phần được thiết kế riêng cho SoOn. Các khối xây dựng chính của OWL là các thành phần và các mẫu.
Trong Owl, mọi phần của giao diện người dùng được quản lý bởi một thành phần: chúng chứa logic và xác định các mẫu được sử dụng để hiển thị giao diện người dùng. Trong thực tế, một thành phần được biểu thị bằng một lớp JavaScript nhỏ phân lớp với lớp Thành phần
.
Để bắt đầu, bạn cần có máy chủ SoOn đang chạy và thiết lập môi trường phát triển. Trước khi bắt đầu bài tập, hãy đảm bảo bạn đã làm theo tất cả các bước được mô tả trong tutorial giới thiệu.
Mẹo
Nếu bạn sử dụng Chrome làm trình duyệt web, bạn có thể cài đặt tiện ích mở rộng Owl Devtools
. Tiện ích mở rộng này cung cấp nhiều tính năng để giúp bạn hiểu và lập hồ sơ cho bất kỳ ứng dụng Owl nào.
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.
Giải pháp cho mỗi bài tập của chương được lưu trữ trên kho lưu trữ hướng dẫn chính thức của SoOn. Bạn nên cố gắng giải quyết chúng trước mà không cần nhìn vào lời giải!
Ví dụ: thành phần Counter
¶
Đầu tiên, chúng ta hãy xem một ví dụ đơn giản. Thành phần Counter
được hiển thị bên dưới là thành phần duy trì giá trị số bên trong, hiển thị và cập nhật nó bất cứ khi nào người dùng nhấp vào nút.
/** @odoo-module **/
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++;
}
}
Thành phần Counter
chỉ định tên của mẫu đại diện cho html của nó. Nó được viết bằng XML bằng ngôn ngữ QWeb:
<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. Hiển thị bộ đếm¶

Trong bài tập đầu tiên, chúng ta hãy sửa đổi thành phần Playground
nằm trong awesome_owl/static/src/
để biến nó thành một bộ đếm. Để xem kết quả, bạn có thể truy cập tuyến đường /awesome_owl
bằng trình duyệt của mình.
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.Trong cùng một thành phần, tạo một phương thức
increment
.Sửa đổi mẫu trong
playground.xml
để nó hiển thị biến bộ đếm của bạn. Sử dụng t-esc để xuất dữ liệu.Thêm một nút trong mẫu và chỉ định thuộc tính t-on-click trong nút để kích hoạt phương thức
increment
bất cứ khi nào nút được nhấp vào.
Quan trọng
Đừng quên /** @odoo-module **/
trong tệp JavaScript của bạn. Bạn có thể tìm thêm thông tin về điều này tại đây.
Mẹo
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.
Bài tập này giới thiệu một tính năng quan trọng của Owl: hệ thống phản ứng. Hàm useState
bao bọc một giá trị trong proxy để Owl có thể theo dõi thành phần nào cần phần nào của trạng thái, để nó có thể được cập nhật bất cứ khi nào giá trị được thay đổi. Hãy thử xóa hàm useState
và xem điều gì sẽ xảy ra.
2. Trích xuất Counter
trong thành phần phụ¶
Hiện tại, chúng ta có logic của bộ đếm trong thành phần Playground
, nhưng nó không thể sử dụng lại được. Chúng ta hãy xem cách tạo một thành phần phụ từ nó:
Trích xuất mã bộ đếm từ thành phần
Playground
thành thành phầnCounter
mới.Trước tiên, bạn có thể thực hiện việc đó trong cùng một tệp, nhưng sau khi hoàn tất, hãy cập nhật mã của bạn để di chuyển
Bộ đếm
trong thư mục và tệp riêng của nó. Nhập nó tương đối từ./counter/counter
. Đảm bảo mẫu nằm trong tệp riêng, có cùng tên.Use
<Counter/>
in the template of thePlayground
component to add two counters in your playground.

Mẹo
Theo quy ước, hầu hết các thành phần mã, mẫu và css phải có cùng tên dạng rắn với thành phần. Ví dụ: nếu chúng ta có thành phần TodoList
, mã của nó phải ở dạng todo_list.js
, todo_list.xml
và nếu cần, todo_list.scss
3. Thành phần Card
đơn giản¶
Các thành phần thực sự là cách tự nhiên nhất để chia giao diện người dùng phức tạp thành nhiều phần có thể tái sử dụng. Nhưng để làm cho chúng thực sự hữu ích, cần có khả năng truyền đạt một số thông tin giữa chúng. Chúng ta hãy xem cách thành phần chính có thể cung cấp thông tin cho thành phần phụ bằng cách sử dụng các thuộc tính (thường được gọi là props <https://github.com/odoo/owl/blob/master/doc/reference/props.md>
_).
Mục tiêu của bài tập này là tạo thành phần Card
, có hai props: title
và content
. Ví dụ: đây là cách nó có thể được sử dụng:
<Card title="'my title'" content="'some content'"/>
Ví dụ trên sẽ tạo ra một số html bằng cách sử dụng bootstrap trông như thế này:
<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>
Tạo thành phần
Card
Nhập nó vào
Playground
và hiển thị một vài thẻ trong mẫu của nó

4. Sử dụng markup
để hiển thị 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.
Trong trường hợp này, vì thành phần Card
có thể được sử dụng để hiển thị bất kỳ loại nội dung nào, nên việc cho phép người dùng hiển thị một số html là điều hợp lý. Việc này được thực hiện bằng lệnh t-out.
Tuy nhiên, việc hiển thị nội dung tùy ý dưới dạng html rất nguy hiểm, nó có thể được sử dụng để chèn mã độc vào, nên theo mặc định, Owl sẽ luôn thoát khỏi một chuỗi trừ khi chuỗi đó được đánh dấu rõ ràng là an toàn bằng chức năng markup
.
Cập nhật
Card
để sử dụngt-out
Cập nhật
Playground
để nhậpmarkup
và sử dụng nó trên một số giá trị htmlĐảm bảo rằng bạn thấy các chuỗi thông thường luôn được thoát, không giống như các chuỗi được đánh dấu.
Ghi chú
Lệnh t-esc
vẫn có thể được sử dụng trong các mẫu Owl. Nó nhanh hơn một chút so với t-out
.

5. Xác thực đạo cụ¶
Thành phần Card
có API ẩn. Nó dự kiến sẽ nhận được hai chuỗi trong props của nó: title
và content
. Hãy để chúng tôi làm cho API đó rõ ràng hơn. Chúng ta có thể thêm định nghĩa đạo cụ cho phép Owl thực hiện bước xác thực ở chế độ dev. Bạn có thể kích hoạt chế độ dev trong Cấu hình ứng dụng (nhưng nó được kích hoạt theo mặc định trên ` sân chơi awesome_owl`).
Thực hành tốt là thực hiện xác thực đạo cụ cho mọi thành phần.
Thêm xác thực đạo cụ vào thành phần
Card
.Đổi tên đạo cụ
title
thành một tên khác trong mẫu sân chơi, sau đó kiểm tra tab Console của công cụ phát triển trên trình duyệt của bạn và bạn có thể thấy lỗi.
6. Tổng của hai Counter
¶
Chúng ta đã thấy trong bài tập trước rằng props
có thể được sử dụng để cung cấp thông tin từ thành phần cha mẹ đến thành phần con. Bây giờ, chúng ta hãy xem cách chúng ta có thể truyền đạt thông tin theo hướng ngược lại: trong bài tập này, chúng ta muốn hiển thị hai thành phần Bộ đếm
và bên dưới chúng là tổng giá trị của chúng. Vì vậy, thành phần gốc (Playground
) cần được thông báo bất cứ khi nào một trong các giá trị Counter
bị thay đổi.
Điều này có thể được thực hiện bằng cách sử dụng callback prop: một prop là một hàm có nghĩa là được gọi lại. Thành phần con có thể chọn gọi hàm đó với bất kỳ đối số nào. Trong trường hợp của chúng tôi, chúng tôi sẽ chỉ thêm một prop onChange
tùy chọn sẽ được gọi bất cứ khi nào thành phần Counter
được tăng lên.
Thêm xác thực prop vào thành phần
Counter
: nó phải chấp nhận prop chức năngonChange
tùy chọn.Cập nhật thành phần
Counter
để gọi proponChange
(nếu nó tồn tại) bất cứ khi nào nó được tăng lên.Sửa đổi thành phần
Playground
để duy trì giá trị trạng thái cục bộ (sum
), ban đầu được đặt thành 2 và hiển thị nó trong mẫu của nóTriển khai phương thức
incrementSum
trongPlayground
Cung cấp phương thức đó làm chỗ dựa cho hai (hoặc nhiều hơn!) thành phần
Counter
phụ.

Quan trọng
There is a subtlety with callback props: they usually should be defined with the .bind
suffix. See the documentation.
7. Danh sách việc cần làm¶
Bây giờ chúng ta hãy khám phá các tính năng khác nhau của Owl bằng cách tạo danh sách việc cần làm. Chúng ta cần hai thành phần: thành phần TodoList
sẽ hiển thị danh sách các thành phần TodoItem
. Danh sách việc cần làm là trạng thái cần được duy trì bởi TodoList
.
Đối với hướng dẫn này, todo
là một đối tượng chứa ba giá trị: id
(số), description
(chuỗi) và cờ isCompleted
(boolean):
{ id: 3, description: "buy milk", isCompleted: false }
Create a
TodoList
and aTodoItem
components.Thành phần
TodoItem
sẽ nhận đượctodo
làm chỗ dựa và hiển thịid
vàdescription
của nó trongdiv
.Hiện tại, hãy mã hóa danh sách việc cần làm:
// 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
.

Mẹo
Since the TodoList
and TodoItem
components are so tightly coupled, it makes
sense to put them in the same folder.
Ghi chú
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. Sử dụng thuộc tính động¶
Hiện tại, thành phần TodoItem
không hiển thị trực quan nếu todo
được hoàn thành. Hãy để chúng tôi thực hiện điều đó bằng cách sử dụng thuộc tính động.
Thêm các lớp Bootstrap
text-muted
vàtext-trang trí-line-through
trên phần tử gốcTodoItem
nếu nó được hoàn thành.Change the hardcoded
this.todos
value to check that it is properly displayed.
Mặc dù lệnh này có tên là t-att
(dành cho thuộc tính), nhưng nó có thể được sử dụng để đặt giá trị class
(và các thuộc tính html chẳng hạn như value
của đầu vào).

Mẹo
Owl cho phép bạn kết hợp các giá trị lớp tĩnh với các giá trị động. Ví dụ sau sẽ hoạt động như mong đợi:
<div class="a" t-att-class="someExpression"/>
Xem thêm: Owl: Thuộc tính lớp động
9. Thêm việc cần làm¶
Cho đến nay, các việc cần làm trong danh sách của chúng tôi đều được mã hóa cứng. Hãy để chúng tôi làm cho nó hữu ích hơn bằng cách cho phép người dùng thêm việc cần làm vào danh sách.
Remove the hardcoded values in the
TodoList
component:this.todos = useState([]);
Thêm đầu vào phía trên danh sách nhiệm vụ với trình giữ chỗ Nhập nhiệm vụ mới.
Thêm một trình xử lý sự kiện vào sự kiện
keyup
có tênaddTodo
.Triển khai
addTodo
để kiểm tra xem enter có được nhấn hay không (ev.keyCode === 13
) và trong trường hợp đó, hãy tạo một việc cần làm mới với nội dung hiện tại của đầu vào làm mô tả và xóa tất cả đầu vào nội dung.Hãy chắc chắn rằng việc cần làm có một id duy nhất. Nó có thể chỉ là một bộ đếm tăng dần ở mỗi việc cần làm.
Điểm thưởng: không làm gì nếu đầu vào trống.

Xem thêm
Lý thuyết: Vòng đời thành phần và các hook¶
Cho đến nay, chúng ta đã thấy một ví dụ về hàm hook: useState
. hook là một chức năng đặc biệt nối vào các phần bên trong của thành phần. Trong trường hợp useState
, nó tạo ra một đối tượng proxy được liên kết với thành phần hiện tại. Đây là lý do tại sao các hàm hook phải được gọi trong phương thức setup
, và không được muộn hơn!
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 cung cấp nhiều hàm hook tích hợp sẵn <https://github.com/odoo/owl/blob/master/doc/reference/hooks.md>`_. Tất cả chúng phải được gọi trong hàm setup
. Ví dụ: nếu bạn muốn thực thi một số mã khi thành phần của bạn được gắn kết, bạn có thể sử dụng hook onMounted
:
setup() {
onMounted(() => {
// do something here
});
}
Mẹo
Tất cả các hàm hook đều bắt đầu bằng use
hoặc on
. Ví dụ: useState
hoặc onMounted
.
10. Tập trung đầu vào¶
Hãy xem cách chúng ta có thể truy cập DOM bằng t-ref và useRef. Ý tưởng chính là bạn cần đánh dấu phần tử đích trong mẫu thành phần bằng t-ref
:
<div t-ref="some_name">hello</div>
Sau đó, bạn có thể truy cập nó trong JS bằng useRef hook. Tuy nhiên, có một vấn đề nếu bạn nghĩ về nó: phần tử html thực tế cho một thành phần không tồn tại khi thành phần đó được tạo. Nó chỉ tồn tại khi thành phần được gắn kết. Nhưng hook phải được gọi trong phương thức setup
. Vì vậy, useRef
trả về một đối tượng chứa khóa el
(cho phần tử) chỉ được xác định khi thành phần được gắn kết.
setup() {
this.myRef = useRef('some_name');
onMounted(() => {
console.log(this.myRef.el);
});
}
Tập trung vào
đầu vào
từ bài tập trước. Việc này phải được thực hiện từ thành phầnTodoList
(lưu ý rằng có một phương thứcfocus
trên phần tử html đầu vào).Điểm thưởng: trích xuất mã thành một hook
useAutofocus
trong mộtawesome_owl/ mới tập tin utils.js
.

Mẹo
Các tham chiếu thường có hậu tố là Ref
để làm rõ rằng chúng là các đối tượng đặc biệt:
this.inputRef = useRef('input');
11. Chuyển đổi việc cần làm¶
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
.
Thêm đầu vào có thuộc tính
type="checkbox"
trước id của tác vụ, thuộc tính này phải được kiểm tra xem trạng tháiisCompleted
có đúng hay không.Mẹo
Owl không tạo các thuộc tính được tính toán bằng lệnh
t-att
nếu nó đánh giá thành một giá trị giả.Thêm đạo cụ gọi lại
toggleState
vàoTodoItem
.Add a
change
event handler on the input in theTodoItem
component and make sure it calls thetoggleState
function with the todo id.Lam cho no hoạt động!

12. Xóa việc cần làm¶
Bước cuối cùng là cho phép người dùng xóa việc cần làm.
Thêm một lệnh gọi lại mới
removeTodo
trongTodoItem
.Chèn
<span class="fa fa-remove"/>
vào mẫu của thành phầnTodoItem
.Bất cứ khi nào người dùng nhấp vào nó, nó sẽ gọi phương thức
removeTodo
.Lam cho no hoạt động!
Mẹo
Nếu bạn đang sử dụng một mảng để lưu trữ danh sách việc cần làm của mình, bạn có thể sử dụng hàm
splice
của JavaScript để xóa việc cần làm khỏi mảng đó.
// 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. Thẻ
chung có khe cắm¶
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.
Chúng ta hãy sửa đổi thành phần Card
để sử dụng các vị trí:
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.

Xem thêm
14. Tối giản nội dung thẻ¶
Cuối cùng, hãy thêm một tính năng vào thành phần Card
để làm cho nó thú vị hơn: chúng tôi muốn có một nút để chuyển đổi nội dung của nó (hiển thị hoặc ẩn nó)
Thêm trạng thái vào thành phần
Card
để theo dõi xem nó có mở (mặc định) hay khôngThêm
t-if
vào mẫu để hiển thị nội dung có điều kiệnThêm một nút trong tiêu đề và sửa đổi mã để chuyển trạng thái khi nhấp vào nút
