From 671a5471cd891ac87a2cffc8a0e7ebac1b519725 Mon Sep 17 00:00:00 2001
From: Aaron Bohy <aab@odoo.com>
Date: Tue, 14 Jan 2020 09:03:21 +0000
Subject: [PATCH] [IMP] web: Owl compatibility: add ComponentAdapter

This commit adds the ComponentAdapter, an Owl component meant to
be used as universal adapter for Owl components that embed Odoo
legacy widgets.

This component will be a precious tool during the transition phase
of converting our JS codebase from the legacy (widget) framework to
Owl.
---
 addons/web/static/src/js/env.js               |  71 +-
 addons/web/static/src/js/owl_compatibility.js | 258 +++++++
 addons/web/static/tests/helpers/test_env.js   |  21 +-
 addons/web/static/tests/helpers/test_utils.js |   1 +
 .../static/tests/helpers/test_utils_mock.js   |  76 +-
 .../static/tests/owl_compatibility_tests.js   | 674 ++++++++++++++++++
 addons/web/views/webclient_templates.xml      |   4 +
 7 files changed, 1090 insertions(+), 15 deletions(-)
 create mode 100644 addons/web/static/src/js/owl_compatibility.js
 create mode 100644 addons/web/static/tests/owl_compatibility_tests.js

