The Odoo Web Library (OWL)¶
Odoo v14 giới thiệu framework javascript mới được gọi là OWL (viết tắt của Odoo Web Library), là UI framework được thiết kế dựa trên component và sử dụng QWeb template để làm cấu trúc. OWL hoạt động tối ưu hơn so với hệ thống widget cũ của Odoo và bổ sung thêm rất nhiều các tính năng mới như hooks, reacitvity, tự động tạo các subcomponent và còn nhiều hơn nữa. Trong phần này, chúng ta sẽ tìm hiểu cách sử dụng dụng OWL component để tạo các phần tử giao diện, bắt đầu từ cách tạo một OWL component cơ bản cho đến vòng đời của mỗi của component, cuối cùng là sử dụng OWL để tạo widget cho trường trên giao diện form. Chúng ta sẽ đi vào các đầu mục:
Note
Bạn có thể tự hỏi: Tại sao Odoo lại không sử dụng các JavaScript phổ biến như React.js hay Vue.js? Bạn có thể tìm được câu trả lời cùng với các thông tin khác về OWL framework tại OWL Github.
Tạo OWL component¶
Mục đính của phần này là để tìm hiểu về các thành phần cơ bản của một OWL component. Để lấy ví dụ, chúng ta sẽ tạo một thanh thông báo đơn giản để hiển thị lên giao diện của Odoo Web Client.
Các bước thực hiện¶
Bố sung file
/viin_education/static/src/js/component.js
và khai báo module mới:odoo.define('my.component', function (require) { "use strict"; // Our code will be here });
Bố sung file
/viin_education/static/src/js/component.js
tới assets phía backend:<template id="assets_backend" inherit_id="web.assets_backend"> <xpath expr="." position="inside"> <script src="/viin_education/static/src/js/component.js" type="text/javascript" /> </xpath> </template>
Import các thư viện sẽ sử dụng tại file
/viin_education/static/src/js/component.js
:const { Component } = owl; const { xml } = owl.tags;
Tạo component mới bằng cách mở
Component
và bổ sung thêm template:class MyComponent extends Component { static template = xml` <div class="bg-primary text-center p-3"> Hello, World </div>` }
Khởi tạo và bổ sung component tới phía web client:
owl.utils.whenReady().then(() => { const app = new MyComponent(); app.mount(document.body); });
Khởi động lại server và cập nhật module viin_education
để áp dụng các thay đổi. Sau khi module được nạp tới Odoo, bạn sẽ thấy thanh thông báo hiển thị như hình:
Cơ chế hoạt động¶
Tại bước 1 và bước 2, chúng ta tạo file JS và bổ sung file tới assets phía backend. Nếu bạn muốn tìm hiểu thêm về assets của odoo, bạn có thể tham khảo thêm tại CMS Website Development.
Tại bước 3, chúng ta import các thư viện tiện ích của OWL thông qua biến toàn cục owl
. Trong ví dụ, chúng ta import thư viện Component
và xml
để phục vụ cho việc tạo component mới.
Tại bước 4, chúng ta tạo component MyComponent
bằng cách mở rộng từ class Component
. Để đơn giản ví dụ, chúng ta định nghĩa QWeb template của class MyComponent
ngay trên chính class đó thông qua xml
. Tuy nhiên chúng ta hoàn toàn có thể nạp template của component từ file riêng.
Note
QWeb template định nghĩa trực tiếp trên file JS không hỗ trợ dịch cũng như chỉnh sửa thông qua kế thừa. Bởi vậy phải luôn ưu tiên việc định nghĩa các QWeb template trên file riêng.
Tại bước 5, chúng ta khởi tạo component MyComponent
và thêm vào body
. Bởi OWL component là class viết theo chuẩn ES6 nên bạn có thể tạo object mới của class đó thông qua từ khóa new
. Sau đó chúng ta sử dụng phương thức mount()
để thêm component tới trang. Nếu bạn để ý, các đoạn mã phía trên được đặt trong callback whenReady()
. Việc này sẽ giúp đảm bảo OWL sẽ luôn được sẵn sàng trước khi nạp các component khác.
Mở rộng¶
OWL là một thư viện riêng và được sử dung như một thư viện JavaScript ngoài, do vậy bạn hoàn toàn có thể sử dụng OWL tại các dự án riêng khác của bạn. Thư viện được cung cấp tại OWL Github. Ngoài ra OWL cũng cung cấp trang để bạn trải nghiệm trước khi cài đặt thiết lập trên hệ thống của bạn tại OWL Playground.
Quản lý hành động người dùng trong OWL component¶
Để làm tăng tính tương tác của giao diện thì các component sẽ phải xử lý được hành động của người dùng như click, hover hoặc gửi form. Trong phần này, chúng ta sẽ tìm hiểu cách để bổ sung thêm nút đóng thanh thông báo phía trên.
Các bước thực hiện¶
Cập nhật bổ sung nút đóng tới QWeb template tại
/viin_education/static/src/js/component.js
:static template = xml` <div class="bg-primary text-center p-3"> Hello, World <button type="button" class="btn btn-danger" t-on-click="onClose">Close Me</button> </div>`
Bổ sung thêm phương thức
onClose
để bắt sự kiện khi ấn nút:class MyComponent extends Component { static template = xml` <div class="bg-primary text-center p-3"> Hello, World <button type="button" class="btn btn-danger" t-on-click="onClose">Close Me</button> </div>` onClose(ev) { this.destroy(); } }
Cập nhật module để áp dụng thay đổi. Sau khi cập nhật, bạn sẽ thấy nút đóng trên thanh thông báo hiển thị như trên hình:
Thanh thông báo sẽ được tự động được đóng khi bạn ấn nút và sẽ xuất hiện trở lại khi bạn reload trang.
Cơ chế hoạt động¶
Tại bước 1, chúng ta bổ sung thêm nút đóng tới component. Tại nút chúng ta bổ sung thêm thuộc tính t-on-click
dùng để gắn tới sự kiện click của người dùng, trong đó giá trị của thuộc tính sẽ là phương thức mà sự kiện đó sẽ gọi tới. Như tại ví dụ chúng ta thiết lập thuộc tính cho nút t-on-click="onClose"
có nghĩa là khi nút được click thì hàm onClose
sẽ được gọi. Cú pháp tổng quan của thuộc tính bắt sự kiện là:
t-on-<tên sự kiện>="<tên phương thức trong component>"
Giả sử chúng ta muốn bắt sự kiện người dùng trỏ chuột tới thì cú pháp sẽ là:
t-on-mouseover="onMouseover"
Tại bước 2, chúng ta bổ sung thêm phương thức onClose
mà sẽ được gọi khi người dùng click tới nút. Trong phương thức này, chúng ta gọi tới phương thức destroy()
là phương thức mặc định của OWL component được dùng để xóa bỏ component đó trong DOM. Ngoài ra còn nhiều phương thức mặc định khác của OWL componet mà chúng ta sẽ được tìm hiểu tại các phần sau.
Mở rộng¶
Các sự kiện được bắt trong OWL không chỉ giới hạn bởi các sự kiện trong DOM mà còn có thể là sự kiện do chính bạn tạo ra. Giả sự bạn kích hoạt thủ công sự kiện có tên là custom-event
thì bạn có thể sử dụng t-on-custom-event
để bắt sự kiện đó.
Quản lý trạng thái của thành phần OWL¶
OWL là framework có khả năng tự động cập nhật UI dựa trên các hook. Thông qua việc cập nhật các hook, các component trên giao diện sẽ được tự động cập nhật lại khi trạng thái của chúng thay đổi. Trong phần này, chúng ta sẽ tìm hiểu cách cập nhật nội dung thông báo dựa trên hành động người dùng.
Các bước thực hiện¶
Tại phần này, chúng ta sẽ hiển thị một chữ số may mắn bất kỳ trên thông báo. Người dùng có thể sinh lại số đó thông qua nút Try Again
bên cạnh.
Cập nhật XML template của component để hiện thị thông báo và nút sinh số mới:
static template = xml` <div class="bg-primary text-center p-3"> Your lucky number today is <t t-esc="state.luckyNumber" />. <button type="button" class="btn btn-secondary ml-3" t-on-click="onClick">Try Again</button> <button type="button" class="btn btn-danger ml-3" t-on-click="onClose">Close Me</button> </div>`
Import
useState
hook tới file JavaScript:const { Component, useState } = owl;
Bổ sung phương thức
constructor
tới component để khởi tạo:constructor() { super(...arguments); this.state = useState({ luckyNumber: this.getLuckyNumber() }); } getLuckyNumber() { return Math.floor(Math.random() * 100); }
Bổ sung các phương thức để bắt sự kiện của người dùng:
onClick() { this.state.luckyNumber = this.getLuckyNumber(); }
Khởi động lại và cập nhật module để áp dụng các thay đổi. Sau khi cập nhật, bạn sẽ thấy thanh thông báo như hình:
Khi bạn ấn nút Try Again
, số trên thông báo sẽ được tự động thay đổi thành số mới.
Cơ chế hoạt động¶
Tại bước 1, chúng ta cập nhật lại XML template với một chút sự thay đổi. Tại dòng thông báo, chúng ta không chỉ sử dụng chữ thuần mà đã có thêm biến state.luckyNumber
được sử dụng thông qua thẻ <t t-esc="state.luckyNumber" />
. Tại đây chúng ta cũng bổ sung thêm nút “Try Again” cùng với thuộc tính t-on-click
để gắn sự kiện tới nút.
Tại bước 2, chúng ta nạp useState
hook từ OWL. Hook này được sử dụng để quản lý các trạng thái của component.
Tại bước 3, chúng ta bổ sung thêm constructor. Constructor này sẽ được gọi khi bạn tạo một instance của object. Trong constructor này, chúng ta bổ sung thêm biến luckyNumber
thông qua hàm useState
để quản lý trạng thái của component. Khi giá trị của biến luckyNumber
được thay đổi thông qua việc click nút thì thông báo trên giao diện cũng được đổi theo.
Important
Chỉ có một quy định duy nhất cho việc khai báo hook là các hook sẽ chỉ hoạt động khi bạn khai báo trong constructor. Bạn có thể tham khảo các kiểu hook khác tại OWL Hook.
Tại bước 4, chúng ta bổ sung thêm phương thức để xử lý sự kiện click tới nút. Khi nút được click, chúng ta sẽ thay đổi trạng thái của component. Do chúng ta sử dụng hook trên trạng thái nên giao diện của component sẽ được tự động cập nhật.
Quá trình hoạt động của OWL component¶
OWL cung cấp rất nhiều phương thức khác nhau để hỗ trợ cho lập trình viên để tạo ra các component mạnh mẽ và mang tính tương tác. Trong phần này, chúng ta sẽ đề cập tới một số các phương thức quan trọng và thời điểm mà chúng sẽ được gọi trong suốt quá trình hoạt động của một component.
Các bước thực hiện¶
Tại hàm
constructor
, chúng ta bổ sung việc xuất tin nhắn ra console:constructor() { console.log('Called from constructor() !'); ... }
Bổ sung phương thức
willStart
tới component:async willStart() { console.log('Called from willStart() !'); }
Bổ sung phương thức
mounted
tới component:mounted() { console.log('Called from mounted() !'); }
Bổ sung phương thức
willPatch
tới component:willPatch() { console.log('Called from willPatch() !'); }
Bổ sung phương thức
patched
tới component:patched() { console.log('Called from patched() !'); }
Bổ sung phương thức
willUnmount
tới component:willUnmount() { console.log('Called from willUnmount() !'); }
Khởi động lại và cập nhật module để áp dụng các thay đổi. Sau khi cập nhật, thực hiện các hành động như ấn nút Try Again
và ấn nút Close Me
rồi lọc các dòng log có chức Called
trên giao diện của console. Lúc này giao diện console sẽ hiển thị như phía dưới:
Cơ chế hoạt động¶
Trong phần này, chúng ta bổ sung thêm rất nhiều phương thức khác nhau và log lại tin nhắn trên console khi gọi tới phương thức đó. Mỗi phương thức sẽ được sử dụng tại các thời điểm khác nhau dựa trên yêu cầu của bạn.
constructor
: Constructor được gọi đầu tiên trong suốt quá trình hoạt động của một component. Hàm sẽ được gọi khi bạn khởi tạo component. Bạn sẽ phải khởi tạo trạng thái của component tại đây.
willStart()
: Phương thức willStart
được gọi ngay sau constructor và trước khi phần tử được kết xuất. Đây là phương thức bất đồng bộ. Bạn có thể thực hiện các tiến trình bất đồng bộ ví dụ như RPC tại đây.
mounted()
: Phương thức mounted
được gọi sau khi component được kết xuất và thêm vào DOM.
willPatch()
: Phương thức willPatch
sẽ được gọi khi trạng thái của component được thay đổi. Phương thức này sẽ được gọi trước khi phần tử được kế xuất lại dựa trên trạng thái mới. Như trong ví dụ, phương thức sẽ được gọi khi bạn click nút Try again
. Lưu ý tại đây là khi phương thức này được gọi, các giá trị trên DOM sẽ là giá trị cũ.
patched()
: Phương thức patched
hoạt động tương tự như phương thức willPatch
. Phương thức sẽ được gọi khi trạng thái của component được thay đổi. Điều khác biệt duy nhất tại đây là phương thức patched
sẽ được gọi sau khi phần tử được kết xuất lại dựa trên trạng thái mới.
willUnmount
: Phương thức willUnmount
sẽ được gọi ngay trước khi phần tử được loại bỏ khỏi DOM. Tại ví dụ trên, phương thức này sẽ được gọi khi bạn click nút Close Me
để đóng thông báo.
Đây là gần như toàn bộ các phương thức cho suốt quá trình hoạt động của một component và chúng sẽ được sử dụng tùy theo mỗi nhu cầu sử dụng khác nhau của người dùng, ví dụ như phương thức mounted
và willUnmounted
có thể được sử dụng để gắn hoặc bỏ gắn các phần tử lắng nghe sự kiện.
Mở rộng¶
Chúng ta vẫn còn một phương thức nữa không được đề cập là willUpdateProps
, được sử dụng khi bạn sử dụng các subcomponent. Phương thức này được gọi khi component gốc truyền trạng thái tới các subcomponent thông qua tham số props
. Đây là phương thức bất đồng bộ, do đó bạn có thể sử dụng được các phương thức bất đồng bộ tại đây như RPC.
Bổ sung trường OWL tới form view¶
Qua các phần trên, chúng ta đã nắm được tất cả các điều cơ bản của OWL. Bây giờ chúng ta sẽ đi tới mảng nâng cao hơn là tạo widget cho trường trên form view, tương tự như việc tạo widget cho trường tại phần trước. Trong phần này, chúng ta sẽ tạo một thanh kéo thả giá trị, cho phép người dùng có thể kéo thanh để thiết lập giá trị cho trường đó.
Các bước thực hiện¶
Khai báo trường
rating
tới modeleducation.student
:rating = fields.Float(string='Rating')
Bổ sung trường lên giao diện cùng với tên widget:
<field name="rating" widget="float_range" />
Bổ sung QWeb template tới trường tại
static/src/xml/qweb_template.xml
:<?xml version="1.0" encoding="UTF-8"?> <templates> <t t-name="OWLFloatRangeInput" owl="1"> <input class="o_float_range_input" type="range" min="0" max="10" step="0.1" t-att-value="props.range_value" t-on-input="inputRange" t-attf-title="There're {{ props.student_count }} students with same rating." /> </t> <span class="o_float_range" t-name="OWLFieldFloatRange" t-on-range-updated="rangeUpdated" owl="1"> <FloatRangeInput range_value="value" student_count="ratingGroupData[value]" /> <span class="o_float_range_value"> <t t-esc="value"/> </span> </span> </templates>
Bổ sung file QWeb tới
manifest
của module:'qweb': [ 'static/src/xml/qweb_template.xml', ],
Bổ sung file SCSS tới
static/src/scss/field_widget.scss
:.o_float_range { display: flex !important; flex-direction: row; align-items: center; } .o_float_range_value { margin-left: 15px; }
Bổ sung thêm file
static/src/js/field_widget.js
:odoo.define('float_range', function(require) { "use strict;" const { Component } = owl; const AbstractField = require('web.AbstractFieldOwl'); const fieldRegistry = require('web.field_registry_owl'); // Our code will go here }
Tại file
field_widget.js
, bổ sung thêm componentFloatRangeInput
:class FloatRangeInput extends Component { static template = 'OWLFloatRangeInput'; inputRange(ev) { this.trigger('range-updated', { value: ev.target.value, }); } }
Tại file
field_widget.js
, bổ sung thêm widget mới bằng cách mở rộngAbstractField
:class FieldFloatRange extends AbstractField { static supportedFieldTypes = ['float']; static template = 'OWLFieldFloatRange'; // Our code will go here } fieldRegistry.add('float_range', FieldFloatRange);
Bổ sung các phương thức tới class
FieldFloatRange
constructor(...args) { super(...args); } async willStart() { this.ratingGroupData = {}; var ratingData = await this.rpc ({ model: this.model, method: 'read_group', domain: [], fields: ['rating'], groupBy: ['rating'], }); ratingData.forEach(res => { this.ratingGroupData[res.rating] = res.rating_count; }) } rangeUpdated(ev) { this._setValue(ev.detail.value); }
Bổ sung các file JavaScript và SCSS tới asset phía backend:
<template id="assets_backend" inherit_id="web.assets_backend"> <xpath expr="." position="inside"> <script src="/viin_education/static/src/js/field_widget.js" type="text/javascript" /> <link href="/viin_education/static/src/scss/field_widget.scss" rel="stylesheet" type="text/scss" /> </xpath> </template>
Khởi động lại và cập nhật module để áp dụng các thay đổi. Tại giao diện form của thông tin học sinh sẽ được hiển thị như hình:
Cơ chế hoạt động¶
Tại bước 1, chúng ta bổ sung thêm trường float tới model education.student
và bổ sung trường lên view tại bước 2.
Tại bước 3, chúng ta bổ sung thêm file QWeb template. Tại đây chúng ta thêm hai template mới tới file nhằm mục đích để lấy ví dụ về việc sử dụng subcomponent. Tại template OWLFieldFloatRange
, chúng ta gọi sử dụng thẻ tag <FloatRangeInput>
để gọi tới subcomponent mà chúng ta sẽ khai báo sau trong JS. Đồng thời tại đây chúng ta truyền thêm hai thuộc tính range_value
và student_count
trong thẻ tag nhằm mục đích truyền các tham số đó xuống subcomponent. Trên template của subcomponent, chúng ta dùng thuộc tính props
để nhận các giá trị truyền từ component phía trên. Ngoài ra tại đây chúng ta cũng bổ sung thêm t-on-range-updated
trên template để lắng nghe các sự kiện được kích hoạt tại phía subcomponent.
Important
Odoo v14 sử dụng đồng thời hệ thống widget và OWL framework. Do cả hai đều sử dụng QWeb template nên bạn sẽ phải sử dụng
owl="1"
để phân biệt các OWL template với QWeb template cũ.
Tại bước 4, chúng ta bổ sung QWeb template tới manifest để chúng có thể được nạp tới trình duyệt.
Tại bước 5, chúng ta bổ sung thêm SCSS để điều chỉnh giao diện cho widget.
Tại bước 6, chúng ta bổ sung thêm JavaScript tới component của trường. Tại đây chúng ta import các thư viện của OWL, AbstractField
và fieldRegistry
. AbstractField
là component OWL trừu tượng của trường, trong đó bao gồm tất cả các phần tử cơ bản cần thiết để tạo nên một trường. fieldRegistry
được sử dụng để làm danh sách các component OWL được dùng cho các trường.
Tại bước 7, chúng ta tạo mới component FloatRangeInput
. Trong component, chúng ta sử dụng biến template
để nạp template từ file XML bên ngoài. Trên component chúng ta cũng thêm phương thức inputRange
được gọi khi người dùng kéo thanh giá trị. Khi phương thức được gọi tới, chúng ta sẽ kích hoạt sự kiện range-updated
để cập nhật lại component gốc thông qua t-on-range-updated
mà chúng ta đã khai báo trên template trước đó.
Tại bước 8 và bước 9, chúng ta tạo component FieldFloatRange
bằng cách mở rộng AbstractField
. Tại đây vì chúng ta khai báo biến static components
nhằm mục đích sử dụng subcomponent tại phía template. Ở phương thức willStart
, do đây là phương thức bất đồng bộ nên chúng ta sẽ sử dụng để gọi RPC lấy các dữ liệu về học sinh. Sau khi người dùng thay đổi giá trị của trường trên thanh kéo, phương thức rangeUpdated
sẽ được gọi để cập nhật giá trị cho trường trên cơ sở dữ liệu thông qua phương thức _setValue
. Giá trị được cập nhật ở đây sẽ được lấy thông qua thuộc tính detail
của biến event. Cuối cùng, chúng ta đăng ký widget tới fieldRegistry
để có thể sử dụng được widget trên các view của odoo.
Tại bước 10, chúng ta bổ sung các file JavaScript và SCSS tới asset phía backend.