diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/web/static/src/js/owl_compatibility.js | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/web/static/src/js/owl_compatibility.js')
| -rw-r--r-- | addons/web/static/src/js/owl_compatibility.js | 540 |
1 files changed, 540 insertions, 0 deletions
diff --git a/addons/web/static/src/js/owl_compatibility.js b/addons/web/static/src/js/owl_compatibility.js new file mode 100644 index 00000000..aabb0f98 --- /dev/null +++ b/addons/web/static/src/js/owl_compatibility.js @@ -0,0 +1,540 @@ +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: + * <ComponentAdapter Component="LegacyWidget" params="params"/> + * will be translated to: + * const LegacyWidget = this.props.Component; + * const legacyWidget = new LegacyWidget(this, this.props.params); + * + * If more than one argument (in addition to 'parent') is given to initialize + * the legacy widget, the arguments order (to initialize the sub widget) has + * to be somehow specified. There are two alternatives. One can either (1) + * specify the prop 'widgetArgs', corresponding to the array of arguments, + * otherwise (2) a subclass of ComponentAdapter has to be defined. This + * subclass must override the 'widgetArgs' getter to translate arguments + * received as props to an array of arguments for the call to init. + * For instance: + * (1) <ComponentAdapter Component="LegacyWidget" firstArg="a" secondArg="b" widgetsArgs="[a, b]"/> + * (2) class SpecificAdapter extends ComponentAdapter { + * get widgetArgs() { + * return [this.props.firstArg, this.props.secondArg]; + * } + * } + * <SpecificAdapter Component="LegacyWidget" firstArg="a" secondArg="b"/> + * + * If the legacy widget has to be updated when props change, one must define + * a subclass of ComponentAdapter to override '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`<div/>`; + } else { + let propsStr = ''; + for (let p in props) { + if (p !== 'Component') { + propsStr += ` ${p}="props.${p}"`; + } + } + template = tags.xml`<t t-component="props.Component"${propsStr}/>`; + } + ComponentAdapter.template = template; + super(...arguments); + this.template = template; + ComponentAdapter.template = null; + + this.widget = null; // widget instance, if Component is a legacy widget + } + + /** + * Starts the legacy widget (not in the DOM yet) + * + * @override + */ + willStart() { + if (!(this.props.Component.prototype instanceof Component)) { + this.widget = new this.props.Component(this, ...this.widgetArgs); + return this.widget._widgetRenderAndInsert(() => {}); + } + } + + /** + * Updates the internal state of the legacy widget (but doesn't re-render + * it yet). + * + * @override + */ + willUpdateProps(nextProps) { + if (this.widget) { + return this.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`<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 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`<t t-component="Component" t-props="props" t-ref="component"/>`; + + return { + ComponentAdapter, + ComponentWrapper, + WidgetAdapterMixin, + }; +}); |
