[IMP] web: add support of owl custom widget

add support of <widget> tag for owl, In order to prepare the future,
we want to convert everything in Owl, in future widgets generated by
<widget> tag will also be converted to owl.

with this commit we support widget to instantiate using ComponentWrapper.

task-2337692

Co-authored-by: Aaron Bohy <aab@odoo.com>
This commit is contained in:
Mohammed Shekha
2021-03-26 12:25:44 +00:00
committed by Yannick Tivisse
co-authored by Aaron Bohy
parent 7fb9c81296
commit 877ef5552b
11 changed files with 215 additions and 12 deletions
@@ -75,9 +75,6 @@ odoo.define('web.FieldWrapper', function (require) {
// Getters
//----------------------------------------------------------------------
get $el() {
return $(this.el);
}
get fieldDependencies() {
return this.Component.fieldDependencies;
}
@@ -518,6 +518,14 @@ odoo.define('web.OwlCompatibility', function () {
this._mountArgs = arguments;
return super.mount(...arguments);
}
//----------------------------------------------------------------------
// Getters
//----------------------------------------------------------------------
get $el() {
return $(this.el);
}
}
ComponentWrapper.template = xml`<t t-component="Component" t-props="props" t-ref="component"/>`;
@@ -13,10 +13,12 @@ var core = require('web.core');
var dom = require('web.dom');
const session = require('web.session');
const utils = require('web.utils');
var widgetRegistry = require('web.widget_registry');
const widgetRegistry = require('web.widget_registry');
const widgetRegistryOwl = require("web.widgetRegistry");
const { WidgetAdapterMixin } = require('web.OwlCompatibility');
const FieldWrapper = require('web.FieldWrapper');
const WidgetWrapper = require("web.WidgetWrapper");
var qweb = core.qweb;
const _t = core._t;
@@ -804,13 +806,29 @@ var BasicRenderer = AbstractRenderer.extend(WidgetAdapterMixin, {
* @returns {jQueryElement}
*/
_renderWidget: function (record, node) {
var Widget = widgetRegistry.get(node.attrs.name);
var widget = new Widget(this, record, node);
const name = node.attrs.name;
const Widget = widgetRegistryOwl.get(name) || widgetRegistry.get(name);
const legacy = !(Widget.prototype instanceof owl.Component);
let widget;
if (legacy) {
widget = new Widget(this, record, node, { mode: this.mode });
} else {
widget = new WidgetWrapper(this, Widget, {
record,
node,
options: { mode: this.mode },
});
}
this.widgets.push(widget);
// Prepare widget rendering and save the related promise
var def = widget._widgetRenderAndInsert(function () {});
let def;
if (legacy) {
def = widget._widgetRenderAndInsert(function () {});
} else {
def = widget.mount(document.createDocumentFragment());
}
this.defs.push(def);
var $el = $('<div>');
@@ -1,6 +1,16 @@
odoo.define('web.widget_registry', function (require) {
"use strict";
////////////////////////////////////////////////////////////////////////////
// /!\ DEPRECATED
//
// /!\ This registry is deprecated. It is used to register legacy Widgets.
// /!\ Use 'web.widgetRegistry' to register Owl Components.
// /!\ Existing Widgets will be at some point converted into Owl Components.
// /!\ All new code should be directly written using Owl Components.
////////////////////////////////////////////////////////////////////////////
// This registry is supposed to contain all custom widgets that will be
// available in the basic views, with the tag <widget/>. There are
// currently no such widget in the web client, but the functionality is
@@ -23,5 +33,5 @@ odoo.define('web.widget_registry', function (require) {
var Registry = require('web.Registry');
return new Registry();
return new Registry(null, (value) => !(value.prototype instanceof owl.Component));
});
@@ -0,0 +1,24 @@
odoo.define("web.widgetRegistry", function (require) {
"use strict";
// This registry is supposed to contain all custom components that will be
// available in the basic views, with the tag <widget/>. Those components
// allow to cleanly add custom behavior in basic views.
//
// The way custom components work is that they register themselves to this
// registry:
//
// widgetRegistry.add('some_name', MyComponent);
//
// Then, they are available with the <widget/> tag (in the arch):
//
// <widget name="some_name"/>
//
// Those components will be then properly instantiated, rendered and
// destroyed at the appropriate time, and will receive the current state in
// props.
const Registry = require("web.Registry");
return new Registry(null, (value) => value.prototype instanceof owl.Component);
});
@@ -0,0 +1,19 @@
odoo.define('web.WidgetWrapper', function (require) {
"use strict";
const { ComponentWrapper } = require('web.OwlCompatibility');
class WidgetWrapper extends ComponentWrapper {
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
updateState(state) {
if (this.componentRef.comp.updateState) {
this.componentRef.comp.updateState(state);
}
}
}
return WidgetWrapper;
});
@@ -13,7 +13,10 @@ var field_utils = require('web.field_utils');
const FieldWrapper = require('web.FieldWrapper');
var utils = require('web.utils');
var Widget = require('web.Widget');
const { WidgetAdapterMixin } = require('web.OwlCompatibility');
var widgetRegistry = require('web.widget_registry');
const widgetRegistryOwl = require('web.widgetRegistry');
const WidgetWrapper = require("web.WidgetWrapper");
var _t = core._t;
var QWeb = core.qweb;
@@ -21,7 +24,7 @@ var QWeb = core.qweb;
var KANBAN_RECORD_COLORS = require('web.basic_fields').FieldColorPicker.prototype.RECORD_COLORS;
var NB_KANBAN_RECORD_COLORS = KANBAN_RECORD_COLORS.length;
var KanbanRecord = Widget.extend({
var KanbanRecord = Widget.extend(WidgetAdapterMixin, {
events: {
'click .oe_kanban_action': '_onKanbanActionClicked',
'click .o_kanban_manage_toggle_button': '_onManageTogglerClicked',
@@ -54,12 +57,21 @@ var KanbanRecord = Widget.extend({
start: function () {
return Promise.all([this._super.apply(this, arguments), this._render()]);
},
/**
* @override
*/
destroy: function () {
this._super(...arguments);
WidgetAdapterMixin.destroy.call(this);
},
/**
* Called each time the record is attached to the DOM.
*/
on_attach_callback: function () {
this.isInDOM = true;
_.invoke(this.subWidgets, 'on_attach_callback');
// call on_attach_callback on child components (tag <widget>)
WidgetAdapterMixin.on_attach_callback.call(this);
},
/**
* Called each time the record is detached from the DOM.
@@ -67,6 +79,8 @@ var KanbanRecord = Widget.extend({
on_detach_callback: function () {
this.isInDOM = false;
_.invoke(this.subWidgets, 'on_detach_callback');
// call on_detach_callback on child components (tag <widget>)
WidgetAdapterMixin.on_detach_callback.call(this);
},
//--------------------------------------------------------------------------
@@ -330,10 +344,24 @@ var KanbanRecord = Widget.extend({
var self = this;
this.$("widget").each(function () {
var $field = $(this);
var Widget = widgetRegistry.get($field.attr('name'));
var widget = new Widget(self, self.state);
var def = widget._widgetRenderAndInsert(function () { });
const name = $field.attr('name');
const Widget = widgetRegistryOwl.get(name) || widgetRegistry.get(name);
const legacy = !(Widget.prototype instanceof owl.Component);
let widget;
if (legacy) {
widget = new Widget(self, self.state);
} else {
widget = new WidgetWrapper(this, Widget, { record: self.state });
}
// Prepare widget rendering and save the related promise
let def;
if (legacy) {
def = widget._widgetRenderAndInsert(function () { });
} else {
def = widget.mount(document.createDocumentFragment());
}
self.defs.push(def);
def.then(function () {
$field.replaceWith(widget.$el);
@@ -16,6 +16,7 @@ var pyUtils = require('web.py_utils');
var RamStorage = require('web.RamStorage');
var testUtils = require('web.test_utils');
var widgetRegistry = require('web.widget_registry');
const widgetRegistryOwl = require('web.widgetRegistry');
var Widget = require('web.Widget');
var _t = core._t;
@@ -7854,6 +7855,8 @@ QUnit.module('Views', {
});
QUnit.test('basic support for widgets', async function (assert) {
// This test could be removed as soon as we drop the support of legacy widgets (see test
// below, which is a duplicate of this one, but with an Owl Component instead).
assert.expect(1);
var MyWidget = Widget.extend({
@@ -7884,6 +7887,35 @@ QUnit.module('Views', {
delete widgetRegistry.map.test;
});
QUnit.test('basic support for widgets (being Owl Components)', async function (assert) {
assert.expect(1);
class MyComponent extends owl.Component {
get value() {
return JSON.stringify(this.props.record.data);
}
}
MyComponent.template = owl.tags.xml`<div t-esc="value"/>`;
widgetRegistryOwl.add('test', MyComponent);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<field name="foo"/>
<field name="bar"/>
<widget name="test"/>
</form>`,
});
assert.strictEqual(form.$('.o_widget').text(), '{"foo":"My little Foo Value","bar":false}');
form.destroy();
delete widgetRegistryOwl.map.test;
});
QUnit.test('attach document widget calls action with attachment ids', async function (assert) {
assert.expect(1);
@@ -12,6 +12,7 @@ var mixins = require('web.mixins');
var testUtils = require('web.test_utils');
var Widget = require('web.Widget');
var widgetRegistry = require('web.widget_registry');
const widgetRegistryOwl = require('web.widgetRegistry');
var makeTestPromise = testUtils.makeTestPromise;
var nextTick = testUtils.nextTick;
@@ -5709,6 +5710,8 @@ QUnit.module('Views', {
});
QUnit.test('basic support for widgets', async function (assert) {
// This test could be removed as soon as we drop the support of legacy widgets (see test
// below, which is a duplicate of this one, but with an Owl Component instead).
assert.expect(1);
var MyWidget = Widget.extend({
@@ -5741,6 +5744,41 @@ QUnit.module('Views', {
delete widgetRegistry.map.test;
});
QUnit.test('basic support for widgets (being Owl Components)', async function (assert) {
assert.expect(1);
class MyComponent extends owl.Component {
get value() {
return JSON.stringify(this.props.record.data);
}
}
MyComponent.template = owl.tags.xml`<div t-esc="value"/>`;
widgetRegistryOwl.add('test', MyComponent);
const kanban = await createView({
View: KanbanView,
model: 'partner',
data: this.data,
arch: `
<kanban class="o_kanban_test">
<field name="foo"/>
<templates>
<t t-name="kanban-box">
<div>
<t t-esc="record.foo.value"/>
<widget name="test"/>
</div>
</t>
</templates>
</kanban>`,
});
assert.strictEqual(kanban.$('.o_widget:eq(2)').text(), '{"foo":"gnap","id":3}');
kanban.destroy();
delete widgetRegistryOwl.map.test;
});
QUnit.test('subwidgets with on_attach_callback when changing record color', async function (assert) {
assert.expect(3);
@@ -18,6 +18,7 @@ var RamStorage = require('web.RamStorage');
var testUtils = require('web.test_utils');
const { patch, unpatch } = require('web.utils');
var widgetRegistry = require('web.widget_registry');
const widgetRegistryOwl = require('web.widgetRegistry');
var Widget = require('web.Widget');
@@ -8647,6 +8648,8 @@ QUnit.module('Views', {
});
QUnit.test('basic support for widgets', async function (assert) {
// This test could be removed as soon as we drop the support of legacy widgets (see test
// below, which is a duplicate of this one, but with an Owl Component instead).
assert.expect(1);
var MyWidget = Widget.extend({
@@ -8673,6 +8676,30 @@ QUnit.module('Views', {
delete widgetRegistry.map.test;
});
QUnit.test('basic support for widgets (being Owl Components)', async function (assert) {
assert.expect(1);
class MyComponent extends owl.Component {
get value() {
return JSON.stringify(this.props.record.data);
}
}
MyComponent.template = owl.tags.xml`<div t-esc="value"/>`;
widgetRegistryOwl.add('test', MyComponent);
const list = await createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree><field name="foo"/><field name="int_field"/><widget name="test"/></tree>',
});
assert.strictEqual(list.$('.o_widget').first().text(), '{"foo":"yop","int_field":10,"id":1}');
list.destroy();
delete widgetRegistryOwl.map.test;
});
QUnit.test('use the limit attribute in arch', async function (assert) {
assert.expect(4);
+2
View File
@@ -305,6 +305,8 @@
<script type="text/javascript" src="/web/static/src/js/fields/field_registry.js"></script>
<script type="text/javascript" src="/web/static/src/js/fields/field_registry_owl.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/basic/widget_registry.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/basic/widget_registry_owl.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/basic/widget_wrapper.js"></script>
<script type="text/javascript" src="/web/static/src/js/fields/field_utils.js"></script>
<script type="text/javascript" src="/web/static/src/js/fields/relational_fields.js"></script>
<script type="text/javascript" src="/web/static/src/js/fields/special_fields.js"></script>