odoo14引入了名为OWL(Odoo Web Library)的JavaScript框架。OWL是以组件为基础的UI框架,通过QWeb模板作为架构。OWL与传统的组件系统相比更快,并引入了一些新的特性,包括hooks、reactivity、the autoinstantiation of subcomponents等。在这章中,我们将学习如何使用OWL创建可交互的UI元素。我们将从最小的OWL组件开始,然后学习组件的生命周期。最后,我们将创建一个新的form视图下的字段控件。本章将包含如下内容:

  • 创建OWL组件
  • 在OWL组件中管理用户动作
  • TODO: Making OWL components reactive
  • 理解OWL的生命周期
  • 向form视图中添加OWL字段

注意
为什么odoo不使用一些比较知名的JavaScript框架,比如React.js、Vue.js呢?你可以在https://github.com/odoo/owl了解到OWL的更多知识。

技术要求

OWL组件是以ES6定义的。在这章中,我们将使用ES6语法。但是一些ES6语法在一些老的浏览器中有问题。请确保使用最新的Chrome或者Firefox浏览器。

创建OWL组件

本节的目标是学习OWL组件的基础知识。我们将创建最小的OWL组件并把它添加到Odoo的Web客户端中。
本节,我们会创建一个小的带有文字的水平条。

准备

本节,我们将使用my_library模块。

步骤

我们将添加一个小的组件,用于展示水平文字的长条。

  1. 添加/my_library/static/src/js/component.js的JavaScript文件并定义新的命名空间:
odoo.define('my.component', function (require) {
	"use strict";
	// Place steps 3, 4, 5 here
});
  1. 添加/my_library/views/tempaltes.xml的项目xml文件并载入js文件:
		在步骤1中创建js文件中新增OWL实用程序const { Component } = owl;
