odoo.define('web.OwlCompatibility', function () {
"use strict";
/**
* This file defines the necessary tools for the transition phase where Odoo
* legacy widgets and Owl components will coexist. There are two possible
* scenarios:
* 1) An Owl component has to instantiate legacy widgets
* 2) A legacy widget has to instantiate Owl components
*/
const { Component, hooks, tags } = owl;
const { useRef, 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
* ----------------------------------------------------------
*
* 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();
* }
* }
*/
class ComponentAdapter extends Component {
/**
* Creates the template on-the-fly, depending on the type of Component
* (legacy widget or Owl component).
*
* @override
*/
constructor(parent, props) {
if (!props.Component) {
throw Error(`ComponentAdapter: 'Component' prop is missing.`);
}
let template;
if (!(props.Component.prototype instanceof Component)) {
template = tags.xml`
`;
} else {
let propsStr = '';
for (let p in props) {
if (p !== 'Component') {
propsStr += ` ${p}="props.${p}"`;
}
}
template = tags.xml``;
}
ComponentAdapter.template = template;
super(...arguments);
this.template = template;
ComponentAdapter.template = null;
this.widget = null; // widget instance, if Component is a legacy widget
}
/**
* Starts the legacy widget (not in the DOM yet)
*
* @override
*/
willStart() {
if (!(this.props.Component.prototype instanceof Component)) {
this.widget = new this.props.Component(this, ...this.widgetArgs);
return this.widget._widgetRenderAndInsert(() => {});
}
}
/**
* Updates the internal state of the legacy widget (but doesn't re-render
* it yet).
*
* @override
*/
willUpdateProps(nextProps) {
if (this.widget) {
return this.updateWidget(nextProps);
}
}
/**
* Hooks just before the actual patch to replace the fake div in the
* vnode by the actual node of the legacy widget. If the widget has to
* be re-render (because it has previously been updated), re-render it.
* This must be synchronous.
*
* @override
*/
__patch(target, vnode) {
if (this.widget) {
if (this.__owl__.vnode) { // not at first rendering
this.renderWidget();
}
vnode.elm = this.widget.el;
}
const result = super.__patch(...arguments);
if (this.widget && this.el !== this.widget.el) {
this.__owl__.vnode.elm = this.widget.el;
}
return result;
}
/**
* @override
*/
mounted() {
if (this.widget && this.widget.on_attach_callback) {
this.widget.on_attach_callback();
}
}
/**
* @override
*/
willUnmount() {
if (this.widget && this.widget.on_detach_callback) {
this.widget.on_detach_callback();
}
}
/**
* @override
*/
__destroy() {
super.__destroy(...arguments);
if (this.widget) {
this.widget.destroy();
}
}
/**
* Getter that translates the props (except 'Component') into the array
* of arguments used to initialize the legacy widget.
*
* Must be overriden if at least two props (other that Component) are
* given.
*
* @returns {Array}
*/
get widgetArgs() {
if (this.props.widgetArgs) {
return this.props.widgetArgs;
}
const args = Object.keys(this.props);
args.splice(args.indexOf('Component'), 1);
if (args.length > 1) {
throw new Error(`ComponentAdapter has more than 1 argument, 'widgetArgs' must be overriden.`);
}
return args.map(a => this.props[a]);
}
/**
* Can be overriden to update the internal state of the widget when props
* change. To ensure that the DOM is updated at once, this function should
* not do a re-rendering (which should be done by 'render' instead).
*
* @param {Object} nextProps
* @returns {Promise}
*/
updateWidget(/*nextProps*/) {
if (this.env.isDebug('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 (this.env.isDebug('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];
const result = service[payload.method].apply(service, args);
payload.callback(result);
} else if (evType === 'get_session') {
if (payload.callback) {
payload.callback(this.env.session);
}
} else if (evType === 'load_views') {
const params = {
model: payload.modelName,
context: payload.context,
views_descr: payload.views,
};
this.env.dataManager
.load_views(params, payload.options || {})
.then(payload.on_success);
} else if (evType === 'load_filters') {
return this.env.dataManager
.load_filters(payload)
.then(payload.on_success);
} else {
payload.__targetWidget = ev.target;
this.trigger(evType.replace(/_/g, '-'), payload);
}
}
}
/**
* 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});
* },
* });
*/
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 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
this.componentRef = useRef("component");
}
/**
* Calls __callMounted on itself and on each sub component (as this
* function isn't recursive) when the component is appended into the DOM.
*/
on_attach_callback() {
function recursiveCallMounted(component) {
if (
component.__owl__.status !== 2 /* RENDERED */ &&
component.__owl__.status !== 3 /* MOUNTED */ &&
component.__owl__.status !== 4 /* UNMOUNTED */
) {
// Avoid calling mounted on a component that is not even
// rendered. Doing otherwise will lead to a crash if a
// specific mounted callback is legitimately relying on the
// component being mounted.
return;
}
for (const key in component.__owl__.children) {
recursiveCallMounted(component.__owl__.children[key]);
}
component.__callMounted();
}
recursiveCallMounted(this);
}
/**
* Calls __callWillUnmount to notify the component it will be unmounted.
*/
on_detach_callback() {
this.__callWillUnmount();
}
/**
* Overrides to remove the reference to this component in the parent.
*
* @override
*/
destroy() {
if (this.parentWidget) {
const parentChildren = children.get(this.parentWidget);
if (parentChildren) {
const index = parentChildren.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__.status === 5 /* destroyed */) {
return new Promise(() => {});
}
Object.assign(this.props, props);
let prom;
if (this.__owl__.status === 3 /* mounted */) {
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 call 'mount' as Owl would skip the rendering
// if we simply call render.
prom = this.mount(...this._mountArgs);
}
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();
const detail = Object.assign({}, ev.detail, {
__originalComponent: ev.originalComponent,
});
this.parentWidget.trigger_up(ev.type.replace(/-/g, '_'), 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);
}
/**
* Stores mount target and position at first mount. That way, when updating
* while out of DOM, we know where and how to remount.
* @see update()
* @override
*/
async mount(target, options) {
if (options && options.position === 'self') {
throw new Error(
'Unsupported position: "self" is not allowed for wrapper components. ' +
'Contact the JS Framework team or open an issue if your use case is relevant.'
);
}
this._mountArgs = arguments;
return super.mount(...arguments);
}
}
ComponentWrapper.template = xml``;
return {
ComponentAdapter,
ComponentWrapper,
WidgetAdapterMixin,
};
});