From 36061a4ab017a463c1d5bf838334cb4c0fac25ee Mon Sep 17 00:00:00 2001 From: "Michael (mcm)" Date: Mon, 4 Sep 2023 13:26:14 +0200 Subject: [PATCH] [IMP] web: simplify owl_compatibility This commit tried to remove owl_compatibility completely but its last uses are either in refactoring or a bit complex to migrate. Instead of removing the helpers completely, this commit simplifies them a lot and makes them more readable and safe. closes odoo/odoo#134295 Task: 3439226 Related: odoo/enterprise#46899 Signed-off-by: Aaron Bohy (aab) --- .../static/src/js/mass_mailing_wysiwyg.js | 8 +- addons/mrp_subcontracting/__manifest__.py | 3 - addons/point_of_sale/__manifest__.py | 1 - addons/project/__manifest__.py | 3 - addons/web/__manifest__.py | 6 - .../src/legacy/js/component_extension.js | 10 - .../static/src/legacy/js/owl_compatibility.js | 744 -------- .../src/legacy/js/public/public_root.js | 7 +- .../web/static/src/legacy/legacy_component.js | 95 - addons/web/static/src/legacy/utils.js | 95 +- .../tests/legacy/component_extension_tests.js | 48 - .../tests/legacy/owl_compatibility_tests.js | 1623 +---------------- addons/web_editor/__manifest__.py | 1 - .../static/src/js/editor/snippets.editor.js | 9 +- .../static/src/js/editor/snippets.options.js | 42 +- .../js/frontend/loadWysiwygFromTextarea.js | 6 +- .../src/components/ace_editor/ace_editor.js | 50 +- addons/website_slides/__manifest__.py | 2 - 18 files changed, 221 insertions(+), 2532 deletions(-) delete mode 100644 addons/web/static/src/legacy/js/component_extension.js delete mode 100644 addons/web/static/src/legacy/js/owl_compatibility.js delete mode 100644 addons/web/static/src/legacy/legacy_component.js delete mode 100644 addons/web/static/tests/legacy/component_extension_tests.js diff --git a/addons/mass_mailing/static/src/js/mass_mailing_wysiwyg.js b/addons/mass_mailing/static/src/js/mass_mailing_wysiwyg.js index 965f9cdda6e..949997bbec7 100644 --- a/addons/mass_mailing/static/src/js/mass_mailing_wysiwyg.js +++ b/addons/mass_mailing/static/src/js/mass_mailing_wysiwyg.js @@ -1,6 +1,6 @@ /** @odoo-module **/ -import { ComponentWrapper } from "@web/legacy/js/owl_compatibility"; +import { attachComponent } from "@web/legacy/utils"; import { Wysiwyg } from "@web_editor/js/wysiwyg/wysiwyg"; import { closestElement } from "@web_editor/js/editor/odoo-editor/src/OdooEditor"; import { Toolbar } from "@web_editor/js/editor/toolbar"; @@ -46,15 +46,13 @@ export class MassMailingWysiwyg extends Wysiwyg { if (fold) { this.snippetsMenu.setFolded(true); if (!this.floatingToolbar) { - // Instantiate and configure new toolbar. - const toolbarWrapper = new ComponentWrapper({}, Toolbar, this.state.toolbarProps); - // The wysiwyg can be instanciated inside an iframe. The dialog // component is mounted on the global document. const toolbarWrapperElement = document.createElement('div'); toolbarWrapperElement.style.display = 'contents'; document.body.append(toolbarWrapperElement); - await toolbarWrapper.mount(toolbarWrapperElement); + // Instantiate and configure new toolbar. + await attachComponent({}, toolbarWrapperElement, Toolbar, this.state.toolbarProps); this.toolbarEl = toolbarWrapperElement.firstChild; this.floatingToolbarEl = this.toolbarEl; diff --git a/addons/mrp_subcontracting/__manifest__.py b/addons/mrp_subcontracting/__manifest__.py index b01a2bd471a..5552ebcce79 100644 --- a/addons/mrp_subcontracting/__manifest__.py +++ b/addons/mrp_subcontracting/__manifest__.py @@ -69,8 +69,6 @@ 'web/static/lib/owl/owl.js', 'web/static/lib/owl/odoo_module.js', 'web/static/src/libs/owl.js', - 'web/static/src/legacy/js/component_extension.js', - 'web/static/src/legacy/legacy_component.js', 'web/static/lib/jquery/jquery.js', 'web/static/lib/jquery.ui/jquery-ui.js', 'web/static/lib/jquery.mjs.nestedSortable/jquery.mjs.nestedSortable.js', @@ -162,7 +160,6 @@ 'web/static/src/legacy/backend_utils.js', 'web/static/src/legacy/utils.js', 'web/static/src/legacy/js/env.js', - 'web/static/src/legacy/js/owl_compatibility.js', 'mrp_subcontracting/static/src/subcontracting_portal/*', 'web/static/src/start.js', diff --git a/addons/point_of_sale/__manifest__.py b/addons/point_of_sale/__manifest__.py index ce4135c68b9..af670a67db6 100644 --- a/addons/point_of_sale/__manifest__.py +++ b/addons/point_of_sale/__manifest__.py @@ -142,7 +142,6 @@ # FIXME POSREF legacy dependencies to remove 'web/static/lib/moment/moment.js', 'web/static/src/legacy/js/promise_extension.js', - 'web/static/src/legacy/legacy_component.js', ], # Bundle that starts the pos, loaded on /pos/ui 'point_of_sale.assets_prod': [ diff --git a/addons/project/__manifest__.py b/addons/project/__manifest__.py index a91408ebbc0..d1f069a95d1 100644 --- a/addons/project/__manifest__.py +++ b/addons/project/__manifest__.py @@ -120,8 +120,6 @@ 'web/static/lib/owl/owl.js', 'web/static/lib/owl/odoo_module.js', 'web/static/src/libs/owl.js', - 'web/static/src/legacy/js/component_extension.js', - 'web/static/src/legacy/legacy_component.js', 'web/static/lib/jquery/jquery.js', 'web/static/lib/jquery.ui/jquery-ui.js', 'web/static/lib/jquery.mjs.nestedSortable/jquery.mjs.nestedSortable.js', @@ -215,7 +213,6 @@ 'web/static/src/legacy/backend_utils.js', 'web/static/src/legacy/utils.js', 'web/static/src/legacy/js/env.js', - 'web/static/src/legacy/js/owl_compatibility.js', 'web_editor/static/src/js/editor/odoo-editor/src/base_style.scss', 'web_editor/static/lib/vkbeautify/**/*', diff --git a/addons/web/__manifest__.py b/addons/web/__manifest__.py index fd1ed4c8185..01162fd2504 100644 --- a/addons/web/__manifest__.py +++ b/addons/web/__manifest__.py @@ -69,8 +69,6 @@ This module provides the core of the Odoo Web Client. 'web/static/src/legacy/js/core/cookie_utils.js', 'web/static/lib/moment/moment.js', - 'web/static/src/legacy/js/component_extension.js', - 'web/static/src/legacy/legacy_component.js', 'web/static/lib/jquery/jquery.js', 'web/static/lib/jquery.ui/jquery-ui.js', 'web/static/lib/jquery.mjs.nestedSortable/jquery.mjs.nestedSortable.js', @@ -151,7 +149,6 @@ This module provides the core of the Odoo Web Client. 'web/static/src/legacy/backend_utils.js', 'web/static/src/legacy/utils.js', 'web/static/src/legacy/js/env.js', - 'web/static/src/legacy/js/owl_compatibility.js', 'web/static/src/legacy/xml/base.xml', # Don't include dark mode files in light mode @@ -206,8 +203,6 @@ This module provides the core of the Odoo Web Client. 'web/static/lib/owl/owl.js', 'web/static/lib/owl/odoo_module.js', 'web/static/src/libs/owl.js', - 'web/static/src/legacy/js/component_extension.js', - 'web/static/src/legacy/legacy_component.js', 'web/static/lib/jquery/jquery.js', 'web/static/lib/jquery.ui/jquery-ui.js', 'web/static/lib/jquery.mjs.nestedSortable/jquery.mjs.nestedSortable.js', @@ -266,7 +261,6 @@ This module provides the core of the Odoo Web Client. 'web/static/src/libs/pdfjs.js', 'web/static/src/legacy/utils.js', - 'web/static/src/legacy/js/owl_compatibility.js', 'web/static/src/legacy/js/public/public_env.js', 'web/static/src/legacy/js/public/public_root.js', 'web/static/src/legacy/js/public/public_root_instance.js', diff --git a/addons/web/static/src/legacy/js/component_extension.js b/addons/web/static/src/legacy/js/component_extension.js deleted file mode 100644 index 3eb4edffac0..00000000000 --- a/addons/web/static/src/legacy/js/component_extension.js +++ /dev/null @@ -1,10 +0,0 @@ -(function () { - - owl.Component.env = {}; - - /** - * Symbol used in ComponentWrapper to redirect Owl events to Odoo legacy - * events. - */ - odoo.widgetSymbol = Symbol('widget'); -})(); diff --git a/addons/web/static/src/legacy/js/owl_compatibility.js b/addons/web/static/src/legacy/js/owl_compatibility.js deleted file mode 100644 index 19ca24dca76..00000000000 --- a/addons/web/static/src/legacy/js/owl_compatibility.js +++ /dev/null @@ -1,744 +0,0 @@ -/** @odoo-module **/ - - import { LegacyComponent } from "@web/legacy/legacy_component"; - import { templates } from "@web/core/assets"; - import { _t } from "@web/core/l10n/translation"; - - /** - * 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 { - App, - Component, - onMounted, - onWillStart, - onWillUnmount, - onPatched, - onWillUpdateProps, - onWillDestroy, - useSubEnv, - xml, - status, - } = owl; - - const widgetSymbol = odoo.widgetSymbol; - const children = new WeakMap(); // associates legacy widgets with their Owl children - - const templateForLegacy = xml``; - const templateForOwl = xml``; - /** - * 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: - * - * 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) - * (2) class SpecificAdapter extends ComponentAdapter { - * get widgetArgs() { - * return [this.props.firstArg, this.props.secondArg]; - * } - * } - * - * - * If the legacy widget has to be updated when props change, one must define - * a subclass of ComponentAdapter to override 'updateWidget' and 'renderWidget'. The - * 'updateWidget' 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 'renderWidget', which will be - * called just before patching the DOM, and which thus must be synchronous. - * For instance: - * class SpecificAdapter extends ComponentAdapter { - * updateWidget(nextProps) { - * return this.widget.updateState(nextProps); - * } - * renderWidget() { - * return this.widget.render(); - * } - * } - */ - export class ComponentAdapter extends LegacyComponent { - /** - * Creates the template on-the-fly, depending on the type of Component - * (legacy widget or Owl component). - * - * @override - */ - constructor(props) { - if (!props.Component) { - throw Error(`ComponentAdapter: 'Component' prop is missing.`); - } - let template; - if (!(props.Component.prototype instanceof Component)) { - template = templateForLegacy; - } else { - template = templateForOwl; - } - ComponentAdapter.template = template; - super(...arguments); - this.template = template; - - this.widget = null; // widget instance, if Component is a legacy widget - } - - setup() { - onWillUpdateProps((nextProps) => { - if (this.widget) { - return this.updateWidget(nextProps); - } - }); - - let widgetIsAttached = false; - const insertWidget = () => { - this.removeEl(); - if (!this.widget || !this.widget.el) { - return; - } - const node = this.__owl__.firstNode(); - node.parentNode.insertBefore(this.widget.el, node); - this.widgetEl = this.widget.el; - widgetIsAttached = true; - }; - - onMounted(() => { - insertWidget(); - if (this.widget && this.widget.on_attach_callback) { - this.widget.on_attach_callback(); - } - }); - - onPatched(() => { - if (widgetIsAttached) { - this.renderWidget(); - } else { - insertWidget(); - } - }); - - onWillDestroy(() => this.__destroy(this.__owl__.parent.component)); - - onWillStart(this.onWillStart); - onWillUnmount(this.onWillUnmount); - } - - onWillStart() { - if (!(this.props.Component.prototype instanceof Component)) { - this.widget = new this.props.Component(this, ...this.widgetArgs); - return this.widget._widgetRenderAndInsert(() => { }); - } - } - - onWillUnmount() { - if (this.widget && this.widget.on_detach_callback) { - this.widget.on_detach_callback(); - } - this.removeEl(); - } - - removeEl() { - if (this.widgetEl) { - this.widgetEl.remove(); - this.widgetEl = null; - } - } - - __destroy() { - this.removeEl(); - if (this.widget) { - this.widget.destroy(); - } - } - - get childProps() { - if (!this._childProps) { - this._childProps = Object.assign({}, this.props); - delete this._childProps.Component; - } - return this._childProps; - } - - /** - * 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} - */ - updateWidget(/*nextProps*/) { - if (odoo.debug.includes("assets")) { - console.warn(`ComponentAdapter: Widget could not be updated, maybe override 'updateWidget' 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 - */ - renderWidget() { - if (odoo.debug.includes("assets")) { - console.warn(`ComponentAdapter: Widget could not be re-rendered, maybe override 'renderWidget' 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]; - //If the service doesn't exist it means that it was translated to Owl - if (service) { - const result = service[payload.method].apply(service, args); - payload.callback(result); - } else { - throw new Error( - `The service "${payload.service}" is not present in the legacy owl environment. - You should probably create a mapper in @web/legacy/utils` - ); - } - } else { - payload.__targetWidget = ev.target; - this.trigger(evType.replace(/_/g, '-'), payload); - } - } - - get el() { - if (this.widget) { - return this.widget.el; - } - return super.el; - } - } - - const bodyRef = { get el() { return document.body } }; - export function standaloneAdapter(props = {}, ref = bodyRef) { - const env = owl.Component.env; - const app = new App(null, { - templates, - env, - dev: !!odoo.debug, - translatableAttributes: ["data-tooltip"], - translateFn: _t, - }); - if (!("Component" in props)) { - props.Component = owl.Component; - } - const component = app.makeNode(ComponentAdapter, props).component; - Object.defineProperty(component, "el", { - get() { - return ref.el; - } - }); - return component; - } - - /** - * Case 2) A legacy widget has to instantiate Owl components - * --------------------------------------------------------- - * - * The WidgetAdapterMixin and the ComponentWrapper are meant to be used - * together when an Odoo legacy widget needs to instantiate Owl components. - * In this case, the widgets/components hierarchy would look like: - * - * Legacy Widget + WidgetAdapterMixin - * | - * ComponentWrapper (Owl component) - * | - * Owl Component - * - * In this case, the parent legacy widget must use the WidgetAdapterMixin, - * which ensures that Owl hooks (mounted, willUnmount, destroy...) are - * properly called on the sub components. Moreover, it must instantiate a - * ComponentWrapper, and provide it the Owl component class to use alongside - * its props. This wrapper will ensure that the Owl component will be - * correctly updated (with willUpdateProps) like it would be if it was embed - * in an Owl hierarchy. Moreover, this wrapper automatically redirects all - * events triggered by the Owl component (or its descendants) to legacy - * custom events (trigger_up) on the parent legacy widget. - - * For example: - * class MyComponent extends Component {} - * MyComponent.template = xml`
Owl component with value
`; - * const MyWidget = Widget.extend(WidgetAdapterMixin, { - * start() { - * this.component = new ComponentWrapper(this, MyComponent, {value: 44}); - * return this.component.mount(this.el); - * }, - * update() { - * return this.component.update({value: 45}); - * }, - * }); - */ - export const WidgetAdapterMixin = { - /** - * Calls on_attach_callback on each child ComponentWrapper, which will - * call __callMounted on each sub component (recursively), to mark them - * as mounted. - */ - on_attach_callback() { - for (const component of children.get(this) || []) { - component.on_attach_callback(); - } - }, - /** - * Calls on_detach_callback on each child ComponentWrapper, which will - * call __callWillUnmount to mark itself and its children as no longer - * mounted. - */ - on_detach_callback() { - for (const component of children.get(this) || []) { - component.on_detach_callback(); - } - }, - /** - * Destroys each sub component when the widget is destroyed. We call the - * private __destroy function as there is no need to remove the el from - * the DOM (will be removed alongside this widget). - */ - destroy() { - for (const wrapper of children.get(this) || []) { - wrapper.destroy(); - } - children.delete(this); - }, - }; - - //----------------------------------// - // Low-level coordination functions // - //----------------------------------// - - /** - * Calls "callback" recursively on a ComponentNode's hierarchy - * - * @param {ComponentNode} - * @param {Boolean} childrenFirst whether to execute on the bottom-most child first - * @param {Function} callback - */ - function recursiveCall(node, childrenFirst = false, callback) { - if (!childrenFirst) { - callback(node); - } - for (const child of Object.values(node.children)) { - recursiveCall(child, childrenFirst, callback); - } - if (childrenFirst) { - callback(node); - } - } - - /** - * Make the node able to distinguish between - * mounting in the DOM and mounting outside of if. - * @param {ComponentNode} node - */ - function prepareForFinish(node) { - const fiber = node.fiber; - const complete = fiber.complete; - fiber.complete = function () { - // if target is not in dom - // just trigger mounted hooks on the Proxy, not on any other node - if (!this.target.ownerDocument.contains(this.target)) { - this.mounted = [this]; - // We skipped a bunch of mounted calls. - // Following calls to patched may crash because of this. - // (e.g. useEffect dependencies set in mounted and used in patched) - } - complete.call(this); - }; - } - - const nodesToRemount = new WeakMap(); - /** - * Pushed a node into the nodesToRemount WeakMap. - * The value is a callback that sets the node for remounting - * @param {ComponentNode} node - * @param {function} updateAndRender The original node's prototype's - */ - function setToRemount(node, updateAndRender) { - let toRemount = true; - - if (!node.isPatched) { - node.isPatched = true; - node.mounted.push(() => { - toRemount = false; - }); - node.willUpdateProps.push(() => { - const rootMounted = node.fiber.root.mounted; - if (toRemount && !rootMounted.includes(node.fiber)) { - rootMounted.push(node.fiber); - } - }); - } - return () => toRemount = true; - } - /** - * Make the node able to remount its children nodes correctly - * Typically, in that case we don't call willPatch and patched hooks, - * rather, we want to call the mounted hooks - * @param {ComponentNode} mainNode - */ - function prepareForRemount(mainNode) { - const updateAndRender = mainNode.updateAndRender; - recursiveCall(mainNode, false, (node) => { - if (mainNode === node) { - return; - } - if (nodesToRemount.has(node)) { - nodesToRemount.get(node)(); - return; - } else { - nodesToRemount.set(node, setToRemount(node, updateAndRender)); - } - }); - } - - /** - * Registers a wrapper instance as a child of the given parent in the - * 'children' weakMap. - * - * @private - * @param {Widget} parent - */ - function registerWrapper(parent, wrapper) { - let parentChildren = children.get(parent); - if (!parentChildren) { - parentChildren = []; - children.set(parent, parentChildren); - } - parentChildren.push(wrapper); - } - - /** - * The component class that will be instanciated between a legacy and an OWL 2 layer. - */ - class ProxyComponent extends LegacyComponent { - setup() { - for (const [hookName, cb] of Object.entries(this.props.hooks)) { - owl[hookName](cb); - } - onWillUnmount(() => { - // The current el will be change if we remount after unmounting - this._handledEvents = new Set(); - }); - this.parentWidget = this.props.parentWidget; - this._handledEvents = new Set(); - useSubEnv({ - [widgetSymbol]: this._addListener.bind(this) - }); - } - /** - * Adds an event handler that will redirect the given Owl event to an - * Odoo legacy event. This function is called just before the event is - * actually triggered. - * - * @private - * @param {string} evType - */ - _addListener(evType) { - if (this.parentWidget && !this._handledEvents.has(evType) && status(this) === "mounted") { - this._handledEvents.add(evType); - this.el.addEventListener(evType, ev => { - // as the WrappeComponent has the same root node as the - // actual sub Component, we have to check that the event - // hasn't been stopped by that component (it would naturally - // call stopPropagation, whereas it should actually call - // stopImmediatePropagation to prevent from getting here) - if (!ev.cancelBubble) { - ev.stopPropagation(); - const detail = Object.assign({}, ev.detail, { - __originalComponent: ev.originalComponent, - }); - this.parentWidget.trigger_up(ev.type.replace(/-/g, '_'), detail); - } - }); - } - } - } - ProxyComponent.template = xml``; - - export class ComponentWrapper { - constructor(parent, Component, props) { - if (parent instanceof Component) { - throw new Error("ComponentWrapper must be used with a legacy Widget as parent"); - } - this.setParent(parent); - const _env = props.env; - delete props.env; - this.props = props; - - this.Component = Component; - - const env = _env || owl.Component.env; - const appConfig = { - env, - templates, - dev: env.debug, - translatableAttributes: ["data-tooltip"], - translateFn: _t, - }; - const app = new App(null, appConfig); - this.app = app; - this.node = this._makeOwlNode(); - this.env = this.node.component.env; - app.root = this.node; - this.__owl__ = Object.create(this.node); - this.componentRef = { comp: null }; - this.status = "new"; - this.setup(); - } - - _makeOwlNode() { - const resolveRender = () => { - if (this.renderResolve) { - this.renderResolve(); - } - }; - - const props = { - props: this.props, - hooks: { - onMounted: resolveRender, - onPatched: resolveRender, - onRendered: () => { - this.componentRef.comp = Object.values(this.node.children)[0].component; - }, - }, - Component: this.Component, - parentWidget: this.parentWidget, - }; - return this.app.makeNode(ProxyComponent, props); - } - - setup() { } - - get el() { - return this.node.component.el; - } - - //------------------// - // OWL 1 - like API // - //------------------// - - async mount(target, options) { - if (this.status === "mounted" || this.status === "willMount") { - return this.render(); - } else if (this.status === "destroyed") { - return; - } - if (target) { - this.target = target; - } - if (options) { - this.mountOptions = options; - } - const remount = this.status === "unmounted"; - if (remount) { - prepareForRemount(this.node); - } - this.status = "willMount"; - const prom = this.app.mountNode(this.node, this.target, this.mountOptions); - if (remount) { - this.node.fiber.deep = true; - } - prepareForFinish(this.node); - await prom; - if (this.target.ownerDocument.contains(this.target)) { - this.status = "mounted"; - } - this.node.willStart = []; - // remove the promise.resolve from mounted callbacks - const mounted = this.node.mounted; - this.node.mounted = mounted.slice(0, mounted.length - 1); - return this; - } - - unmount() { - this.on_detach_callback(); - this.el.remove(); - this.node.bdom = null; - } - - render() { - if (this.status !== "mounted") { - return; - } - if (this.renderProm) { - this.node.render(true); - return this.renderProm; - } - this.renderProm = new Promise((resolve, reject) => { - this.renderResolve = resolve; - this.renderReject = reject; - }).then(() => { - this.renderProm = null; - this.renderResolve = null; - this.renderReject = null; - }); - this.node.render(true); - return this.renderProm; - } - - trigger() { - return this.node.component.trigger(...arguments); - } - - destroy() { - if (["willMount", "unmounted"].includes(this.status)) { - recursiveCall(this.node, false, (node) => { - node.willUnmount = []; - }); - } - this.app.destroy(); - this.status = "destroyed"; - } - - //---------------------// - // API from legacy POV // - //---------------------// - - get $el() { - return $(this.el); - } - - on_attach_callback() { - if (!this.el || !this.el.ownerDocument.contains(this.el)) { - return; - } - if (this.status === "mounted") { - return; - } - recursiveCall(this.node, true, (node) => { - for (const cb of node.mounted) { - cb(); - } - }); - this.node.status = 1; - this.status = "mounted"; - } - - /** - * Calls willUnmount to notify the component it will be unmounted. - */ - on_detach_callback() { - if (this.status === "unmounted") { - return; - } - recursiveCall(this.node, false, (node) => { - // node.status might be "new" (0) here, if the component is - // currently being re-rendered, and a new component has just - // been instantiated, but as the rendering isn't completed, it - // isn't mounted yet - if (node.status === 1) { - const component = node.component; - for (const cb of node.willUnmount) { - cb.call(component); - } - } - }); - this.node.status = 0; - this.status = "unmounted"; - } - - async update(nextProps) { - if (this.status === "destroyed") { - return; - } - const props = this.node.component.props.props; - const nextComponentProps = Object.assign({}, props, nextProps); - this.node.component.props.props = nextComponentProps; - this.props = nextComponentProps; - if (this.status === "unmounted") { - return this.mount(this.target); - } else { - return this.render(); - } - } - - setParent(parent) { - if (parent instanceof Component) { - throw new Error('ComponentWrapper must be used with a legacy Widget as parent'); - } - if (parent) { - registerWrapper(parent, this); - } - if (this.parentWidget) { - const parentChildren = children.get(this.parentWidget); - parentChildren.splice(parentChildren.indexOf(this), 1); - } - - this.parentWidget = parent; - if (this.node) { - this.node.component.parentWidget = parent; - } - } - } - - export default { - ComponentAdapter, - ComponentWrapper, - WidgetAdapterMixin, - standaloneAdapter, - }; diff --git a/addons/web/static/src/legacy/js/public/public_root.js b/addons/web/static/src/legacy/js/public/public_root.js index e2182d25474..2076849f3e1 100644 --- a/addons/web/static/src/legacy/js/public/public_root.js +++ b/addons/web/static/src/legacy/js/public/public_root.js @@ -13,8 +13,8 @@ import { makeLegacyDialogMappingService, mapLegacyEnvToWowlEnv, makeLegacyRPCService, + createWidgetParent, } from "../../utils"; -import { standaloneAdapter } from "@web/legacy/js/owl_compatibility"; import { makeEnv, startServices } from "@web/env"; import { loadJS, templates } from '@web/core/assets'; @@ -24,7 +24,7 @@ import { jsonrpc } from '@web/core/network/rpc_service'; import { renderToString } from "@web/core/utils/render"; import { _t } from "@web/core/l10n/translation"; import { omit } from "@web/core/utils/objects"; -import { Component, App, whenReady } from "@odoo/owl"; +import { App, whenReady } from "@odoo/owl"; import { getOrigin } from '@web/core/utils/urls'; const { Settings } = luxon; @@ -395,8 +395,7 @@ export async function createPublicRoot(RootWidget) { await startServices(wowlEnv); mapLegacyEnvToWowlEnv(legacyEnv, wowlEnv); - // The root widget's parent is a standalone adapter so that it has _trigger_up - const publicRoot = new RootWidget(standaloneAdapter({ Component })); + const publicRoot = new RootWidget(createWidgetParent(legacyEnv)); const app = new App(MainComponentsContainer, { templates, env: wowlEnv, diff --git a/addons/web/static/src/legacy/legacy_component.js b/addons/web/static/src/legacy/legacy_component.js deleted file mode 100644 index b88e5b338a4..00000000000 --- a/addons/web/static/src/legacy/legacy_component.js +++ /dev/null @@ -1,95 +0,0 @@ -/** @odoo-module **/ - -/** - * @deprecated - * This component SHOULD NOT be used! - * It's used in legacy when components should work like in owl 1. - * - * If you need to get the component's `el` then use `useRef` - * and if you need to send info to the component's parent then use props. - * - * e.g. - * // OWL 1 - * class Component_OWL1 extends owl.Component { - * static template = owl.tags.xml` - * - * `; - * mounted() { - * this.el.style = "color: red"; - * } - * } - * - * // OWL 2 - * class Component_OWL2 extends owl.Component { - * static template = owl.xml` - * - * `; - * setup() { - * this.btnRef = useRef("btn"); - * owl.onMounted(() => { - * this.btnRef.el.style = "color: red"; - * }); - * } - * } - */ -export class LegacyComponent extends owl.Component { - get el() { - const bdom = this.__owl__.bdom; - if (!bdom) { - return null; - } - - const el = (bdom.component && bdom.component.el) || bdom.firstNode(); - return el.nodeType === Node.ELEMENT_NODE ? el : null; - } - /** - * Add a new method to owl Components to ensure that no performed RPC is - * resolved/rejected when the component is destroyed. - */ - rpc() { - return new Promise((resolve, reject) => { - return this.env.services - .rpc(...arguments) - .then((result) => { - if (owl.status(this) !== "destroyed") { - resolve(result); - } - }) - .catch((reason) => { - if (owl.status(this) !== "destroyed") { - reject(reason); - } - }); - }); - } - - /** - * Emit a custom event of type 'eventType' with the given 'payload' on the - * component's el, if it exists. However, note that the event will only bubble - * up to the parent DOM nodes. Thus, it must be called between mounted() and - * willUnmount(). - */ - trigger(eventType, payload) { - this.__trigger(eventType, payload); - } - /** - * Private trigger method, allows to choose the component which triggered - * the event in the first place - */ - __trigger(eventType, payload) { - if (this.env[odoo.widgetSymbol]) { - this.env[odoo.widgetSymbol](eventType); - } - if (this.el) { - const ev = new CustomEvent(eventType, { - bubbles: true, - cancelable: true, - detail: payload, - }); - this.el.dispatchEvent(ev); - } - } -} -LegacyComponent.props = { - "*": true, -}; diff --git a/addons/web/static/src/legacy/utils.js b/addons/web/static/src/legacy/utils.js index bdff1ea1dc6..3dd6690f13c 100644 --- a/addons/web/static/src/legacy/utils.js +++ b/addons/web/static/src/legacy/utils.js @@ -2,8 +2,20 @@ import { browser } from "../core/browser/browser"; import { useService } from "@web/core/utils/hooks"; -import { Component, useComponent, xml } from "@odoo/owl"; -import { loadJS } from "@web/core/assets"; +import { + App, + Component, + onMounted, + onWillStart, + onWillUnmount, + useComponent, + useEnv, + useRef, + useState, + xml, +} from "@odoo/owl"; +import { loadJS, templates } from "@web/core/assets"; +import { _t } from "@web/core/l10n/translation"; import { ConnectionAbortedError, ConnectionLostError, @@ -234,3 +246,82 @@ export function useWowlService(serviceName) { component.env = env; return service; } + +export function createWidgetParent(env) { + return { + env, + _trigger_up: (ev) => { + if (ev.name === "call_service") { + let args = ev.data.args || []; + if (ev.data.service === "ajax" && ev.data.method === "rpc") { + // ajax service uses an extra 'target' argument for rpc + args = args.concat(ev.target); + } + const service = env.services[ev.data.service]; + const result = service[ev.data.method].apply(service, args); + ev.data.callback(result); + } + }, + }; +} + +export function useWidget(refName, widgetClass, params = []) { + const ref = useRef(refName); + const env = useEnv(); + + const parent = createWidgetParent(env); + const widget = new widgetClass(parent, ...params); + + onWillStart(() => { + return widget._widgetRenderAndInsert(() => {}); + }); + onMounted(() => { + ref.el.append(widget.el); + }); + onWillUnmount(() => { + widget.destroy(); + }); + + return widget; +} + +const rootTemplate = xml``; +export async function attachComponent(parent, element, componentClass, props = {}) { + class Root extends Component { + static template = rootTemplate; + static components = { SubComp: componentClass }; + state = useState(props); + } + + const env = Component.env; + const app = new App(Root, { + env, + templates, + dev: env.debug, + translatableAttributes: ["data-tooltip"], + translateFn: _t, + }); + + if (parent.__parentedMixin) { + parent.__parentedChildren.push({ + get $el() { + return $(app.root.el); + }, + destroy() { + app.destroy(); + }, + }); + } + + const component = await app.mount(element); + const subComp = Object.values(component.__owl__.children)[0].component; + return { + component: subComp, + destroy() { + app.destroy(); + }, + update(props) { + Object.assign(component.state, props); + }, + }; +} diff --git a/addons/web/static/tests/legacy/component_extension_tests.js b/addons/web/static/tests/legacy/component_extension_tests.js deleted file mode 100644 index 762c4d1cfb3..00000000000 --- a/addons/web/static/tests/legacy/component_extension_tests.js +++ /dev/null @@ -1,48 +0,0 @@ -/** @odoo-module **/ - - import makeTestEnvironment from "@web/../tests/legacy/helpers/test_env"; - import testUtils from "@web/../tests/legacy/helpers/test_utils"; - import { destroy, getFixture, mount } from "@web/../tests/helpers/utils"; - import { LegacyComponent } from "@web/legacy/legacy_component"; - - const { xml } = owl; - - let target; - QUnit.module("web", { beforeEach() { target = getFixture(); }}, function () { - QUnit.module("Component Extension"); - - QUnit.test("Component destroyed while performing successful RPC", async function (assert) { - assert.expect(1); - - class Parent extends LegacyComponent {} - Parent.template = xml`
`; - - const env = makeTestEnvironment({}, () => Promise.resolve()); - - const parent = await mount(Parent, target, { env }); - - parent.rpc({}).then(() => { throw new Error(); }); - destroy(parent); - - await testUtils.nextTick(); - - assert.ok(true, "Promise should still be pending"); - }); - - QUnit.test("Component destroyed while performing failed RPC", async function (assert) { - assert.expect(1); - - class Parent extends LegacyComponent {} - Parent.template = xml`
`; - - const env = makeTestEnvironment({}, () => Promise.reject()); - const parent = await mount(Parent, target, { env }); - - parent.rpc({}).catch(() => { throw new Error(); }); - destroy(parent); - - await testUtils.nextTick(); - - assert.ok(true, "Promise should still be pending"); - }); - }); diff --git a/addons/web/static/tests/legacy/owl_compatibility_tests.js b/addons/web/static/tests/legacy/owl_compatibility_tests.js index f664740bdde..f7dd0306a72 100644 --- a/addons/web/static/tests/legacy/owl_compatibility_tests.js +++ b/addons/web/static/tests/legacy/owl_compatibility_tests.js @@ -1,1559 +1,128 @@ /** @odoo-module **/ - import { - ComponentAdapter, - ComponentWrapper, - WidgetAdapterMixin, - } from "@web/legacy/js/owl_compatibility"; - import testUtils from "@web/../tests/legacy/helpers/test_utils"; - import Widget from "@web/legacy/js/core/widget"; import { registry } from "@web/core/registry"; - import { LegacyComponent } from "@web/legacy/legacy_component"; - import { mapLegacyEnvToWowlEnv, useWowlService } from "@web/legacy/utils"; + import { attachComponent, mapLegacyEnvToWowlEnv, useWidget, useWowlService } from "@web/legacy/utils"; import makeTestEnvironment from "@web/../tests/legacy/helpers/test_env"; import { makeTestEnv } from "@web/../tests/helpers/mock_env"; - import { getFixture, mount, useLogLifeCycle, destroy } from "@web/../tests/helpers/utils"; - - const makeTestPromise = testUtils.makeTestPromise; - const nextTick = testUtils.nextTick; - - const { + import { getFixture, mount } from "@web/../tests/helpers/utils"; + import Widget from "@web/legacy/js/core/widget"; + import { Component, - EventBus, - onError, onMounted, + onPatched, onWillDestroy, onWillStart, - onWillUnmount, onWillUpdateProps, + useRef, useState, xml, - } = owl; - - const WidgetAdapter = Widget.extend(WidgetAdapterMixin, { - destroy() { - this._super(...arguments); - WidgetAdapterMixin.destroy.call(this, ...arguments); - }, - }); + } from "@odoo/owl"; + import { nextTick } from "./helpers/test_utils"; QUnit.module("Owl Compatibility", function () { - QUnit.module("ComponentAdapter"); + QUnit.test("useWidget", async (assert) => { + assert.expect(9); - 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 LegacyComponent { - constructor() { - super(...arguments); - this.MyWidget = MyWidget; - } - } - Parent.template = xml` -
- -
`; - Parent.components = { ComponentAdapter }; - - const target = getFixture(); - const parent = await mount(Parent, target); - - assert.strictEqual(parent.el.innerHTML, '
Hello World!
'); - }); - - 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 LegacyComponent { - constructor() { - super(...arguments); - this.MyWidget = MyWidget; - } - } - Parent.template = xml` -
- -
`; - Parent.components = { ComponentAdapter }; - - const target = getFixture(); - const parent = await mount(Parent, target); - - assert.strictEqual(parent.el.innerHTML, '
Hello World!
'); - }); - - 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 LegacyComponent { - setup() { - this.MyWidget = MyWidget; - this.error = false; - onError((e) => { - assert.strictEqual( - e.toString(), - // eslint-disable-next-line no-useless-escape - `Error: The following error occurred in onWillStart: \"ComponentAdapter has more than 1 argument, 'widgetArgs' must be overriden.\"` - ); - this.error = true; - this.render(); - }); - } - } - Parent.template = xml` -
- Error - -
`; - Parent.components = { ComponentAdapter }; - - const target = getFixture(); - await mount(Parent, target); - }); - - 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 LegacyComponent { - constructor() { - super(...arguments); - this.MyWidget = MyWidget; - } - } - Parent.template = xml` -
- -
`; - Parent.components = { MyWidgetAdapter }; - - const target = getFixture(); - const parent = await mount(Parent, target); - - assert.strictEqual(parent.el.innerHTML, '
Hello World!
'); - }); - - 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 LegacyComponent { - constructor() { - super(...arguments); - this.MyWidget = MyWidget; - } - } - Parent.template = xml` -
- -
`; - Parent.components = { ComponentAdapter }; - - const target = getFixture(); - const parent = await mount(Parent, target); - - assert.strictEqual(parent.el.innerHTML, '
Hello World!
'); - }); - - 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 { - updateWidget(nextProps) { - return this.widget.update(nextProps.name); - } - renderWidget() { - this.widget.render(); - } - } - class Parent extends LegacyComponent { - constructor() { - super(...arguments); - this.MyWidget = MyWidget; - this.state = useState({ - name: "World", - }); - } - } - Parent.template = xml` -
- -
`; - Parent.components = { MyWidgetAdapter }; - - const target = getFixture(); - const parent = await mount(Parent, target); - - assert.strictEqual(parent.el.innerHTML, '
Hello World!
'); - - parent.state.name = "GED"; - await nextTick(); - - assert.strictEqual(parent.el.innerHTML, '
Hello GED!
'); - }); - - 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 { - updateWidget(nextProps) { - return this.widget.update(nextProps.name); - } - renderWidget() { - this.widget.render(); - } - } - class AsyncComponent extends LegacyComponent { - setup() { - onWillUpdateProps(() => prom); - } - } - AsyncComponent.template = xml`
Hi !
`; - class Parent extends LegacyComponent { - constructor() { - super(...arguments); - this.MyWidget = MyWidget; - this.state = useState({ - name: "World", - }); - } - } - Parent.template = xml` -
- - -
`; - Parent.components = { AsyncComponent, MyWidgetAdapter }; - - const target = getFixture(); - const parent = await mount(Parent, target); - - assert.strictEqual(parent.el.innerHTML, '
Hi World!
Hello World!
'); - - parent.state.name = "GED"; - await nextTick(); - - assert.strictEqual(parent.el.innerHTML, '
Hi World!
Hello World!
'); - - prom.resolve(); - await nextTick(); - - assert.strictEqual(parent.el.innerHTML, '
Hi GED!
Hello GED!
'); - - assert.verifySteps(['render', 'update', 'render']); - }); - - QUnit.test("sub widget methods are correctly called", async function (assert) { - assert.expect(6); - - const MyWidget = Widget.extend({ - on_attach_callback: function () { - assert.step('on_attach_callback'); - }, - on_detach_callback: function () { - assert.ok(document.body.contains(this.el)); - assert.step('on_detach_callback'); - }, - destroy: function () { - assert.step('destroy'); - this._super.apply(this, arguments); - }, - }); - class Parent extends LegacyComponent { - constructor() { - super(...arguments); - this.MyWidget = MyWidget; - } - } - Parent.template = xml` -
- -
`; - Parent.components = { ComponentAdapter }; - - const target = getFixture(); - const parent = await mount(Parent, target); - - assert.verifySteps(['on_attach_callback']); - - destroy(parent); - - 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 LegacyComponent {} - MyComponent.template = xml`
component
`; - class Parent extends LegacyComponent { - constructor() { - super(...arguments); - this.Children = [MyWidget, MyComponent]; - } - } - Parent.template = xml` -
- -
`; - Parent.components = { ComponentAdapter }; - - const target = getFixture(); - const parent = await mount(Parent, target); - - assert.strictEqual(parent.el.innerHTML, '
widget
component
'); - }); - - 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); + let widget = null; + const CustomWidget = Widget.extend({ + init(_, ...params) { + this._super(...arguments); widget = this; + assert.step("widget initialized"); + assert.deepEqual(params, ["a", 1]); + this.params = params; }, - }); - class Parent extends LegacyComponent { - constructor() { - super(...arguments); - this.MyWidget = MyWidget; - } - onSomeEvent(ev) { - assert.step(ev.detail.value); - assert.ok(ev.detail.__targetWidget instanceof MyWidget); - } - } - Parent.template = xml` -
- -
`; - Parent.components = { ComponentAdapter }; - - const target = getFixture(); - await mount(Parent, target); - - widget.trigger_up('some-event', { value: 'a' }); - widget.trigger_up('some_event', { value: 'b' }); // _ are converted to - - - assert.verifySteps(['a', 'b']); - }); - - 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 LegacyComponent { - constructor() { - super(...arguments); - this.MyWidget = MyWidget; - } - } - Parent.template = xml` -
- -
`; - Parent.components = { ComponentAdapter }; - - const env = { - services: { - math: { - sqrt: v => Math.sqrt(v), - } - } - }; - - const target = getFixture(); - await mount(Parent, target, { env }); - }); - - 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 LegacyComponent { - constructor() { - super(...arguments); - this.MyWidget1 = MyWidget1; - this.MyWidget2 = MyWidget2; - this.state = useState({ - flag: true, - }); - } - } - Parent.template = xml` -
- - -
`; - Parent.components = { ComponentAdapter }; - - const target = getFixture(); - const parent = await mount(Parent, target); - - assert.strictEqual(parent.el.innerHTML, '
Hi
'); - - parent.state.flag = false; - await nextTick(); - - assert.strictEqual(parent.el.innerHTML, '
Hello
'); - - parent.state.flag = true; - await nextTick(); - - assert.strictEqual(parent.el.innerHTML, '
Hi
'); - }); - - 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 LegacyComponent { - constructor() { - super(...arguments); - this.MyWidget = MyWidget; - this.state = useState({ - flag: true, - }); - } - onSomeEvent(ev) { - assert.step(ev.detail.value); - } - } - Parent.template = xml` -
- -
`; - Parent.components = { ComponentAdapter }; - - const target = getFixture(); - const parent = await mount(Parent, target); - - assert.strictEqual(parent.el.innerHTML, '
Hi
'); - 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, '
Hi
'); - myWidget.trigger_up('some-event', { value: 'c' }); - - assert.verifySteps(['a', 'c']); - }); - - QUnit.test("adapter contains the el of sub widget as firstChild (modify)", async function (assert) { - assert.expect(7); - - let myWidget; - const MyWidget = Widget.extend({ - events: { - click: "_onClick", - }, - init: function (parent, name) { - myWidget = this; - this._super.apply(this, arguments); - this.name = name; - }, - start: function () { - this.render(); - }, - render: function () { - this.$el.text("Click me!"); - }, - update: function (name) { - this.name = name; - }, - _onClick: function () { - assert.step(this.name); - }, - }); - class MyWidgetAdapter extends ComponentAdapter { - updateWidget(nextProps) { - return this.widget.update(nextProps.name); - } - renderWidget() { - this.widget.render(); - } - } - class Parent extends LegacyComponent { - constructor() { - super(...arguments); - this.MyWidget = MyWidget; - this.state = useState({ - name: "GED", - }); - } - } - Parent.template = xml` - - `; - Parent.components = { MyWidgetAdapter }; - - const target = getFixture(); - const parent = await mount(Parent, target); - - const widgetEl = myWidget.el; - - assert.strictEqual(target.firstChild, widgetEl); - await testUtils.dom.click(widgetEl); - - parent.state.name = "AAB"; - await nextTick(); - - assert.strictEqual(widgetEl, myWidget.el); - await testUtils.dom.click(widgetEl); - - parent.state.name = "MCM"; - await nextTick(); - - assert.strictEqual(widgetEl, myWidget.el); - await testUtils.dom.click(widgetEl); - - assert.verifySteps(["GED", "AAB", "MCM"]); - }); - - QUnit.test("adapter handles a widget that replaces its el", async function (assert) { - assert.expect(10); - - let renderId = 0; - const MyWidget = Widget.extend({ - events: { - click: "_onClick", - }, - init: function (parent, name) { - this._super.apply(this, arguments); - this.name = name; - }, - start: function () { - this.render(); - }, - render: function () { - this._replaceElement("
Click me!
"); - this.el.classList.add(`widget_id_${renderId++}`); - }, - update: function (name) { - this.name = name; - }, - _onClick: function () { - assert.step(this.name); - }, - }); - class MyWidgetAdapter extends ComponentAdapter { - updateWidget(nextProps) { - return this.widget.update(nextProps.name); - } - renderWidget() { - this.widget.render(); - } - } - class Parent extends LegacyComponent { - constructor() { - super(...arguments); - this.MyWidget = MyWidget; - this.state = useState({ - name: "GED", - }); - } - } - Parent.template = xml` - - `; - Parent.components = { MyWidgetAdapter }; - - const target = getFixture(); - const parent = await mount(Parent, target); - - assert.containsOnce(target, ".widget_id_0"); - - await testUtils.dom.click(target.querySelector(".widget_id_0")); - - parent.state.name = "AAB"; - await nextTick(); - - assert.containsNone(target, ".widget_id_0"); - assert.containsOnce(target, ".widget_id_1"); - await testUtils.dom.click(target.querySelector(".widget_id_1")); - - parent.state.name = "MCM"; - await nextTick(); - - assert.containsNone(target, ".widget_id_0"); - assert.containsNone(target, ".widget_id_1"); - assert.containsOnce(target, ".widget_id_2"); - await testUtils.dom.click(target.querySelector(".widget_id_2")); - - assert.verifySteps(["GED", "AAB", "MCM"]); - }); - - QUnit.module('WidgetAdapterMixin and ComponentWrapper'); - - QUnit.test("widget with sub component", async function (assert) { - assert.expect(2); - - let component; - let wrapper; - class MyComponent extends LegacyComponent { - setup() { - component = this; - } - } - MyComponent.template = xml`
Component
`; - const MyWidget = WidgetAdapter.extend({ start() { - wrapper = new ComponentWrapper(this, MyComponent, {}); - return wrapper.mount(this.el); + this.$el.text("Hello World!"); + }, + callTriggerUp() { + this.call("test", "call", { [this.params[0]]: this.params[1] }); }, }); + class ComponentAdapter extends Component { + static template = xml`
`; + setup() { + this.containerRef = useRef("container"); + this.widget = useWidget("container", CustomWidget, ["a", 1]); + assert.strictEqual(this.widget, widget); + } + } + + class Toggle extends Component { + static components = { ComponentAdapter }; + static template = xml``; + state = useState({ active: true }); + } + const target = getFixture(); - const widget = new MyWidget(); - await widget.appendTo(target); + registry.category("services").add("test", { + start: () => ({ + call: (p) => { + assert.step("triggered up"); + assert.deepEqual(p, { a: 1 }); + }, + }), + }); + const component = await mount(Toggle, target, { + env: await makeTestEnv(), + }); + assert.verifySteps(["widget initialized"]); - assert.strictEqual(widget.el.innerHTML, "
Component
"); - assert.strictEqual(wrapper.componentRef.comp, component); + assert.strictEqual( + target.querySelector("#adapter").textContent, + "Hello World!" + ); - widget.destroy(); + widget.callTriggerUp(); + assert.verifySteps(["triggered up"]); + + component.state.active = false; + await nextTick(); + assert.ok(widget.isDestroyed()); }); - QUnit.test("sub component hooks are correctly called", async function (assert) { + QUnit.test("attachComponent", async (assert) => { assert.expect(13); - class MyComponent extends LegacyComponent { + class CustomComponent extends Component { + static template = xml`
`; setup() { assert.step("setup"); - onWillStart(() => { - assert.step("willStart"); - }); - onMounted(() => { - assert.step("mounted"); - }); - onWillUnmount(() => { - assert.step("willUnmount"); - }); - onWillDestroy(() => { - assert.step("willDestroy"); - }); + onWillStart(() => assert.step("onWillStart")); + onMounted(() => assert.step("onMounted")); + onWillUpdateProps(() => assert.step("onWillUpdateProps")); + onPatched(() => assert.step("onPatched")); + onWillDestroy(() => assert.step("onWillDestroy")); } } - MyComponent.template = xml`
Component
`; - const MyWidget = WidgetAdapter.extend({ - start() { - const component = new ComponentWrapper(this, MyComponent, {}); - return component.mount(this.el); + + const CustomWidget = Widget.extend({ + async start() { + this.el.id = "widget"; + this.component = await attachComponent(this, this.el, CustomComponent, { text: "Hello" }); }, }); const target = getFixture(); - const widget = new MyWidget(); + const widget = new CustomWidget(); await widget.appendTo(target); - assert.verifySteps(["setup", "willStart", "mounted"]); - assert.strictEqual(widget.el.innerHTML, "
Component
"); + assert.verifySteps(["setup", "onWillStart", "onMounted"]); + assert.strictEqual(target.querySelector("#widget > #component").textContent, "Hello"); - widget.$el.detach(); - widget.on_detach_callback(); + widget.component.update({ text: "World" }); + await nextTick(); + assert.verifySteps(["onWillUpdateProps", "onPatched"]); + assert.strictEqual(target.querySelector("#widget > #component").textContent, "World"); - assert.verifySteps(["willUnmount"]); - - widget.$el.appendTo(target); - widget.on_attach_callback(); - - assert.verifySteps(["mounted"]); - assert.strictEqual(widget.el.innerHTML, "
Component
"); + widget.component.destroy(); + assert.verifySteps(["onWillDestroy"]); + assert.containsOnce(target, "#widget"); + assert.containsNone(target, "#widget > #component"); widget.destroy(); - - assert.verifySteps(["willUnmount", "willDestroy"]); - }); - - QUnit.test("sub component hooks are correctly called when appended in iframe", async function (assert) { - assert.expect(13); - - class MyComponent extends LegacyComponent { - setup() { - assert.step("setup"); - onWillStart(() => { - assert.step("willStart"); - }); - onMounted(() => { - assert.step("mounted"); - }); - onWillUnmount(() => { - assert.step("willUnmount"); - }); - onWillDestroy(() => { - assert.step("willDestroy"); - }); - } - } - MyComponent.template = xml`
Component
`; - const MyWidget = WidgetAdapter.extend({ - start() { - const component = new ComponentWrapper(this, MyComponent, {}); - return component.mount(this.el); - }, - }); - - const widget = new MyWidget(); - const target = getFixture(); - const iframe = document.createElement("iframe"); - target.appendChild(iframe); - await widget.appendTo(iframe.contentWindow.document.body); - - assert.verifySteps(["setup", "willStart", "mounted"]); - assert.strictEqual(widget.el.innerHTML, "
Component
"); - - widget.$el.detach(); - widget.on_detach_callback(); - - assert.verifySteps(["willUnmount"]); - - widget.$el.appendTo(iframe.contentWindow.document.body); - widget.on_attach_callback(); - - assert.verifySteps(["mounted"]); - assert.strictEqual(widget.el.innerHTML, "
Component
"); - - widget.destroy(); - - assert.verifySteps(["willUnmount", "willDestroy"]); - }); - - QUnit.test("lifecycle with several sub components", async function (assert) { - assert.expect(21); - - class MyComponent extends LegacyComponent { - setup() { - useLogLifeCycle(assert.step.bind(assert), this.props.id); - } - } - MyComponent.template = xml`
Component
`; - const MyWidget = WidgetAdapter.extend({ - start() { - const c1 = new ComponentWrapper(this, MyComponent, {id: 1}); - const c2 = new ComponentWrapper(this, MyComponent, {id: 2}); - return Promise.all([c1.mount(this.el), c2.mount(this.el)]); - } - }); - - const target = getFixture(); - const widget = new MyWidget(); - await widget.appendTo(target); - - assert.strictEqual(widget.el.innerHTML, '
Component 1
Component 2
'); - assert.verifySteps([ - "onWillStart MyComponent 1", - "onWillStart MyComponent 2", - "onWillRender MyComponent 1", - "onRendered MyComponent 1", - "onWillRender MyComponent 2", - "onRendered MyComponent 2", - "onMounted MyComponent 1", - "onMounted MyComponent 2" - ]); - - widget.$el.detach(); - widget.on_detach_callback(); - assert.verifySteps([ - "onWillUnmount MyComponent 1", - "onWillUnmount MyComponent 2" - ]); - - widget.$el.appendTo(target); - widget.on_attach_callback(); - assert.verifySteps([ - "onMounted MyComponent 1", - "onMounted MyComponent 2" - ]); - - widget.destroy(); - assert.verifySteps([ - "onWillUnmount MyComponent 1", - "onWillDestroy MyComponent 1", - "onWillUnmount MyComponent 2", - "onWillDestroy MyComponent 2" - ]); - }); - - QUnit.test("lifecycle with several levels of sub components", async function (assert) { - assert.expect(21); - - class MyChildComponent extends LegacyComponent { - setup() { - useLogLifeCycle(assert.step.bind(assert)); - } - } - MyChildComponent.template = xml`
child
`; - class MyComponent extends LegacyComponent { - setup() { - useLogLifeCycle(assert.step.bind(assert)); - } - } - MyComponent.template = xml`
`; - MyComponent.components = { MyChildComponent }; - const MyWidget = WidgetAdapter.extend({ - start() { - let component = new ComponentWrapper(this, MyComponent, {}); - return component.mount(this.el); - } - }); - - const target = getFixture(); - const widget = new MyWidget(); - await widget.appendTo(target); - - assert.strictEqual(widget.el.innerHTML, '
child
'); - assert.verifySteps([ - "onWillStart MyComponent", - "onWillRender MyComponent", - "onWillStart MyChildComponent", - "onRendered MyComponent", - "onWillRender MyChildComponent", - "onRendered MyChildComponent", - "onMounted MyChildComponent", - "onMounted MyComponent" - ]); - - widget.$el.detach(); - widget.on_detach_callback(); - assert.verifySteps([ - "onWillUnmount MyComponent", - "onWillUnmount MyChildComponent" - ]); - - widget.$el.appendTo(target); - widget.on_attach_callback(); - assert.verifySteps([ - "onMounted MyChildComponent", - "onMounted MyComponent" - ]); - - widget.destroy(); - assert.verifySteps([ - "onWillUnmount MyComponent", - "onWillUnmount MyChildComponent", - "onWillDestroy MyChildComponent", - "onWillDestroy MyComponent" - ]); - }); - - QUnit.test("lifecycle mount in fragment", async function (assert) { - assert.expect(17); - - class MyChildComponent extends LegacyComponent { - setup() { - useLogLifeCycle(assert.step.bind(assert)); - } - } - MyChildComponent.template = xml`
child
`; - class MyComponent extends LegacyComponent { - setup() { - useLogLifeCycle(assert.step.bind(assert)); - } - } - MyComponent.template = xml`
`; - MyComponent.components = { MyChildComponent }; - const MyWidget = WidgetAdapter.extend({ - start() { - let component = new ComponentWrapper(this, MyComponent, {}); - return component.mount(this.el); - } - }); - - const target = getFixture(); - const widget = new MyWidget(); - const fragment = document.createElement("div"); - await widget.appendTo(fragment); - - assert.strictEqual(widget.el.innerHTML, '
child
'); - assert.verifySteps([ - "onWillStart MyComponent", - "onWillRender MyComponent", - "onWillStart MyChildComponent", - "onRendered MyComponent", - "onWillRender MyChildComponent", - "onRendered MyChildComponent", - ]); - - widget.$el.appendTo(target); - widget.on_attach_callback(); - assert.verifySteps([ - "onMounted MyChildComponent", - "onMounted MyComponent" - ]); - - widget.on_attach_callback(); - assert.verifySteps([]); - - widget.destroy(); - assert.verifySteps([ - "onWillUnmount MyComponent", - "onWillUnmount MyChildComponent", - "onWillDestroy MyChildComponent", - "onWillDestroy MyComponent" - ]); - }); - - QUnit.test("lifecycle mount/unmount", async function (assert) { - assert.expect(37); - - class MyChildComponent extends LegacyComponent { - setup() { - useLogLifeCycle(assert.step.bind(assert)); - } - } - MyChildComponent.template = xml`
child
`; - class MyComponent extends LegacyComponent { - setup() { - useLogLifeCycle(assert.step.bind(assert)); - } - } - MyComponent.template = xml`
`; - MyComponent.components = { MyChildComponent }; - - const target = getFixture(); - const widget = new ComponentWrapper(null, MyComponent, {}); - await widget.mount(target); - - assert.strictEqual(target.innerHTML, "
child
"); - assert.verifySteps([ - "onWillStart MyComponent", - "onWillRender MyComponent", - "onWillStart MyChildComponent", - "onRendered MyComponent", - "onWillRender MyChildComponent", - "onRendered MyChildComponent", - "onMounted MyChildComponent", - "onMounted MyComponent" - ]); - - widget.unmount(); - assert.strictEqual(target.innerHTML, ""); - assert.verifySteps([ - "onWillUnmount MyComponent", - "onWillUnmount MyChildComponent", - ]); - - await widget.mount(); - - assert.verifySteps([ - "onWillUpdateProps MyComponent", - "onWillRender MyComponent", - "onWillUpdateProps MyChildComponent", - "onRendered MyComponent", - "onWillRender MyChildComponent", - "onRendered MyChildComponent", - "onMounted MyChildComponent", - "onMounted MyComponent" - ]); - assert.strictEqual(target.innerHTML, "
child
"); - - widget.unmount(); - assert.strictEqual(target.innerHTML, ""); - assert.verifySteps([ - "onWillUnmount MyComponent", - "onWillUnmount MyChildComponent", - ]); - - await widget.mount(); - - assert.verifySteps([ - "onWillUpdateProps MyComponent", - "onWillRender MyComponent", - "onWillUpdateProps MyChildComponent", - "onRendered MyComponent", - "onWillRender MyChildComponent", - "onRendered MyChildComponent", - "onMounted MyChildComponent", - "onMounted MyComponent" - ]); - }); - - QUnit.test("lifecycle mount/unmount/update", async function (assert) { - assert.expect(24); - - class MyChildComponent extends LegacyComponent { - setup() { - useLogLifeCycle(assert.step.bind(assert)); - } - } - MyChildComponent.template = xml`
child
`; - class MyComponent extends LegacyComponent { - setup() { - useLogLifeCycle(assert.step.bind(assert)); - } - } - MyComponent.template = xml`
`; - MyComponent.components = { MyChildComponent }; - - const target = getFixture(); - const widget = new ComponentWrapper(null, MyComponent, { - text: "takeNoMess" - }); - await widget.mount(target); - - assert.strictEqual(target.innerHTML, "
child takeNoMess
"); - assert.verifySteps([ - "onWillStart MyComponent", - "onWillRender MyComponent", - "onWillStart MyChildComponent", - "onRendered MyComponent", - "onWillRender MyChildComponent", - "onRendered MyChildComponent", - "onMounted MyChildComponent", - "onMounted MyComponent" - ]); - - widget.unmount(); - assert.strictEqual(target.innerHTML, ""); - assert.verifySteps([ - "onWillUnmount MyComponent", - "onWillUnmount MyChildComponent", - ]); - - await widget.update({ - text: "leveeBreaks" - }); - - assert.verifySteps([ - "onWillUpdateProps MyComponent", - "onWillRender MyComponent", - "onWillUpdateProps MyChildComponent", - "onRendered MyComponent", - "onWillRender MyChildComponent", - "onRendered MyChildComponent", - "onMounted MyChildComponent", - "onMounted MyComponent" - ]); - assert.strictEqual(target.innerHTML, "
child leveeBreaks
"); - }); - - QUnit.test("lifecycle mount/unmount/update/render", async function (assert) { - assert.expect(36); - - class MyChildComponent extends LegacyComponent { - setup() { - useLogLifeCycle(assert.step.bind(assert)); - } - } - MyChildComponent.template = xml`
child
`; - class MyComponent extends LegacyComponent { - setup() { - useLogLifeCycle(assert.step.bind(assert)); - } - } - MyComponent.template = xml`
`; - MyComponent.components = { MyChildComponent }; - - const target = getFixture(); - const widget = new ComponentWrapper(null, MyComponent, { - text: "takeNoMess" - }); - await widget.mount(target); - - assert.strictEqual(target.innerHTML, "
child takeNoMess
"); - assert.verifySteps([ - "onWillStart MyComponent", - "onWillRender MyComponent", - "onWillStart MyChildComponent", - "onRendered MyComponent", - "onWillRender MyChildComponent", - "onRendered MyChildComponent", - "onMounted MyChildComponent", - "onMounted MyComponent" - ]); - - widget.unmount(); - assert.strictEqual(target.innerHTML, ""); - assert.verifySteps([ - "onWillUnmount MyComponent", - "onWillUnmount MyChildComponent", - ]); - - await widget.update({ - text: "leveeBreaks" - }); - - assert.verifySteps([ - "onWillUpdateProps MyComponent", - "onWillRender MyComponent", - "onWillUpdateProps MyChildComponent", - "onRendered MyComponent", - "onWillRender MyChildComponent", - "onRendered MyChildComponent", - "onMounted MyChildComponent", - "onMounted MyComponent" - ]); - assert.strictEqual(target.innerHTML, "
child leveeBreaks
"); - - await widget.render(); - assert.verifySteps([ - "onWillUpdateProps MyComponent", - "onWillRender MyComponent", - "onWillUpdateProps MyChildComponent", - "onRendered MyComponent", - "onWillRender MyChildComponent", - "onRendered MyChildComponent", - "onWillPatch MyComponent", - "onWillPatch MyChildComponent", - "onPatched MyChildComponent", - "onPatched MyComponent" - ]); - assert.strictEqual(target.innerHTML, "
child leveeBreaks
"); - }); - - QUnit.test("sub component can be updated (in DOM)", async function (assert) { - assert.expect(2); - - class MyComponent extends LegacyComponent {} - MyComponent.template = xml`
Component
`; - const MyWidget = WidgetAdapter.extend({ - start() { - this.component = new ComponentWrapper(this, MyComponent, {val: 1}); - return this.component.mount(this.el); - }, - update() { - return this.component.update({val: 2}); - }, - }); - - const target = getFixture(); - const widget = new MyWidget(); - await widget.appendTo(target); - - assert.strictEqual(widget.el.innerHTML, '
Component 1
'); - - await widget.update(); - - assert.strictEqual(widget.el.innerHTML, '
Component 2
'); - - widget.destroy(); - }); - - QUnit.test("sub component can be updated (not in DOM)", async function (assert) { - assert.expect(18); - - class MyComponent extends LegacyComponent { - setup() { - useLogLifeCycle((log) => assert.step(log)); - } - } - MyComponent.template = xml`
Component
`; - const MyWidget = WidgetAdapter.extend({ - start() { - this.component = new ComponentWrapper(this, MyComponent, {val: 1}); - return this.component.mount(this.el); - }, - update() { - return this.component.update({val: 2}); - }, - }); - - const target = getFixture(); - const widget = new MyWidget(); - await widget.appendTo(target); - assert.verifySteps([ - "onWillStart MyComponent", - "onWillRender MyComponent", - "onRendered MyComponent", - "onMounted MyComponent" - ]); - - assert.strictEqual(target.innerHTML, '
Component 1
'); - - widget.$el.detach(); - widget.on_detach_callback(); - - assert.verifySteps([ - "onWillUnmount MyComponent" - ]); - - await widget.update(); - assert.verifySteps([ - "onWillUpdateProps MyComponent", - "onWillRender MyComponent", - "onRendered MyComponent" - ]); - - widget.$el.appendTo(target); - widget.on_attach_callback(); - assert.verifySteps([ - "onMounted MyComponent" - ]); - - assert.strictEqual(target.innerHTML, '
Component 2
'); - - widget.destroy(); - - assert.verifySteps([ - "onWillUnmount MyComponent", - "onWillDestroy MyComponent" - ]); - }); - - QUnit.test("update a destroyed sub component", async function (assert) { - assert.expect(1); - - class MyComponent extends LegacyComponent {} - MyComponent.template = xml`
Component
`; - const MyWidget = WidgetAdapter.extend({ - start() { - this.component = new ComponentWrapper(this, MyComponent, {val: 1}); - return this.component.mount(this.el); - }, - update() { - this.component.update({val: 2}); - }, - }); - - const target = getFixture(); - const widget = new MyWidget(); - await widget.appendTo(target); - - assert.strictEqual(widget.el.innerHTML, '
Component 1
'); - - widget.destroy(); - - widget.update(); // should not crash - }); - - QUnit.test("sub component that triggers events", async function (assert) { - assert.expect(3); - - class WidgetComponent extends LegacyComponent {} - WidgetComponent.template = xml`
Component
`; - - const MyWidget = WidgetAdapter.extend({ - custom_events: Object.assign({}, Widget.custom_events, { - some_event: function (ev) { - assert.step(ev.data.value); - } - }), - start() { - this.component = new ComponentWrapper(this, WidgetComponent, {}); - return this.component.mount(this.el); - }, - }); - - const target = getFixture(); - const widget = new MyWidget(); - await widget.appendTo(target); - - widget.component.trigger('some_event', { value: 'a' }); - widget.component.trigger('some-event', { value: 'b' }); // - are converted to _ - - assert.verifySteps(['a', 'b']); - - widget.destroy(); - }); - - QUnit.test("change parent of ComponentWrapper", async function (assert) { - assert.expect(7); - - let myComponent; - let widget1; - let widget2; - class WidgetComponent extends LegacyComponent {} - WidgetComponent.template = xml`
Component
`; - const MyWidget = WidgetAdapter.extend({ - custom_events: Object.assign({}, Widget.custom_events, { - some_event: function (ev) { - assert.strictEqual(this, ev.data.widget); - assert.step(ev.data.value); - } - }), - }); - const Parent = Widget.extend({ - start() { - const proms = []; - myComponent = new ComponentWrapper(null, WidgetComponent, {}); - widget1 = new MyWidget(); - widget2 = new MyWidget(); - proms.push(myComponent.mount(this.el)); - proms.push(widget1.appendTo(this.$el)); - proms.push(widget2.appendTo(this.$el)); - return Promise.all(proms); - } - }); - - const target = getFixture(); - const parent = new Parent(); - await parent.appendTo(target); - - // 1. No parent - myComponent.trigger('some-event', { value: 'a', widget: null }); - - assert.verifySteps([]); - - // 2. No parent --> parent (widget1) - myComponent.unmount(); - await myComponent.mount(widget1.el); - myComponent.setParent(widget1); - - myComponent.trigger('some-event', { value: 'b', widget: widget1 }); - - assert.verifySteps(['b']); - - // 3. Parent (widget1) --> new parent (widget2) - myComponent.unmount(); - await myComponent.mount(widget2.el); - myComponent.setParent(widget2); - - myComponent.trigger('some-event', { value: 'c', widget: widget2 }); - - assert.verifySteps(['c']); - - parent.destroy(); - }); - - QUnit.module('Several layers of legacy widgets and Owl components'); - - QUnit.test("Owl over legacy over Owl", async function (assert) { - assert.expect(7); - - let leafComponent; - class MyComponent extends LegacyComponent {} - MyComponent.template = xml`Component`; - const MyWidget = WidgetAdapter.extend({ - custom_events: { - widget_event: function (ev) { - assert.step(`[widget] widget-event ${ev.data.value}`); - }, - both_event: function (ev) { - assert.step(`[widget] both-event ${ev.data.value}`); - if (ev.data.value === 4) { - ev.stopPropagation(); - } - } - }, - start() { - leafComponent = new ComponentWrapper(this, MyComponent, {}); - return leafComponent.mount(this.el); - }, - }); - class Parent extends LegacyComponent { - constructor() { - super(...arguments); - this.MyWidget = MyWidget; - } - onRootEvent(ev) { - assert.step(`[root] root-event ${ev.detail.value}`); - } - onBothEvent(ev) { - assert.step(`[root] both-event ${ev.detail.value}`); - } - } - Parent.template = xml` -
- -
`; - Parent.components = { ComponentAdapter }; - - - const target = getFixture(); - const parent = await mount(Parent, target); - - assert.strictEqual(parent.el.innerHTML, '
Component
'); - - leafComponent.trigger('root-event', { value: 1 }); - leafComponent.trigger('widget-event', { value: 2 }); - leafComponent.trigger('both-event', { value: 3 }); - leafComponent.trigger('both-event', { value: 4 }); // will be stopped by widget - - assert.verifySteps([ - '[root] root-event 1', - '[widget] widget-event 2', - '[widget] both-event 3', - '[root] both-event 3', - '[widget] both-event 4', - ]); - }); - - QUnit.test("Legacy over Owl over legacy", async function (assert) { - assert.expect(7); - - let leafWidget; - const MyWidget = Widget.extend({ - start: function () { - leafWidget = this; - this.$el.text('Widget'); - } - }); - class MyComponent extends LegacyComponent { - constructor() { - super(...arguments); - this.MyWidget = MyWidget; - } - onComponentEvent(ev) { - assert.step(`[component] component-event ${ev.detail.value}`); - } - onBothEvent(ev) { - assert.step(`[component] both-event ${ev.detail.value}`); - if (ev.detail.value === 4) { - ev.stopPropagation(); - } - } - } - MyComponent.template = xml` - - - `; - MyComponent.components = { ComponentAdapter }; - const Parent = WidgetAdapter.extend({ - custom_events: { - root_event: function (ev) { - assert.step(`[root] root-event ${ev.data.value}`); - }, - both_event: function (ev) { - assert.step(`[root] both-event ${ev.data.value}`); - }, - }, - start() { - const component = new ComponentWrapper(this, MyComponent, {}); - return component.mount(this.el); - } - }); - - const target = getFixture(); - const parent = new Parent(); - await parent.appendTo(target); - - assert.strictEqual(parent.el.innerHTML, '
Widget
'); - - leafWidget.trigger_up('root-event', { value: 1 }); - leafWidget.trigger_up('component-event', { value: 2 }); - leafWidget.trigger_up('both-event', { value: 3 }); - leafWidget.trigger_up('both-event', { value: 4 }); // will be stopped by component - - assert.verifySteps([ - '[root] root-event 1', - '[component] component-event 2', - '[component] both-event 3', - '[root] both-event 3', - '[component] both-event 4', - ]); - - parent.destroy(); }); QUnit.module("useWowlService"); @@ -1579,32 +148,4 @@ await mount(MyComponent, getFixture(), { env: legacyEnv }); }); - - QUnit.module("EventBus"); - - QUnit.test("unregister multiple listener from the same target", async function (assert) { - const bus = new EventBus(); - let i = 0; - - const a = () => assert.step(`a:${i++}`); - const b = () => assert.step(`b:${i++}`); - - bus.addEventListener("a", a); - bus.addEventListener("b", b); - - bus.trigger("a"); - bus.trigger("b"); - - bus.removeEventListener("a", a); - bus.removeEventListener("b", b); - - bus.trigger("a"); - bus.trigger("b"); - - assert.verifySteps([ - "a:0", - "b:1", - ], "callback should not be called after unregistering them"); - }); - }); diff --git a/addons/web_editor/__manifest__.py b/addons/web_editor/__manifest__.py index e08bd4e055c..16f141bada4 100644 --- a/addons/web_editor/__manifest__.py +++ b/addons/web_editor/__manifest__.py @@ -58,7 +58,6 @@ Odoo Web Editor widget. 'web/static/lib/owl/owl.js', 'web/static/lib/owl/odoo_module.js', 'web/static/src/libs/owl.js', - 'web/static/src/legacy/legacy_component.js', 'web/static/lib/jquery/jquery.js', 'web/static/lib/jquery.ui/jquery-ui.js', 'web/static/lib/jquery.mjs.nestedSortable/jquery.mjs.nestedSortable.js', diff --git a/addons/web_editor/static/src/js/editor/snippets.editor.js b/addons/web_editor/static/src/js/editor/snippets.editor.js index 952a179006a..457f54bbde2 100644 --- a/addons/web_editor/static/src/js/editor/snippets.editor.js +++ b/addons/web_editor/static/src/js/editor/snippets.editor.js @@ -17,7 +17,7 @@ import { debounce, throttleForAnimation } from "@web/core/utils/timing"; import { uniqueId } from "@web/core/utils/functions"; import { sortBy, unique } from "@web/core/utils/arrays"; import { browser } from "@web/core/browser/browser"; -import { ComponentWrapper } from "@web/legacy/js/owl_compatibility"; +import { attachComponent } from "@web/legacy/utils"; import { Toolbar } from "@web_editor/js/editor/toolbar"; import { Component, @@ -4297,14 +4297,13 @@ var SnippetsMenu = Widget.extend({ wysiwygState: Object, }; } - const toolbarWrapper = new ComponentWrapper(this, WebsiteToolbar, { - wysiwygState: this.options.wysiwyg.state, - }); // Add the toolbarWrapperEl to the dom for owl to properly mount the // Toolbar. document.body.append(this._toolbarWrapperEl); this._toolbarWrapperEl.style.display = 'none'; - await toolbarWrapper.mount(this._toolbarWrapperEl); + await attachComponent(this, this._toolbarWrapperEl, WebsiteToolbar, { + wysiwygState: this.options.wysiwyg.state, + }); this._toolbarWrapperEl.style.display = 'contents'; const toolbarEl = this._toolbarWrapperEl.firstChild; diff --git a/addons/web_editor/static/src/js/editor/snippets.options.js b/addons/web_editor/static/src/js/editor/snippets.options.js index f1548fa13da..fae479e053e 100644 --- a/addons/web_editor/static/src/js/editor/snippets.options.js +++ b/addons/web_editor/static/src/js/editor/snippets.options.js @@ -1,6 +1,6 @@ /** @odoo-module **/ -import { ComponentWrapper } from "@web/legacy/js/owl_compatibility"; +import { attachComponent } from "@web/legacy/utils"; import { MediaDialog } from "@web_editor/components/media_dialog/media_dialog"; import Dialog from "@web/legacy/js/core/dialog"; import dom from "@web/legacy/js/core/dom"; @@ -1413,6 +1413,8 @@ const ColorpickerUserValueWidget = SelectUserValueWidget.extend({ const _super = this._super.bind(this); const args = arguments; + this.resetTabCount = 0; + if (!this.options.dataAttributes.lazyPalette === 'true') { await this._renderColorPalette(); } @@ -1444,7 +1446,7 @@ const ColorpickerUserValueWidget = SelectUserValueWidget.extend({ this.colorPaletteWrapper?.update({ selectedCC: this._ccValue, selectedColor: this._value, - resetTabCount: this.colorPaletteWrapper.node.component.props.props.resetTabCount + 1, + resetTabCount: ++this.resetTabCount, }); } else { // TODO review in master, this does async stuff. Maybe the open @@ -1591,9 +1593,10 @@ const ColorpickerUserValueWidget = SelectUserValueWidget.extend({ * @private * @returns {Promise} */ - _renderColorPalette: function () { + _renderColorPalette: async function () { + this.resetTabCount = 0; const options = { - resetTabCount: 0, + resetTabCount: this.resetTabCount, selectedCC: this._ccValue, selectedColor: this._value, onSetColorNames: (colorNames) => { @@ -1636,12 +1639,8 @@ const ColorpickerUserValueWidget = SelectUserValueWidget.extend({ options.document = this.$target[0].ownerDocument; options.getTemplate = wysiwyg.getColorpickerTemplate.bind(wysiwyg); } - if (this.colorPaletteWrapper) { - this.colorPaletteWrapper.destroy(); - } - this.colorPaletteWrapper = new ComponentWrapper(this, ColorPalette, options); - - return this.colorPaletteWrapper.mount(this.colorPaletteEl); + this.colorPaletteWrapper?.destroy(); + this.colorPaletteWrapper = await attachComponent(this, this.colorPaletteEl, ColorPalette, options); }, /** * @override @@ -6020,15 +6019,14 @@ registry.ImageTools = ImageHandlerOption.extend({ this.trigger_up('disable_loading_effect'); const img = this._getImg(); const document = this.$el[0].ownerDocument; - const imageCropWrapper = new ComponentWrapper(this, ImageCrop, { + const imageCropWrapperElement = document.createElement('div'); + document.body.append(imageCropWrapperElement); + const imageCropWrapper = await attachComponent(this, imageCropWrapperElement, ImageCrop, { rpc: this._rpc.bind(this), activeOnStart: true, media: img, mimetype: this._getImageMimetype(img), }); - const imageCropWrapperElement = document.createElement('div'); - document.body.append(imageCropWrapperElement); - await imageCropWrapper.mount(imageCropWrapperElement); await new Promise(resolve => { this.$target.one('image_cropper_destroyed', async () => { @@ -6037,9 +6035,10 @@ registry.ImageTools = ImageHandlerOption.extend({ } await this._reapplyCurrentShape(); resolve(); - imageCropWrapperElement.remove(); }); }); + imageCropWrapperElement.remove(); + imageCropWrapper.destroy(); this.trigger_up('enable_loading_effect'); }, /** @@ -6083,18 +6082,17 @@ registry.ImageTools = ImageHandlerOption.extend({ // Mount the ImageCrop to call the reset method. As we need the state of // the component to be mounted before calling reset, mount it // temporarily into the body. - const imageCropWrapper = new ComponentWrapper(this, ImageCrop, { + const imageCropWrapperElement = document.createElement('div'); + document.body.append(imageCropWrapperElement); + const imageCropWrapper = await attachComponent(this, imageCropWrapperElement, ImageCrop, { rpc: this._rpc.bind(this), activeOnStart: true, media: img, mimetype: this._getImageMimetype(img), }); - const imageCropWrapperElement = document.createElement('div'); - document.body.append(imageCropWrapperElement); - await imageCropWrapper.mount(imageCropWrapperElement); - await imageCropWrapper.componentRef.comp.mountedPromise; - await imageCropWrapper.componentRef.comp.reset(); - imageCropWrapper.unmount(); + await imageCropWrapper.component.mountedPromise; + await imageCropWrapper.component.reset(); + imageCropWrapper.destroy(); imageCropWrapperElement.remove(); await this._reapplyCurrentShape(); diff --git a/addons/web_editor/static/src/js/frontend/loadWysiwygFromTextarea.js b/addons/web_editor/static/src/js/frontend/loadWysiwygFromTextarea.js index 5d2137172a7..275ef43b2de 100644 --- a/addons/web_editor/static/src/js/frontend/loadWysiwygFromTextarea.js +++ b/addons/web_editor/static/src/js/frontend/loadWysiwygFromTextarea.js @@ -1,7 +1,7 @@ /** @odoo-module **/ import { loadBundle } from "@web/core/assets"; -import { useWowlService } from '@web/legacy/utils'; +import { useWowlService, attachComponent } from '@web/legacy/utils'; import { requireLegacyModule } from '@web_editor/js/frontend/loader'; export async function loadWysiwygFromTextarea(parent, textarea, options) { @@ -16,7 +16,6 @@ export async function loadWysiwygFromTextarea(parent, textarea, options) { currentOptions.value = '


'; } - const { ComponentWrapper } = await requireLegacyModule('@web/legacy/js/owl_compatibility') const { Wysiwyg } = await requireLegacyModule('@web_editor/js/wysiwyg/wysiwyg', async () => { await loadBundle("web_editor.assets_wysiwyg"); }); @@ -40,11 +39,10 @@ export async function loadWysiwygFromTextarea(parent, textarea, options) { $form.append($textarea); $wysiwygWrapper.html(''); const wysiwygWrapper = $wysiwygWrapper[0]; - const componentWrapper = new ComponentWrapper(parent, LegacyWysiwyg, { + await attachComponent(parent, wysiwygWrapper, LegacyWysiwyg, { options: currentOptions, editingValue: currentOptions.value, }); - componentWrapper.mount(wysiwygWrapper); $form.find('.note-editable').data('wysiwyg', wysiwyg); diff --git a/addons/website/static/src/components/ace_editor/ace_editor.js b/addons/website/static/src/components/ace_editor/ace_editor.js index fa6fb5d0013..3e13b257c59 100644 --- a/addons/website/static/src/components/ace_editor/ace_editor.js +++ b/addons/website/static/src/components/ace_editor/ace_editor.js @@ -1,10 +1,10 @@ /** @odoo-module */ -import {ComponentAdapter} from '@web/legacy/js/owl_compatibility'; +import { useWidget } from "@web/legacy/utils"; import {useService} from '@web/core/utils/hooks'; import AceEditor from '@web_editor/js/common/ace'; -const {Component} = owl; +import { Component, xml } from "@odoo/owl"; export const WebsiteAceEditor = AceEditor.extend({ @@ -37,12 +37,7 @@ export const WebsiteAceEditor = AceEditor.extend({ // specific or because its parent was edited too and the view // got copy/unlink). const selectedView = Object.values(this.views).find(view => view.id === this._getSelectedResource()); - let context; - this.trigger_up('context_get', { - callback: (ctx) => { - context = ctx; - }, - }); + const context = this.options.getContext(); defs.push(this._rpc({ model: 'ir.ui.view', method: 'search_read', @@ -97,37 +92,19 @@ export const WebsiteAceEditor = AceEditor.extend({ * @override */ _rpc(options) { - let context; - this.trigger_up('context_get', { - callback: (ctx) => { - context = ctx; - }, - }); - return this._super({...options, context: context}); + return this._super({ ...options, context: this.options.getContext() }); }, }); -export class AceEditorAdapterComponent extends ComponentAdapter { - setup() { - super.setup(); +export class AceEditorAdapterComponent extends Component { + static template = xml`
`; + setup() { this.website = useService("website"); this.user = useService("user"); this.env = Component.env; - } - _trigger_up(event) { - if (event.name === 'context_get') { - return event.data.callback({...this.user.context, website_id: this.website.currentWebsite.id}); - } - super._trigger_up(event); - } - - /** - * @override - */ - get widgetArgs() { - return [ + useWidget("wrapper", WebsiteAceEditor, [ this.website.pageDocument && this.website.pageDocument.documentElement.dataset.viewXmlid, { toggleAceEditor: () => this.website.context.showAceEditor = false, @@ -139,10 +116,11 @@ export class AceEditorAdapterComponent extends ComponentAdapter { reload: () => { this.website.contentWindow.location.reload(); }, - } - ]; + getContext: () => ({ + ...this.user.context, + website_id: this.website.currentWebsite.id, + }), + }, + ]); } } -AceEditorAdapterComponent.defaultProps = { - Component: WebsiteAceEditor, -}; diff --git a/addons/website_slides/__manifest__.py b/addons/website_slides/__manifest__.py index df0dbfe2d3c..9290fdec5f9 100644 --- a/addons/website_slides/__manifest__.py +++ b/addons/website_slides/__manifest__.py @@ -157,8 +157,6 @@ Featuring 'web/static/lib/owl/owl.js', 'web/static/lib/owl/odoo_module.js', 'web/static/src/libs/owl.js', - 'web/static/src/legacy/js/component_extension.js', - 'web/static/src/legacy/legacy_component.js', 'web/static/lib/jquery/jquery.js', 'web/static/lib/jquery.ui/jquery-ui.js', 'web/static/lib/jquery.mjs.nestedSortable/jquery.mjs.nestedSortable.js',