const { xml } = owl.tags;在步骤1中创建的js文件中添加OWL组件及基础的模板:class MyComponent extends Component {
	static template = xml`		
		 Welcome to Odoo 
}初始化组件并添加到网页客户端:owl.utils.whenReady().then(() => {
	const app = new MyComponent();
	app.mount(document.body);
});安装/更新my_library模块应用更改。当我们的模块完成安装后,我们可以看到水平条。这只是一个简单的组件。它不能响应用户事件,你也不能移除他。原理步骤1、步骤2,我们添加了js文件并将其添加到后台资源(assets)中。如果想学习assets的内容,可参考14章、CMS网站开发、静态资源管理。步骤3,我们通过OWL初始化了一个变量。所有通过OWL实例化的变量在全局变量owl中都是可见的。在我们的例子中,我们使用了OWL实例。首先,我们定义了Component,然后通过owl.tags定义了xml。对于OWL组件而言,Component是核心类,通过扩展它,我们可以创建我们自己的组件。步骤4,我们创建了组件,MyComponent。简单起见,我们仅添加了QWeb的模板。如果你观察的比较仔细,可以看到我们使用xml...定义了模板。这就是内联模板(inline template)。然后,你也可以载入QWeb模板。小贴士内联QWeb模板并不支持翻译及通过继承进行修改。因此,尽量使用单独的QWeb文件。步骤5,我们实例化了MyComponent并把它追加到body中。OWL组件是ES6的类,所以你能够通过new创建实体。然后通过mount()函数添加到页面中。我们把我们的代码写在了whenReady()的回调函数中。这可以确保在使用OWL组件前,所有的OWL功能都被加载完成。更多OWL在odoo中是单独加载的库,就像其他的JS库一样。你能够使用OWL构建其他的项目。在OWL组件中管理用户行为为了确保用户接口具有可交互性,组件需要响应用户的点击、悬停及表格的提交。在本节中,我们将添加一个按钮并处理点击事件。准备步骤本节,我们将添加删除按钮。通过点击删除按钮,可以移除组件。如下:更新QWeb模板并添加icon图标。static template = xml`	
		 Welcome to Odoo 
		 `添加onRemove处理函数class MyComponent extends Component {
	static template = xml`		
		 Welcome to Odoo 
	onRemove(ev) {
	this.destroy();
}更新模块后,视图如下:点击移除的图标后,组件将被删除。当刷新页面后,水平条将再次出现。原理步骤1,我们添加了移除的图标,并且添加了t-on-click属性。这将绑定点击事件。属性的值就是响应方法的名称。在我们的例子中,onRemove是我们的响应函数。组件的事件语法如下t-on-=""比如,当我们想当鼠标移至组件上时进行响应,则可以t-on-mouseover="onMouseover"在添加响应代码后,当我们的鼠标悬停在组件上时,OWL将会调用onMouseover方法。步骤2,我们添加了onRemove方法。当我们点击移除图标时调用。在这个方法中,我们调用了destory()方法,这将会移除组件。在destory()函数中,我们接收JavaScript事件对象。destory()是OWL组件的默认方法之一。更多事件并不局限于DOM事件。你可以添加自己的事件。比如,你触发了名为my-custom-event的方法,你可以使用t-on-my-custom-event捕获事件。Making OWL 组件reactiveOWL是一个强有力的框架,可根据钩子自动更新UI。有了更新钩子,当组件的内部状态发生变化后,组件的UI可自动更新。在本节中,我们将更新展示在组件UI中的内容。准备步骤本节中,我们在文本两边添加了箭头的图标。通过点击箭头,我们可以改变文本内容。如下:更新XML的模板。添加两个绑定事件的按钮。可以从列表中动态检索文本。static template = xml`	
	`在JavaScript文件中引入userState钩子:const { Component, useState } = owl;添加constructor方法并初始化一些变量constructor() {
	super(...arguments);
	this.messageList = [
		'Hello World',
		'Welcome to Odoo',
		'Odoo is awesome',
		'You are awesome too'
	this.state = useState({ currentIndex: 0 });
}在组件类中,添加用户点击事件onNext(ev) {
	this.state.currentIndex++;
onPrevious(ev) {
	this.state.currentIndex--;
}更新模块,展示如下:原理步骤1,我们更新了XML模板。我们做了两个改动。我们通过消息的列表渲染文本消息,我们基于在state变量中的currentIndex的值选择消息。我们在文本框两边添加了两个箭头。并通过t-on-click属性绑定了点击事件。步骤2,我们引入了useState钩子。将用于处理组件的状态。步骤3,我们添加了构造函数(constructor)。当我们创建对象实体时,构造函数将会被调用。在构造函数中,我们添加了消息的列表。然后通过useState钩子新增了state的变量。当state变化的时候,UI也将更新。在我们的例子中,我们在useState钩子中使用了currentIndex。当currentIndex变化了,UI也将随之变化。重要信息在定义钩子的时候只有一条规则,只有在构造函数中定义了钩子,钩子才会生效。几个其他钩子可以在https://github.com/odoo/owl/ blob/master/doc/reference/hooks.md详细了解。步骤4,我们添加了箭头的点击事件。通过点击箭头,我们可以改变组件的状态。因为我们再state上使用了钩子,UI也将随之变化。理解OWL的生命周期OWL组件有几个方法帮助开发人员创建强有力的组件。本节,我们将了解组件重要的方法及组件的生命周期。本节,我们添加了几个方法,我们将在console中输出日志以了解组件的生命周期。准备步骤在构造函数(constructor)中添加日志constructor() {
	console.log('CALLED:> constructor');
...添加willStart方法async willStart() {
	console.log('CALLED:> willStart');
}添加mounted方法mounted() {
	console.log('CALLED:> mounted');
}添加willPatch方法willPatch() {
	console.log('CALLED:> willPatch');
}添加patched方法patched() {
	console.log('CALLED:> patched');
}添加willUnmount()方法willUnmount() {
	console.log('CALLED:> willUnmount');
}更新模块后如下图:原理constructor(): 构造函数,最先被调用。将在这里设置组件的初始状态。willStart(): 在构造函数之后,渲染元素之前。这是异步函数,可以进行诸如RPC的异步操作。mounted(): 在元素渲染、DOM添加之后调用。willPatch(): 在组将的状态发生变化之后调用。这个方法将在元素被根据新的状态重新渲染前调用。例如,当我们点击箭头的时候,该函数被调用。但是这时dom依旧是老的值。patched(): 与willPatched()类似。在组件的状态发生变化的时候调用。不同点是,函数在元素基于新的状态渲染后调用。willUnmount(): 在元素被移除前调用。以上是组件的生命周期,你可以根据实际需要编写相应函数。比如,mounted和willUnmount方法可以用来绑定和解绑事件监听。更多还有一个重要的方法,他在你使用子组件的使用调用。OWL传递通过props参数传递父组件的状态给子组件,当props变化的时候,willUpdateProps方法将被调用。这是一个异步方法,意味着你可以进行诸如RPC的异步操作。为form视图添加OWL字段至此,我们学习了OWL的基础知识。现在我们创建一个form视图下的字段展示组件。我们将创建一个颜色部件,通过选择颜色保存数值。为了让例子更丰富,我们使用了OWL的先进理念。我们将创建复杂的组件,用户事件,扩展的QWeb模板等。准备步骤在library.book模型中添加颜色的整数型字段color = fields.Integer()在form视图中添加相同的字段在static/src/xml/qweb_tempalte.xml中添加字段的QWeb模板
        在manifest文件中添加QWeb文件"qweb": [
	'static/src/xml/qweb_template.xml',
],现在我们在static/src/scss/field_widget.scss中添加一些SCSS。文件太长了,可直接在https://github.com/ PacktPublishing/Odoo-13-Development-Cookbook-Fourth- Edition/blob/master/Chapter16/05_owl_field/my_library/ static/src/scss/field_widget.scss中查看。添加static/src/js/field_widget.jsodoo.define('my_field_widget', function (require) {
    "use strict";
    const { Component } = owl;
    const AbstractField = require(
    'web.AbstractFieldOwl');
    const fieldRegistry = require(
    'web.field_registry_owl');
// Place steps 7 and 8 here
});步骤7,添加颜色选择组件class ColorPill extends Component {
    static template = 'OWLColorPill';
    pillClicked() {
    this.trigger('color-updated', {val:
    this.props.pill_no});
}步骤8,扩展AbstractFieldclass FieldColor extends AbstractField {
    static supportedFieldTypes = ['integer'];
    static template = 'OWLFieldColorPills';
    static components = { ColorPill };
    // Add methods from step 9 here
fieldRegistry.add('int_color', FieldColor);步骤9,添加方法constructor(...args) {
    super(...args);
    this.totalColors = Array.from({ length: 10 },
        (_, i) => (i + 1).toString());
async willStart() {
    this.colorGroupData = {};
    var colorData = await this.rpc({
        model: this.model, method: 'read_group',
        domain: [], fields: ['color'],
        groupBy: ['color'],
    colorData.forEach(res => {
        this.colorGroupData[res.color] =
            res.color_count;
colorUpdated(ev) {
    this._setValue(ev.detail.val);
}步骤10,将js、scss文件添加到后台资源。更新模块,如下图这个字段看起来就像上一章中的color小部件,但实际的区别在于它的底层。这个新字段是用OWL构建的,而前一个字段是用小部件构建的。原理步骤1,我们创建了整型字段。步骤2,我们添加到form视图。步骤3,我们添加了QWeb模板。我们添加了两个模板,一个是颜色的选择,另一个是字段本身。我们使用两个模板是为了更好的理解子组件的概念。仔细查看模板,可以发现我们使用了标签。浙江实例化子组件。在标签中,我们传递active和pill_no属性。这些属性的值将通过子组件参数props获取。同时,t-on-color-updated属性被用来监听子组件的自定义事件。重要信息odoo14使用widget系统和OWL框架。两个都使用QWeb模板。为了将OWL QWeb模板与传统的QWeb 模板区分,我们需使用owl="1"来标识步骤4,添加文件到manifest中。步骤5,添加SCSS的样式。步骤6,添加JS。我们引入OWL实用程序,并导入AbstractField和fieldRegistry。AbstractField是抽象的OWL组件。他包含所有基础元素。fieldRegistry被用来展示OWL组件。步骤7,我们创建了ColorPill组件。组件中template变量是从外部XML 文件中加载的模板的名称。ColorPill组件有pillClicked方法,用于用户在颜色上的点击。在方法内部,当我们在FieldColor组件上使用t-on-color-updated的触发的color-updated事件将会被父组件FieldColor组件捕获。步骤8、9,我们创建了FieldColor组件,它是AbstractField的拓展。我们之所以使用AbstractField组件,是因为它具备创建字段小部件的所有要素。我们再一开始使用了components静态变量。当你在模板中使用子组件的时候,你需要通过components静态变量列出所有的组件。我们添加了willStart方法。willStart方法是异步方法,我们可以调用RPC实现获取特定颜色组图书的数量。然后,我们添加了colorUpdated方法,在我们点击是调用。所以,当我们变化了字段的值时。setValue方法将会设置字段的值。注意,由子组件触发的数据,在event参数下的detail属性中是可以访问到的。最后,我们在fieldRegistry中注册了小部件,这意味着今后我们将能够通过表单视图中的小部件属性使用字段。在步骤10中,我们将JavaScript和SCSS文件加载到后端资产中。" _ue_custom_node_="true">