summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js
diff options
context:
space:
mode:
authormcm-odoo <mcm@odoo.com>2020-01-14 10:13:15 +0000
committerAaron Bohy <aab@odoo.com>2020-01-21 10:21:08 +0000
commita8e65f22ed6224ba8125fb99c407928ff644cac2 (patch)
tree9b819fcc0162058bcc2faa0538c31b8acb617a5b /addons/web/static/src/js
parent671a5471cd891ac87a2cffc8a0e7ebac1b519725 (diff)
[IMP] web: Owl compatibility: add WidgetAdapterMixin
and ComponentWrapper This commit adds the necessary tools to use when an Odoo legacy widget has to instantiate Owl components.
Diffstat (limited to 'addons/web/static/src/js')
-rw-r--r--addons/web/static/src/js/component_extension.js29
-rw-r--r--addons/web/static/src/js/owl_compatibility.js237
2 files changed, 261 insertions, 5 deletions
diff --git a/addons/web/static/src/js/component_extension.js b/addons/web/static/src/js/component_extension.js
index 39568bd0594..ce9a3228569 100644
--- a/addons/web/static/src/js/component_extension.js
+++ b/addons/web/static/src/js/component_extension.js
@@ -1,8 +1,14 @@
-/**
- * Add a new method to owl Components to ensure that no performed RPC is
- * resolved/rejected when the component is destroyed.
- */
(function () {
+ /**
+ * Symbol used in ComponentWrapper to redirect Owl events to Odoo legacy
+ * events.
+ */
+ odoo.widgetSymbol = Symbol('widget');
+
+ /**
+ * Add a new method to owl Components to ensure that no performed RPC is
+ * resolved/rejected when the component is destroyed.
+ */
owl.Component.prototype.rpc = function () {
return new Promise((resolve, reject) => {
return this.env.services.rpc(...arguments)
@@ -18,4 +24,19 @@
});
});
};
+
+ /**
+ * Patch owl.Component.__trigger method to call a hook that adds a listener
+ * for the triggered event just before triggering it. This is useful if
+ * there are legacy widgets in the ancestors. In that case, there would be
+ * a widgetSymbol key in the environment, corresponding to the hook to call
+ * (see ComponentWrapper).
+ */
+ const originalTrigger = owl.Component.prototype.__trigger;
+ owl.Component.prototype.__trigger = function (component, evType, payload) {
+ if (this.env[odoo.widgetSymbol]) {
+ this.env[odoo.widgetSymbol](evType);
+ }
+ originalTrigger.call(this, component, evType, payload);
+ };
})();
diff --git a/addons/web/static/src/js/owl_compatibility.js b/addons/web/static/src/js/owl_compatibility.js
index e04e42c30b5..ff119f93f8d 100644
--- a/addons/web/static/src/js/owl_compatibility.js
+++ b/addons/web/static/src/js/owl_compatibility.js
@@ -9,7 +9,12 @@ odoo.define('web.OwlCompatibility', function () {
* 2) A legacy widget has to instantiate Owl components
*/
- const { Component, tags } = owl;
+ const { Component, hooks, tags } = owl;
+ const { useSubEnv } = hooks;
+ const { xml } = tags;
+
+ const widgetSymbol = odoo.widgetSymbol;
+ const children = new WeakMap(); // associates legacy widgets with their Owl children
/**
* Case 1) An Owl component has to instantiate legacy widgets
@@ -252,7 +257,237 @@ odoo.define('web.OwlCompatibility', function () {
}
}
+
+ /**
+ * 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`<div>Owl component with value <t t-esc="props.value"/></div>`;
+ * 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});
+ * },
+ * });
+ */
+ const WidgetAdapterMixin = {
+ /**
+ * Calls __callMounted on each sub component, and its descendants (this
+ * function isn't recursive) when the widget is appended into the DOM.
+ */
+ on_attach_callback() {
+ function recursiveCallMounted(component) {
+ for (const key in component.__owl__.children) {
+ recursiveCallMounted(component.__owl__.children[key]);
+ }
+ component.__callMounted();
+ }
+ for (const component of children.get(this) || []) {
+ recursiveCallMounted(component);
+ }
+ },
+ /**
+ * Calls __callWillUnmount on each sub component (this function is
+ * recursive and thus will be automatically called on its descendants)
+ * when the widget is removed/detached from the DOM.
+ */
+ on_detach_callback() {
+ for (const component of children.get(this) || []) {
+ component.__callWillUnmount();
+ }
+ },
+ /**
+ * 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 component of children.get(this) || []) {
+ component.__destroy();
+ }
+ children.delete(this);
+ },
+ };
+ class ComponentWrapper extends Component {
+ /**
+ * Stores the reference of the instance in the parent (in __components).
+ * Also creates a sub environment with a function that will be called
+ * just before events are triggered (see component_extension.js). This
+ * allows to add DOM event listeners on-the-fly, to redirect those Owl
+ * custom (yet DOM) events to legacy custom events (trigger_up).
+ *
+ * @override
+ * @param {Widget|null} parent
+ * @param {Component} Component this is a Class, not an instance
+ * @param {Object} props
+ */
+ constructor(parent, Component, props) {
+ if (parent instanceof Component) {
+ throw new Error('ComponentWrapper must be used with a legacy Widget as parent');
+ }
+ super(null, props);
+ if (parent) {
+ this._register(parent);
+ }
+ useSubEnv({
+ [widgetSymbol]: this._addListener.bind(this)
+ });
+
+ this.parentWidget = parent;
+ this.Component = Component;
+ this.props = props || {};
+ this._handledEvents = new Set(); // Owl events we are redirecting
+ }
+
+ /**
+ * Define (empty) on_attach_callback (resp. on_detach_callback) on the
+ * Wrapper in case the parent would call them, for instance in
+ * on_attach_callback (resp. on_detach_callback). We only do that to
+ * prevent a crash, but the logic is already handled by the mixin.
+ */
+ on_attach_callback() {}
+ on_detach_callback() {}
+
+ /**
+ * Overrides to remove the reference to this component in the parent.
+ *
+ * @override
+ */
+ destroy() {
+ if (this.parentWidget) {
+ const index = children.get(this.parentWidget).indexOf(this);
+ children.get(this.parentWidget).splice(index, 1);
+ }
+ super.destroy();
+ }
+
+ /**
+ * Changes the parent of the wrapper component. This is a function of the
+ * legacy widgets (ParentedMixin), so we have to handle it someway.
+ * It simply removes the reference of this component in the current
+ * parent (if there was one), and adds the reference to the new one.
+ *
+ * We have at least one usecase for this: in views, the renderer is
+ * instantiated without parent, then a controller is instantiated with
+ * the renderer as argument, and finally, setParent is called to set the
+ * controller as parent of the renderer. This implies that Owl renderers
+ * can't trigger events in their constructor.
+ *
+ * @param {Widget} parent
+ */
+ setParent(parent) {
+ if (parent instanceof Component) {
+ throw new Error('ComponentWrapper must be used with a legacy Widget as parent');
+ }
+ this._register(parent);
+ if (this.parentWidget) {
+ const parentChildren = children.get(this.parentWidget);
+ parentChildren.splice(parentChildren.indexOf(this), 1);
+ }
+ this.parentWidget = parent;
+ }
+
+ /**
+ * Updates the props and re-render the component.
+ *
+ * @async
+ * @param {Object} props
+ * @return {Promise}
+ */
+ async update(props = {}) {
+ if (this.__owl__.isDestroyed) {
+ return new Promise(() => {});
+ }
+
+ Object.assign(this.props, props);
+
+ let prom;
+ if (this.__owl__.isMounted) {
+ prom = this.render();
+ } else {
+ // we may not be in the DOM, but actually want to be redrawn
+ // (e.g. we were detached from the DOM, and now we're going to
+ // be re-attached, but we need to be reloaded first). In this
+ // case, we have to fool Owl as it would skip the rendering if
+ // we simply call render.
+ const tmpEl = document.createElement('div');
+ this.el.parentElement.replaceChild(tmpEl, this.el);
+ prom = this.mount(tmpEl, { position: 'self' });
+ }
+ return prom;
+ }
+
+ /**
+ * 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)) {
+ 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();
+ this.parentWidget.trigger_up(ev.type.replace(/-/g, '_'), ev.detail);
+ }
+ });
+ }
+ }
+
+ /**
+ * Registers this instance as a child of the given parent in the
+ * 'children' weakMap.
+ *
+ * @private
+ * @param {Widget} parent
+ */
+ _register(parent) {
+ let parentChildren = children.get(parent);
+ if (!parentChildren) {
+ parentChildren = [];
+ children.set(parent, parentChildren);
+ }
+ parentChildren.push(this);
+ }
+ }
+ ComponentWrapper.template = xml`<t t-component="Component" t-props="props"/>`;
+
return {
ComponentAdapter,
+ ComponentWrapper,
+ WidgetAdapterMixin,
};
});