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ầncá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.

Video: Cách sử dụng DevTools

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

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

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.

  1. Modify playground.js so that it acts as a counter like in the example above. Keep Playground 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.

  2. Trong cùng một thành phần, tạo một phương thức increment.

  3. 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.

  4. 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ó:

  1. Trích xuất mã bộ đếm từ thành phần Playground thành thành phần Counter mới.

  2. 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.

  3. Use <Counter/> in the template of the Playground component to add two counters in your playground.

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

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: titlecontent. 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>
  1. Tạo thành phần Card

  2. Nhập nó vào Playground và hiển thị một vài thẻ trong mẫu của nó

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

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.

  1. Cập nhật Card để sử dụng t-out

  2. Cập nhật Playground để nhập markup và sử dụng nó trên một số giá trị html

  3. Đả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.

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

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ó: titlecontent. 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.

  1. Thêm xác thực đạo cụ vào thành phần Card.

  2. Đổ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.

  1. 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ăng onChange tùy chọn.

  2. Cập nhật thành phần Counter để gọi prop onChange (nếu nó tồn tại) bất cứ khi nào nó được tăng lên.

  3. 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ó

  4. Triển khai phương thức incrementSum trong Playground

  5. 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ụ.

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

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 }
  1. Create a TodoList and a TodoItem components.

  2. Thành phần TodoItem sẽ nhận được todo làm chỗ dựa và hiển thị iddescription của nó trong div.

  3. 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 }]);
    
  4. Use t-foreach to display each todo in a TodoItem.

  5. Display a TodoList in the playground.

  6. Add props validation to TodoItem.

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

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.

  1. Thêm các lớp Bootstrap text-mutedtext-trang trí-line-through trên phần tử gốc TodoItem nếu nó được hoàn thành.

  2. 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).

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

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.

  1. Remove the hardcoded values in the TodoList component:

    this.todos = useState([]);
    
  2. 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.

  3. Thêm một trình xử lý sự kiện vào sự kiện keyup có tên addTodo.

  4. 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.

  5. 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.

  6. Điểm thưởng: không làm gì nếu đầu vào trống.

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

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!

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

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-refuseRef. Ý 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);
   });
}
  1. 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ần TodoList (lưu ý rằng có một phương thức focus trên phần tử html đầu vào).

  2. Điểm thưởng: trích xuất mã thành một hook useAutofocus trong một awesome_owl/ mới tập tin utils.js.

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

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.

  1. 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ái isCompleted 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ả.

  2. Thêm đạo cụ gọi lại toggleState vào TodoItem.

  3. Add a change event handler on the input in the TodoItem component and make sure it calls the toggleState function with the todo id.

  4. Lam cho no hoạt động!

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

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.

  1. Thêm một lệnh gọi lại mới removeTodo trong TodoItem.

  2. Chèn <span class="fa fa-remove"/> vào mẫu của thành phần TodoItem.

  3. Bất cứ khi nào người dùng nhấp vào nó, nó sẽ gọi phương thức removeTodo.

  4. 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);
}
../../../_images/delete_todo.png

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í:

  1. Remove the content prop.

  2. Use the default slot to define the body.

  3. Insert a few cards with arbitrary content, such as a Counter component.

  4. (bonus) Add prop validation.

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

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ó)

  1. 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ông

  2. Thêm t-if vào mẫu để hiển thị nội dung có điều kiện

  3. Thê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

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