【odoo14】【好书学习】第十六章、odoo web库(OWL)

老韩头的开发日常【好书学习】系列

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文件:
<template id="assets_end" inherit_id="web.assets_backend">
	<xpath expr="." position="inside">
		<script src="/my_library/static/src/js/component.js" type="text/javascript" />
	</xpath>
</template>
  1. 在步骤1中创建js文件中新增OWL实用程序
const { Component } = owl;
const { xml } = owl.tags;
  1. 在步骤1中创建的js文件中添加OWL组件及基础的模板:
class MyComponent extends Component {
	static template = xml`
		<div class="bg-info text-center p-2">
		<b> Welcome to Odoo </b>
		</div>`
}
  1. 初始化组件并添加到网页客户端:
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组件中管理用户行为

为了确保用户接口具有可交互性,组件需要响应用户的点击、悬停及表格的提交。
在本节中,我们将添加一个按钮并处理点击事件。

准备

步骤

本节,我们将添加删除按钮。通过点击删除按钮,可以移除组件。如下:

  1. 更新QWeb模板并添加icon图标。
static template = xml`
	<div class="bg-info text-center p-2">
		<b> Welcome to Odoo </b>
		<i class="fa fa-close p-1 float-right" style="cursor: pointer;" t-on-click="onRemove"> </i>
</div>`
  1. 添加onRemove处理函数
class MyComponent extends Component {
	static template = xml`
		<div class="bg-info text-center p-2">
		<b> Welcome to Odoo </b>
		<i class="fa fa-close p-1 float-right"
		style="cursor: pointer;"
		t-on-click="onRemove"> </i>
		</div>`
	onRemove(ev) {
	this.destroy();
	}
}

更新模块后,视图如下:

点击移除的图标后,组件将被删除。当刷新页面后,水平条将再次出现。

原理

步骤1,我们添加了移除的图标,并且添加了t-on-click属性。这将绑定点击事件。属性的值就是响应方法的名称。在我们的例子中,onRemove是我们的响应函数。组件的事件语法如下

t-on-<name of event>="<method name in component>"

比如,当我们想当鼠标移至组件上时进行响应,则可以

t-on-mouseover="onMouseover"

在添加响应代码后,当我们的鼠标悬停在组件上时,OWL将会调用onMouseover方法。
步骤2,我们添加了onRemove方法。当我们点击移除图标时调用。在这个方法中,我们调用了destory()方法,这将会移除组件。在destory()函数中,我们接收JavaScript事件对象。destory()是OWL组件的默认方法之一。

更多

事件并不局限于DOM事件。你可以添加自己的事件。比如,你触发了名为my-custom-event的方法,你可以使用t-on-my-custom-event捕获事件。

Making OWL 组件reactive

OWL是一个强有力的框架,可根据钩子自动更新UI。有了更新钩子,当组件的内部状态发生变化后,组件的UI可自动更新。在本节中,我们将更新展示在组件UI中的内容。

准备

步骤

本节中,我们在文本两边添加了箭头的图标。通过点击箭头,我们可以改变文本内容。如下:

  1. 更新XML的模板。添加两个绑定事件的按钮。可以从列表中动态检索文本。
static template = xml`
	<div class="bg-info text-center p-2">
		<i class="fa fa-arrow-left p-1"
		style="cursor: pointer;"
		t-on-click="onPrevious"> </i>
		<b t-esc="messageList[Math.abs(
		state.currentIndex%4)]"/>
		<i class="fa fa-arrow-right p-1"
		style="cursor: pointer;"
		t-on-click="onNext"> </i>
		<i class="fa fa-close p-1 float-right"
		style="cursor: pointer;"
		t-on-click="onRemove"> </i>
	</div>`
  1. 在JavaScript文件中引入userState钩子:
const { Component, useState } = owl;
  1. 添加constructor方法并初始化一些变量
constructor() {
	super(...arguments);
	this.messageList = [
		'Hello World',
		'Welcome to Odoo',
		'Odoo is awesome',
		'You are awesome too'
	];
	this.state = useState({ currentIndex: 0 });
}
  1. 在组件类中,添加用户点击事件
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中输出日志以了解组件的生命周期。

准备

步骤

  1. 在构造函数(constructor)中添加日志
constructor() {
	console.log('CALLED:> constructor');
...
  1. 添加willStart方法
async willStart() {
	console.log('CALLED:> willStart');
}
  1. 添加mounted方法
mounted() {
	console.log('CALLED:> mounted');
}
  1. 添加willPatch方法
willPatch() {
	console.log('CALLED:> willPatch');
}
  1. 添加patched方法
patched() {
	console.log('CALLED:> patched');
}
  1. 添加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模板等。

准备

步骤

  1. 在library.book模型中添加颜色的整数型字段
color = fields.Integer()
  1. 在form视图中添加相同的字段
<field name="color" widget="int_color"/>
  1. 在static/src/xml/qweb_tempalte.xml中添加字段的QWeb模板
<?xml version="1.0" encoding="UTF-8"?>
<templates>
    <t t-name="OWLColorPill" owl="1">
        <span t-attf-class="o_color_pill o_color_{{props.pill_no}} {{props.active and'active' or ''}}" t-att-data-val="props.pill_no" t-on-click="pillClicked"t-attf-title="This color is used in {{props.book_count or 0 }} books." />
    </t>
    <span t-name="OWLFieldColorPills" owl="1" class="o_int_colorpicker" t-on-color-updated="colorUpdated">
        <t t-foreach="totalColors" t-as='pill_no'>
            <ColorPill t-if="mode === 'edit' or value == pill_no" pill_no='pill_no' active='value == pill_no' book_count="colorGroupData[pill_no]"/>
        </t>
    </span>
</templates>
  1. 在manifest文件中添加QWeb文件
"qweb": [
	'static/src/xml/qweb_template.xml',
],
  1. 现在我们在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中查看。
  2. 添加static/src/js/field_widget.js
odoo.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,扩展AbstractField

class 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文件添加到后台资源。

<template id="assets_backend" inherit_id="web.assets_ backend">
    <xpath expr="." position="inside">
        <script src="/my_library/static/src/js
/component.js" type="text/javascript" />
        <script src="/my_library/static/src/js
/field_widget.js" type="text/javascript" />
        <link href="/my_library/static/src/scss
/field_widget.scss" rel="stylesheet" type="text/scss" />
    </xpath>
</template>

更新模块,如下图

这个字段看起来就像上一章中的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文件加载到后端资产中。

posted @ 2021-03-01 23:26  老韩头的开发日常  阅读(2722)  评论(0编辑  收藏  举报