diff --git a/addons/web/static/src/js/env.js b/addons/web/static/src/js/env.js
index c223fcc91ee9..72d14ae3e60e 100644
--- a/addons/web/static/src/js/env.js
+++ b/addons/web/static/src/js/env.js
@@ -1,15 +1,15 @@
 odoo.define("web.env", function (require) {
     "use strict";
 
-    const { _lt, _t, bus } = require("web.core");
-    const { blockUI, unblockUI } = require("web.framework");
-    const { device, isDebug } = require("web.config");
     const { jsonRpc } = require('web.ajax');
+    const { device, isDebug } = require("web.config");
+    const { _lt, _t, bus, serviceRegistry } = require("web.core");
+    const dataManager = require('web.data_manager');
+    const { blockUI, unblockUI } = require("web.framework");
     const rpc = require("web.rpc");
     const session = require("web.session");
     const utils = require("web.utils");
 
-
     const qweb = new owl.QWeb({ translateFn: _t });
 
     function ajaxJsonRPC() {
@@ -59,6 +59,64 @@ odoo.define("web.env", function (require) {
         utils.set_cookie(...arguments);
     }
 
+    // ServiceProvider
+    const services = {}; // dict containing deployed service instances
+    const UndeployedServices = {}; // dict containing classes of undeployed services
+    function _deployServices() {
+        let done = false;
+        while (!done) {
+            const serviceName = _.findKey(UndeployedServices, Service => {
+                // no missing dependency
+                return !_.some(Service.prototype.dependencies, depName => {
+                    return !services[depName];
+                });
+            });
+            if (serviceName) {
+                const Service = UndeployedServices[serviceName];
+                // we created a patched version of the Service in which the 'trigger_up'
+                // function directly calls the requested service, instead of triggering
+                // a 'call_service' event up, which wouldn't work as services have
+                // no parent
+                const PatchedService = Service.extend({
+                    _trigger_up: function (ev) {
+                        this._super(...arguments);
+                        if (!ev.is_stopped() && ev.name === 'call_service') {
+                            const payload = ev.data;
+                            let args = payload.args || [];
+                            if (payload.service === 'ajax' && payload.method === 'rpc') {
+                                // ajax service uses an extra 'target' argument for rpc
+                                args = args.concat(ev.target);
+                            }
+                            const service = services[payload.service];
+                            const result = service[payload.method].apply(service, args);
+                            payload.callback(result);
+                        }
+                    },
+                });
+                const service = new PatchedService();
+                services[serviceName] = service;
+                delete UndeployedServices[serviceName];
+                service.start();
+            } else {
+                done = true;
+            }
+        }
+    }
+    _.each(serviceRegistry.map, (Service, serviceName) => {
+        if (serviceName in UndeployedServices) {
+            throw new Error(`Service ${serviceName} is already loaded.`);
+        }
+        UndeployedServices[serviceName] = Service;
+    });
+    serviceRegistry.onAdd((serviceName, Service) => {
+        if (serviceName in services || serviceName in UndeployedServices) {
+            throw new Error(`Service ${serviceName} is already loaded.`);
+        }
+        UndeployedServices[serviceName] = Service;
+        _deployServices();
+    });
+    _deployServices();
+
     // There should be as much dependencies as possible in the env object.
     // This will allow an easier testing of components.
     // See https://github.com/odoo/owl/blob/master/doc/reference/environment.md#content-of-an-environment
@@ -67,10 +125,11 @@ odoo.define("web.env", function (require) {
         _lt,
         _t,
         bus,
+        dataManager,
         device,
         isDebug,
         qweb,
-        services: {
+        services: Object.assign(services, {
             ajaxJsonRPC,
             blockUI,
             getCookie,
@@ -80,7 +139,7 @@ odoo.define("web.env", function (require) {
             rpc: performRPC,
             setCookie,
             unblockUI,
-        },
+        }),
         session,
     };
 });
diff --git a/addons/web/static/src/js/owl_compatibility.js b/addons/web/static/src/js/owl_compatibility.js
new file mode 100644
index 000000000000..e04e42c30b54
--- /dev/null
+++ b/addons/web/static/src/js/owl_compatibility.js
@@ -0,0 +1,258 @@
+odoo.define('web.OwlCompatibility', function () {
+    "use strict";
+
+    /**
+     * This file defines the necessary tools for the transition phase where Odoo
+     * legacy widgets and Owl components will coexist. There are two possible
+     * scenarios:
+     *  1) An Owl component has to instantiate legacy widgets
+     *  2) A legacy widget has to instantiate Owl components
+     */
+
+    const { Component, tags } = owl;
+
+    /**
+     * Case 1) An Owl component has to instantiate legacy widgets
+     * ----------------------------------------------------------
+     *
+     * The ComponentAdapter is an Owl component meant to be used as universal
+     * adapter for Owl components that embed Odoo legacy widgets (or dynamically
+     * both Owl components and Odoo legacy widgets), e.g.:
+     *
+     *                           Owl Component
+     *                                 |
+     *                         ComponentAdapter (Owl component)
+     *                                 |
+     *                       Legacy Widget(s) (or Owl component(s))
+     *
+     *
+     * The adapter takes the component/widget class as 'Component' prop, and the
+     * arguments (except first arg 'parent') to initialize it as props.
+     * For instance:
+     *     <ComponentAdapter Component="LegacyWidget" params="params"/>
+     * will be translated to:
+     *     const LegacyWidget = this.props.Component;
+     *     const legacyWidget = new LegacyWidget(this, this.props.params);
+     *
+     * If more than one argument (in addition to 'parent') is given to initialize
+     * the legacy widget, the arguments order (to initialize the sub widget) has
+     * to be somehow specified. There are two alternatives. One can either (1)
+     * specify the prop 'widgetArgs', corresponding to the array of arguments,
+     * otherwise (2) a subclass of ComponentAdapter has to be defined. This
+     * subclass must override the 'widgetArgs' getter to translate arguments
+     * received as props to an array of arguments for the call to init.
+     * For instance:
+     *     (1) <ComponentAdapter Component="LegacyWidget" firstArg="a" secondArg="b" widgetsArgs="[a, b]"/>
+     *     (2) class SpecificAdapter extends ComponentAdapter {
+     *             get widgetArgs() {
+     *                 return [this.props.firstArg, this.props.secondArg];
+     *             }
+     *         }
+     *         <SpecificAdapter Component="LegacyWidget" firstArg="a" secondArg="b"/>
+     *
+     * If the legacy widget has to be updated when props change, one must define
+     * a subclass of ComponentAdapter to override 'update' and 'render'. The
+     * 'update' function takes the nextProps as argument, and should update the
+     * internal state of the widget (might be async, and return a Promise).
+     * However, to ensure that the DOM is updated all at once, it shouldn't do
+     * a re-rendering. This is the role of function 'render', which will be
+     * called just before patching the DOM, and which thus must be synchronous.
+     * For instance:
+     *     class SpecificAdapter extends ComponentAdapter {
+     *         update(nextProps) {
+     *             return this.widget.updateState(nextProps);
+     *         }
+     *         render() {
+     *             return this.widget.render();
+     *         }
+     *     }
+     */
+    class ComponentAdapter extends Component {
+        /**
+         * Creates the template on-the-fly, depending on the type of Component
+         * (legacy widget or Owl component).
+         *
+         * @override
+         */
+        constructor(parent, props) {
+            if (!props.Component) {
+                throw Error(`ComponentAdapter: 'Component' prop is missing.`);
+            }
+            let template;
+            if (!(props.Component.prototype instanceof Component)) {
+                template = tags.xml`<div/>`;
+            } else {
+                let propsStr = '';
+                for (let p in props) {
+                    if (p !== 'Component') {
+                        propsStr += ` ${p}="props.${p}"`;
+                    }
+                }
+                template = tags.xml`<t t-component="props.Component"${propsStr}/>`;
+            }
+            ComponentAdapter.template = template;
+            super(...arguments);
+            this.template = template;
+            ComponentAdapter.template = null;
+
+            this.widget = null; // widget instance, if Component is a legacy widget
+        }
+
+        /**
+         * Starts the legacy widget (not in the DOM yet)
+         *
+         * @override
+         */
+        willStart() {
+            if (!(this.props.Component.prototype instanceof Component)) {
+                this.widget = new this.props.Component(this, ...this.widgetArgs);
+                return this.widget._widgetRenderAndInsert(() => {});
+            }
+        }
+
+        /**
+         * Updates the internal state of the legacy widget (but doesn't re-render
+         * it yet).
+         *
+         * @override
+         */
+        willUpdateProps(nextProps) {
+            if (this.widget) {
+                return this.update(nextProps);
+            }
+        }
+
+        /**
+         * Hooks just before the actual patch to replace the fake div in the
+         * vnode by the actual node of the legacy widget. If the widget has to
+         * be re-render (because it has previously been updated), re-render it.
+         * This must be synchronous.
+         *
+         * @override
+         */
+        __patch(vnode) {
+            if (this.widget) {
+                if (this.__owl__.vnode) { // not at first rendering
+                    this.render();
+                }
+                vnode.elm = this.widget.el;
+            }
+            return super.__patch(...arguments);
+        }
+
+        /**
+         * @override
+         */
+        mounted() {
+            if (this.widget && this.widget.on_attach_callback) {
+                this.widget.on_attach_callback();
+            }
+        }
+
+        /**
+         * @override
+         */
+        willUnmount() {
+            if (this.widget && this.widget.on_detach_callback) {
+                this.widget.on_detach_callback();
+            }
+        }
+
+        /**
+         * @override
+         */
+        __destroy() {
+            super.__destroy(...arguments);
+            if (this.widget) {
+                this.widget.destroy();
+            }
+        }
+
+        /**
+         * Getter that translates the props (except 'Component') into the array
+         * of arguments used to initialize the legacy widget.
+         *
+         * Must be overriden if at least two props (other that Component) are
+         * given.
+         *
+         * @returns {Array}
+         */
+        get widgetArgs() {
+            if (this.props.widgetArgs) {
+                return this.props.widgetArgs;
+            }
+            const args = Object.keys(this.props);
+            args.splice(args.indexOf('Component'), 1);
+            if (args.length > 1) {
+                throw new Error(`ComponentAdapter has more than 1 argument, 'widgetArgs' must be overriden.`);
+            }
+            return args.map(a => this.props[a]);
+        }
+
+        /**
+         * Can be overriden to update the internal state of the widget when props
+         * change. To ensure that the DOM is updated at once, this function should
+         * not do a re-rendering (which should be done by 'render' instead).
+         *
+         * @param {Object} nextProps
+         * @returns {Promise}
+         */
+        update(/*nextProps*/) {
+            console.warn(`ComponentAdapter: Widget could not be updated, maybe override 'update' function?`);
+        }
+
+        /**
+         * Can be overriden to re-render the widget after an update. This
+         * function will be called just before patchin the DOM, s.t. the DOM is
+         * updated at once. It must be synchronous
+         */
+        render() {
+            console.warn(`ComponentAdapter: Widget could not be re-rendered, maybe override 'render' function?`);
+        }
+
+        /**
+         * Mocks _trigger_up to redirect Odoo legacy events to OWL events.
+         *
+         * @private
+         * @param {OdooEvent} ev
+         */
+        _trigger_up(ev) {
+            const evType = ev.name;
+            const payload = ev.data;
+            if (evType === 'call_service') {
+                let args = payload.args || [];
+                if (payload.service === 'ajax' && payload.method === 'rpc') {
+                    // ajax service uses an extra 'target' argument for rpc
+                    args = args.concat(ev.target);
+                }
+                const service = this.env.services[payload.service];
+                const result = service[payload.method].apply(service, args);
+                payload.callback(result);
+            } else if (evType === 'get_session') {
+                if (payload.callback) {
+                    payload.callback(this.env.session);
+                }
+            } else if (evType === 'load_views') {
+                const params = {
+                    model: payload.modelName,
+                    context: payload.context,
+                    views_descr: payload.views,
+                };
+                this.env.dataManager
+                    .load_views(params, payload.options || {})
+                    .then(payload.on_success);
+            } else if (evType === 'load_filters') {
+                return this.env.dataManager
+                    .load_filters(payload)
+                    .then(payload.on_success);
+            } else {
+                payload.__targetWidget = ev.target;
+                this.trigger(evType.replace(/_/g, '-'), payload);
+            }
+        }
+    }
+
+    return {
+        ComponentAdapter,
+    };
+});
diff --git a/addons/web/static/tests/helpers/test_env.js b/addons/web/static/tests/helpers/test_env.js
index 417350ef65bb..fabe52ff4440 100644
--- a/addons/web/static/tests/helpers/test_env.js
+++ b/addons/web/static/tests/helpers/test_env.js
@@ -1,8 +1,10 @@
 odoo.define('web.test_env', async function (require) {
     "use strict";
 
-    const { buildQuery } = require("web.rpc");
+    const AbstractStorageService = require('web.AbstractStorageService');
     const Bus = require("web.Bus");
+    const RamStorage = require('web.RamStorage');
+    const { buildQuery } = require("web.rpc");
     const session = require('web.session');
 
     /**
@@ -45,6 +47,10 @@ odoo.define('web.test_env', async function (require) {
      */
     function makeTestEnvironment(env = {}, providedRPC = null) {
         const proxiedEnv = _proxify(env, 'env');
+        const RamStorageService = AbstractStorageService.extend({
+            storage: new RamStorage(),
+        });
+        let testEnv = {};
         const defaultEnv = {
             _t: env._t || (s => s),
             _lt: env._lt || (s => s),
@@ -54,11 +60,22 @@ odoo.define('web.test_env', async function (require) {
             }, env.device),
             qweb: new owl.QWeb({ templates: session.owlTemplates }),
             services: Object.assign({
+                ajax: { // for legacy subwidgets
+                    rpc() {
+                        const prom = testEnv.session.rpc(...arguments);
+                        prom.abort = function () {
+                            throw new Error("Can't abort this request");
+                        };
+                        return prom;
+                    },
+                },
                 getCookie() { },
                 rpc(params, options) {
                     const query = buildQuery(params);
                     return testEnv.session.rpc(query.route, query.params, options);
                 },
+                local_storage: new RamStorageService(),
+                session_storage: new RamStorageService(),
             }, env.services),
             session: Object.assign({
                 rpc(route, params, options) {
@@ -69,7 +86,7 @@ odoo.define('web.test_env', async function (require) {
                 },
             }, env.session),
         };
-        const testEnv = Object.assign(proxiedEnv, defaultEnv);
+        testEnv = Object.assign(proxiedEnv, defaultEnv);
         return testEnv;
     }
 
diff --git a/addons/web/static/tests/helpers/test_utils.js b/addons/web/static/tests/helpers/test_utils.js
index 447c39240a71..a9c07973566c 100644
--- a/addons/web/static/tests/helpers/test_utils.js
+++ b/addons/web/static/tests/helpers/test_utils.js
@@ -121,6 +121,7 @@ return Promise.all([
     return {
         mock: {
             addMockEnvironment: testUtilsMock.addMockEnvironment,
+            getMockedOwlEnv: testUtilsMock.getMockedOwlEnv,
             intercept: testUtilsMock.intercept,
             patch: testUtilsMock.patch,
             patchDate: testUtilsMock.patchDate,
diff --git a/addons/web/static/tests/helpers/test_utils_mock.js b/addons/web/static/tests/helpers/test_utils_mock.js
index b4c722b7de11..fe45bbb0e8e2 100644
--- a/addons/web/static/tests/helpers/test_utils_mock.js
+++ b/addons/web/static/tests/helpers/test_utils_mock.js
@@ -10,14 +10,15 @@ odoo.define('web.test_utils_mock', function (require) {
  * testUtils file.
  */
 
-var basic_fields = require('web.basic_fields');
-var config = require('web.config');
-var core = require('web.core');
-var dom = require('web.dom');
-var MockServer = require('web.MockServer');
-var session = require('web.session');
+const basic_fields = require('web.basic_fields');
+const config = require('web.config');
+const core = require('web.core');
+const dom = require('web.dom');
+const makeTestEnvironment = require('web.test_env');
+const MockServer = require('web.MockServer');
+const session = require('web.session');
 
-var DebouncedField = basic_fields.DebouncedField;
+const DebouncedField = basic_fields.DebouncedField;
 
 
 //------------------------------------------------------------------------------
@@ -359,6 +360,66 @@ function fieldsViewGet(server, params) {
     return fieldsView;
 }
 
+/**
+ * Returns a mocked environment to be used by OWL components in tests.
+ *
+ * @param {Object} [params]
+ * @param {Object} [params.actions] the actions given to the mock server
+ * @param {Object} [params.archs] this archs given to the mock server
+ * @param {Object} [params.data] the business data given to the mock server
+ * @param {boolean} [params.debug]
+ * @param {function} [params.mockRPC]
+ * @returns {Object}
+ */
+function getMockedOwlEnv(params) {
+    params = params || {};
+    let Server = MockServer;
+    if (params.mockRPC) {
+        Server = MockServer.extend({ _performRpc: params.mockRPC });
+    }
+    const server = new Server(params.data, {
+        actions: params.actions,
+        archs: params.archs,
+        debug: params.debug,
+    });
+    const env = {
+        dataManager: {
+            load_action: (actionID, context) => {
+                return server.performRpc('/web/action/load', {
+                    kwargs: {
+                        action_id: actionID,
+                        additional_context: context,
+                    },
+                });
+            },
+            load_views: (params, options) => {
+                return server.performRpc('/web/dataset/call_kw/' + params.model, {
+                    args: [],
+                    kwargs: {
+                        context: params.context,
+                        options: options,
+                        views: params.views_descr,
+                    },
+                    method: 'load_views',
+                    model: params.model,
+                }).then(function (views) {
+                    return _.mapObject(views, viewParams => {
+                        return fieldsViewGet(server, viewParams);
+                    });
+                });
+            },
+            load_filters: params => {
+                if (params.debug) {
+                    console.log('[mock] load_filters', params);
+                }
+                return Promise.resolve([]);
+            },
+        },
+        session: params.session || {},
+    };
+    return makeTestEnvironment(env, server.performRpc.bind(server));
+}
+
 /**
  * intercepts an event bubbling up the widget hierarchy. The event intercepted
  * must be a "custom event", i.e. an event generated by the method 'trigger_up'.
@@ -573,6 +634,7 @@ function patchSetTimeout() {
 return {
     addMockEnvironment: addMockEnvironment,
     fieldsViewGet: fieldsViewGet,
+    getMockedOwlEnv: getMockedOwlEnv,
     intercept: intercept,
     patchDate: patchDate,
     patch: patch,
diff --git a/addons/web/static/tests/owl_compatibility_tests.js b/addons/web/static/tests/owl_compatibility_tests.js
new file mode 100644
index 000000000000..b82eeb048570
--- /dev/null
+++ b/addons/web/static/tests/owl_compatibility_tests.js
@@ -0,0 +1,674 @@
+odoo.define('web.OwlCompatibilityTests', function (require) {
+    "use strict";
+
+    const { ComponentAdapter } = require('web.OwlCompatibility');
+    const testUtils = require('web.test_utils');
+    const Widget = require('web.Widget');
+
+    const getMockedOwlEnv = testUtils.mock.getMockedOwlEnv;
+    const makeTestPromise = testUtils.makeTestPromise;
+    const nextTick = testUtils.nextTick;
+
+    const { Component, tags, useState } = owl;
+    const { xml } = tags;
+
+    QUnit.module("Owl Compatibility", function () {
+        QUnit.module("ComponentAdapter");
+
+        QUnit.test("sub widget with no argument", async function (assert) {
+            assert.expect(1);
+
+            const MyWidget = Widget.extend({
+                start: function () {
+                    this.$el.text('Hello World!');
+                }
+            });
+            class Parent extends Component {
+                constructor() {
+                    super(...arguments);
+                    this.MyWidget = MyWidget;
+                }
+            }
+            Parent.template = xml`
+                <div>
+                    <ComponentAdapter Component="MyWidget"/>
+                </div>`;
+            Parent.components = { ComponentAdapter };
+
+            const target = testUtils.prepareTarget();
+            const parent = new Parent();
+            await parent.mount(target);
+
+            assert.strictEqual(parent.el.innerHTML, '<div>Hello World!</div>');
+
+            parent.destroy();
+        });
+
+        QUnit.test("sub widget with one argument", async function (assert) {
+            assert.expect(1);
+
+            const MyWidget = Widget.extend({
+                init: function (parent, name) {
+                    this._super.apply(this, arguments);
+                    this.name = name;
+                },
+                start: function () {
+                    this.$el.text(`Hello ${this.name}!`);
+                }
+            });
+            class Parent extends Component {
+                constructor() {
+                    super(...arguments);
+                    this.MyWidget = MyWidget;
+                }
+            }
+            Parent.template = xml`
+                <div>
+                    <ComponentAdapter Component="MyWidget" name="'World'"/>
+                </div>`;
+            Parent.components = { ComponentAdapter };
+
+            const target = testUtils.prepareTarget();
+            const parent = new Parent();
+            await parent.mount(target);
+
+            assert.strictEqual(parent.el.innerHTML, '<div>Hello World!</div>');
+
+            parent.destroy();
+        });
+
+        QUnit.test("sub widget with several arguments (common Adapter)", async function (assert) {
+            assert.expect(1);
+
+            const MyWidget = Widget.extend({
+                init: function (parent, a1, a2) {
+                    this._super.apply(this, arguments);
+                    this.a1 = a1;
+                    this.a2 = a2;
+                },
+                start: function () {
+                    this.$el.text(`${this.a1} ${this.a2}!`);
+                }
+            });
+            class Parent extends Component {
+                constructor() {
+                    super(...arguments);
+                    this.MyWidget = MyWidget;
+                }
+            }
+            Parent.template = xml`
+                <div>
+                    <ComponentAdapter Component="MyWidget" a1="'Hello'" a2="'World'"/>
+                </div>`;
+            Parent.components = { ComponentAdapter };
+
+            const target = testUtils.prepareTarget();
+            const parent = new Parent();
+            try {
+                await parent.mount(target);
+            } catch (e) {
+                assert.strictEqual(e.toString(),
+                    `Error: ComponentAdapter has more than 1 argument, 'widgetArgs' must be overriden.`);
+            }
+
+            parent.destroy();
+        });
+
+        QUnit.test("sub widget with several arguments (specific Adapter)", async function (assert) {
+            assert.expect(1);
+
+            const MyWidget = Widget.extend({
+                init: function (parent, a1, a2) {
+                    this._super.apply(this, arguments);
+                    this.a1 = a1;
+                    this.a2 = a2;
+                },
+                start: function () {
+                    this.$el.text(`${this.a1} ${this.a2}!`);
+                }
+            });
+            class MyWidgetAdapter extends ComponentAdapter {
+                get widgetArgs() {
+                    return [this.props.a1, this.props.a2];
+                }
+            }
+            class Parent extends Component {
+                constructor() {
+                    super(...arguments);
+                    this.MyWidget = MyWidget;
+                }
+            }
+            Parent.template = xml`
+                <div>
+                    <MyWidgetAdapter Component="MyWidget" a1="'Hello'" a2="'World'"/>
+                </div>`;
+            Parent.components = { MyWidgetAdapter };
+
+            const target = testUtils.prepareTarget();
+            const parent = new Parent();
+            await parent.mount(target);
+
+            assert.strictEqual(parent.el.innerHTML, '<div>Hello World!</div>');
+
+            parent.destroy();
+        });
+
+        QUnit.test("sub widget and widgetArgs props", async function (assert) {
+            assert.expect(1);
+
+            const MyWidget = Widget.extend({
+                init: function (parent, a1, a2) {
+                    this._super.apply(this, arguments);
+                    this.a1 = a1;
+                    this.a2 = a2;
+                },
+                start: function () {
+                    this.$el.text(`${this.a1} ${this.a2}!`);
+                }
+            });
+            class Parent extends Component {
+                constructor() {
+                    super(...arguments);
+                    this.MyWidget = MyWidget;
+                }
+            }
+            Parent.template = xml`
+                <div>
+                    <ComponentAdapter Component="MyWidget" a1="'Hello'" a2="'World'" widgetArgs="['Hello', 'World']"/>
+                </div>`;
+            Parent.components = { ComponentAdapter };
+
+            const target = testUtils.prepareTarget();
+            const parent = new Parent();
+            await parent.mount(target);
+
+            assert.strictEqual(parent.el.innerHTML, '<div>Hello World!</div>');
+
+            parent.destroy();
+        });
+
+        QUnit.test("sub widget is updated when props change", async function (assert) {
+            assert.expect(2);
+
+            const MyWidget = Widget.extend({
+                init: function (parent, name) {
+                    this._super.apply(this, arguments);
+                    this.name = name;
+                },
+                start: function () {
+                    this.render();
+                },
+                render: function () {
+                    this.$el.text(`Hello ${this.name}!`);
+                },
+                update: function (name) {
+                    this.name = name;
+                },
+            });
+            class MyWidgetAdapter extends ComponentAdapter {
+                update(nextProps) {
+                    return this.widget.update(nextProps.name);
+                }
+                render() {
+                    this.widget.render();
+                }
+            }
+            class Parent extends Component {
+                constructor() {
+                    super(...arguments);
+                    this.MyWidget = MyWidget;
+                    this.state = useState({
+                        name: "World",
+                    });
+                }
+            }
+            Parent.template = xml`
+                <div>
+                    <MyWidgetAdapter Component="MyWidget" name="state.name"/>
+                </div>`;
+            Parent.components = { MyWidgetAdapter };
+
+            const target = testUtils.prepareTarget();
+            const parent = new Parent();
+            await parent.mount(target);
+
+            assert.strictEqual(parent.el.innerHTML, '<div>Hello World!</div>');
+
+            parent.state.name = "GED";
+            await nextTick();
+
+            assert.strictEqual(parent.el.innerHTML, '<div>Hello GED!</div>');
+
+            parent.destroy();
+        });
+
+        QUnit.test("sub widget is updated when props change (async)", async function (assert) {
+            assert.expect(7);
+
+            const prom = makeTestPromise();
+            const MyWidget = Widget.extend({
+                init: function (parent, name) {
+                    this._super.apply(this, arguments);
+                    this.name = name;
+                },
+                start: function () {
+                    this.render();
+                },
+                render: function () {
+                    this.$el.text(`Hello ${this.name}!`);
+                    assert.step('render');
+                },
+                update: function (name) {
+                    assert.step('update');
+                    this.name = name;
+                },
+            });
+            class MyWidgetAdapter extends ComponentAdapter {
+                update(nextProps) {
+                    return this.widget.update(nextProps.name);
+                }
+                render() {
+                    this.widget.render();
+                }
+            }
+            class AsyncComponent extends Component {
+                willUpdateProps() {
+                    return prom;
+                }
+            }
+            AsyncComponent.template = xml`<div>Hi <t t-esc="props.name"/>!</div>`;
+            class Parent extends Component {
+                constructor() {
+                    super(...arguments);
+                    this.MyWidget = MyWidget;
+                    this.state = useState({
+                        name: "World",
+                    });
+                }
+            }
+            Parent.template = xml`
+                <div>
+                    <AsyncComponent name="state.name"/>
+                    <MyWidgetAdapter Component="MyWidget" name="state.name"/>
+                </div>`;
+            Parent.components = { AsyncComponent, MyWidgetAdapter };
+
+            const target = testUtils.prepareTarget();
+            const parent = new Parent();
+            await parent.mount(target);
+
+            assert.strictEqual(parent.el.innerHTML, '<div>Hi World!</div><div>Hello World!</div>');
+
+            parent.state.name = "GED";
+            await nextTick();
+
+            assert.strictEqual(parent.el.innerHTML, '<div>Hi World!</div><div>Hello World!</div>');
+
+            prom.resolve();
+            await nextTick();
+
+            assert.strictEqual(parent.el.innerHTML, '<div>Hi GED!</div><div>Hello GED!</div>');
+
+            assert.verifySteps(['render', 'update', 'render']);
+
+            parent.destroy();
+        });
+
+        QUnit.test("sub widget methods are correctly called", async function (assert) {
+            assert.expect(8);
+
+            const MyWidget = Widget.extend({
+                on_attach_callback: function () {
+                    assert.step('on_attach_callback');
+                },
+                on_detach_callback: function () {
+                    assert.step('on_detach_callback');
+                },
+                destroy: function () {
+                    assert.step('destroy');
+                    this._super.apply(this, arguments);
+                },
+            });
+            class Parent extends Component {
+                constructor() {
+                    super(...arguments);
+                    this.MyWidget = MyWidget;
+                }
+            }
+            Parent.template = xml`
+                <div>
+                    <ComponentAdapter Component="MyWidget"/>
+                </div>`;
+            Parent.components = { ComponentAdapter };
+
+            const target = testUtils.prepareTarget();
+            const parent = new Parent();
+            await parent.mount(target);
+
+            assert.verifySteps(['on_attach_callback']);
+
+            parent.unmount();
+            await parent.mount(target);
+
+            assert.verifySteps(['on_detach_callback', 'on_attach_callback']);
+
+            parent.destroy();
+
+            assert.verifySteps(['on_detach_callback', 'destroy']);
+        });
+
+        QUnit.test("dynamic sub widget/component", async function (assert) {
+            assert.expect(1);
+
+            const MyWidget = Widget.extend({
+                start: function () {
+                    this.$el.text('widget');
+                },
+            });
+            class MyComponent extends Component {}
+            MyComponent.template = xml`<div>component</div>`;
+            class Parent extends Component {
+                constructor() {
+                    super(...arguments);
+                    this.Children = [MyWidget, MyComponent];
+                }
+            }
+            Parent.template = xml`
+                <div>
+                    <ComponentAdapter t-foreach="Children" t-as="Child" Component="Child"/>
+                </div>`;
+            Parent.components = { ComponentAdapter };
+
+            const target = testUtils.prepareTarget();
+            const parent = new Parent();
+            await parent.mount(target);
+
+            assert.strictEqual(parent.el.innerHTML, '<div>widget</div><div>component</div>');
+
+            parent.destroy();
+        });
+
+        QUnit.test("sub widget that triggers events", async function (assert) {
+            assert.expect(5);
+
+            let widget;
+            const MyWidget = Widget.extend({
+                init: function () {
+                    this._super.apply(this, arguments);
+                    widget = this;
+                },
+            });
+            class Parent extends Component {
+                constructor() {
+                    super(...arguments);
+                    this.MyWidget = MyWidget;
+                }
+                onSomeEvent(ev) {
+                    assert.step(ev.detail.value);
+                    assert.ok(ev.detail.__targetWidget instanceof MyWidget);
+                }
+            }
+            Parent.template = xml`
+                <div t-on-some-event="onSomeEvent">
+                    <ComponentAdapter Component="MyWidget"/>
+                </div>`;
+            Parent.components = { ComponentAdapter };
+
+            const target = testUtils.prepareTarget();
+            const parent = new Parent();
+            await parent.mount(target);
+
+            widget.trigger_up('some-event', { value: 'a' });
+            widget.trigger_up('some_event', { value: 'b' }); // _ are converted to -
+
+            assert.verifySteps(['a', 'b']);
+
+            parent.destroy();
+        });
+
+        QUnit.test("sub widget that calls _rpc", async function (assert) {
+            assert.expect(3);
+
+            const MyWidget = Widget.extend({
+                willStart: function () {
+                    return this._rpc({ route: 'some/route', params: { val: 2 } });
+                },
+            });
+            class Parent extends Component {
+                constructor() {
+                    super(...arguments);
+                    this.MyWidget = MyWidget;
+                }
+            }
+            Parent.env = getMockedOwlEnv({
+                mockRPC: function (route, args) {
+                    assert.step(`${route} ${args.val}`);
+                    return Promise.resolve();
+                },
+            });
+            Parent.template = xml`
+                <div>
+                    <ComponentAdapter Component="MyWidget"/>
+                </div>`;
+            Parent.components = { ComponentAdapter };
+
+            const target = testUtils.prepareTarget();
+            const parent = new Parent();
+            await parent.mount(target);
+
+            assert.strictEqual(parent.el.innerHTML, '<div></div>');
+            assert.verifySteps(['some/route 2']);
+
+            parent.destroy();
+        });
+
+        QUnit.test("sub widget that calls a service", async function (assert) {
+            assert.expect(1);
+
+            const MyWidget = Widget.extend({
+                start: function () {
+                    let result;
+                    this.trigger_up('call_service', {
+                        service: 'math',
+                        method: 'sqrt',
+                        args: [9],
+                        callback: r => {
+                            result = r;
+                        },
+                    });
+                    assert.strictEqual(result, 3);
+                },
+            });
+            class Parent extends Component {
+                constructor() {
+                    super(...arguments);
+                    this.MyWidget = MyWidget;
+                }
+            }
+            const env = getMockedOwlEnv();
+            env.services.math = {
+                sqrt: v => Math.sqrt(v),
+            };
+            Parent.env = env;
+            Parent.template = xml`
+                <div>
+                    <ComponentAdapter Component="MyWidget"/>
+                </div>`;
+            Parent.components = { ComponentAdapter };
+
+            const target = testUtils.prepareTarget();
+            const parent = new Parent();
+            await parent.mount(target);
+
+            parent.destroy();
+        });
+
+        QUnit.test("sub widget that requests the session", async function (assert) {
+            assert.expect(1);
+
+            const MyWidget = Widget.extend({
+                start: function () {
+                    assert.strictEqual(this.getSession().key, 'value');
+                },
+            });
+            class Parent extends Component {
+                constructor() {
+                    super(...arguments);
+                    this.MyWidget = MyWidget;
+                }
+            }
+            Parent.env = getMockedOwlEnv({
+                session: { key: 'value' },
+            });
+            Parent.template = xml`
+                <div>
+                    <ComponentAdapter Component="MyWidget"/>
+                </div>`;
+            Parent.components = { ComponentAdapter };
+
+            const target = testUtils.prepareTarget();
+            const parent = new Parent();
+            await parent.mount(target);
+
+            parent.destroy();
+        });
+
+        QUnit.test("sub widget that calls load_views", async function (assert) {
+            assert.expect(4);
+
+            const MyWidget = Widget.extend({
+                willStart: function () {
+                    return this.loadViews('some_model', { x: 2 }, [[false, 'list']]);
+                },
+            });
+            class Parent extends Component {
+                constructor() {
+                    super(...arguments);
+                    this.MyWidget = MyWidget;
+                }
+            }
+            Parent.env = getMockedOwlEnv({
+                mockRPC: function (route, args) {
+                    assert.strictEqual(route, '/web/dataset/call_kw/some_model');
+                    assert.deepEqual(args.kwargs.context, { x: 2 });
+                    assert.deepEqual(args.kwargs.views, [[false, 'list']]);
+                    return Promise.resolve();
+                },
+            });
+            Parent.template = xml`
+                <div>
+                    <ComponentAdapter Component="MyWidget"/>
+                </div>`;
+            Parent.components = { ComponentAdapter };
+
+            const target = testUtils.prepareTarget();
+            const parent = new Parent();
+            await parent.mount(target);
+
+            assert.strictEqual(parent.el.innerHTML, '<div></div>');
+
+            parent.destroy();
+        });
+
+        QUnit.test("sub widgets in a t-if/t-else", async function (assert) {
+            assert.expect(3);
+
+            const MyWidget1 = Widget.extend({
+                start: function () {
+                    this.$el.text('Hi');
+                },
+            });
+            const MyWidget2 = Widget.extend({
+                start: function () {
+                    this.$el.text('Hello');
+                },
+            });
+            class Parent extends Component {
+                constructor() {
+                    super(...arguments);
+                    this.MyWidget1 = MyWidget1;
+                    this.MyWidget2 = MyWidget2;
+                    this.state = useState({
+                        flag: true,
+                    });
+                }
+            }
+            Parent.template = xml`
+                <div>
+                    <ComponentAdapter t-if="state.flag" Component="MyWidget1"/>
+                    <ComponentAdapter t-else="" Component="MyWidget2"/>
+                </div>`;
+            Parent.components = { ComponentAdapter };
+
+            const target = testUtils.prepareTarget();
+            const parent = new Parent();
+            await parent.mount(target);
+
+            assert.strictEqual(parent.el.innerHTML, '<div>Hi</div>');
+
+            parent.state.flag = false;
+            await nextTick();
+
+            assert.strictEqual(parent.el.innerHTML, '<div>Hello</div>');
+
+            parent.state.flag = true;
+            await nextTick();
+
+            assert.strictEqual(parent.el.innerHTML, '<div>Hi</div>');
+
+            parent.destroy();
+        });
+
+        QUnit.test("sub widget in a t-if, and events", async function (assert) {
+            assert.expect(6);
+
+            let myWidget;
+            const MyWidget = Widget.extend({
+                start: function () {
+                    myWidget = this;
+                    this.$el.text('Hi');
+                },
+            });
+            class Parent extends Component {
+                constructor() {
+                    super(...arguments);
+                    this.MyWidget = MyWidget;
+                    this.state = useState({
+                        flag: true,
+                    });
+                }
+                onSomeEvent(ev) {
+                    assert.step(ev.detail.value);
+                }
+            }
+            Parent.template = xml`
+                <div t-on-some-event="onSomeEvent">
+                    <ComponentAdapter t-if="state.flag" Component="MyWidget"/>
+                </div>`;
+            Parent.components = { ComponentAdapter };
+
+            const target = testUtils.prepareTarget();
+            const parent = new Parent();
+            await parent.mount(target);
+
+            assert.strictEqual(parent.el.innerHTML, '<div>Hi</div>');
+            myWidget.trigger_up('some-event', { value: 'a' });
+
+            parent.state.flag = false;
+            await nextTick();
+
+            assert.strictEqual(parent.el.innerHTML, '');
+            myWidget.trigger_up('some-event', { value: 'b' });
+
+            parent.state.flag = true;
+            await nextTick();
+
+            assert.strictEqual(parent.el.innerHTML, '<div>Hi</div>');
+            myWidget.trigger_up('some-event', { value: 'c' });
+
+            assert.verifySteps(['a', 'c']);
+
+            parent.destroy();
+        });
+    });
+});
diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml
index 15bd4c4923a0..07ca6f100667 100644
--- a/addons/web/views/webclient_templates.xml
+++ b/addons/web/views/webclient_templates.xml
@@ -380,6 +380,8 @@
         <script type="text/javascript" src="/web/static/src/js/widgets/attach_document.js"></script>
         <script type="text/javascript" src="/web/static/src/js/fields/signature.js"></script>
 
+        <script type="text/javascript" src="/web/static/src/js/owl_compatibility.js"></script>
+
         <script type="text/javascript" src="/web/static/src/js/report/utils.js"/>
         <script type="text/javascript" src="/web/static/src/js/report/client_action.js"/>
         <link rel="stylesheet" type="text/scss" href="/web/static/src/scss/report_backend.scss"/>
@@ -726,6 +728,8 @@
                 <script type="text/javascript" src="/web/static/tests/tools/debug_manager_tests.js"/>
 
                 <script type="text/javascript" src="/web/static/tests/helpers/test_utils_tests.js"></script>
+
+                <script type="text/javascript" src="/web/static/tests/owl_compatibility_tests.js"></script>
             </t>
 
             <div id="qunit"/>
-- 
GitLab