[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.
This commit is contained in:
mcm-odoo
2020-01-21 10:21:08 +00:00
committed by Aaron Bohy
parent 671a5471cd
commit a8e65f22ed
2 changed files with 261 additions and 5 deletions
@@ -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);
};
})();
+236 -1
View File
@@ -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,
};
});