DEV Community

Coding Dodo
Coding Dodo

Posted on • Originally published at codingdodo.com on

OWL in Odoo 14 - How to extend and patch existing OWL Components.

OWL in Odoo 14 - How to extend and patch existing OWL Components.

In this article, we will see how to extend, monkey-patch and, modify existing OWL Components in Odoo 14. There is a lot of confusion about that, and the existing way of overriding Odoo Widget doesn't work in that case.

We will focus on OWL Components inside Odoo 14, the process will probably be different in Odoo 15 since the WebClient has been entirely rewritten in OWL.

Finally, this article assumes you have a good understanding of OWL already, if this is not the case check out this article series where we create the Realworld App with OWL and go other most of the functionalities of the Framework.

All OWL-related content is available here.

Introduction

First and foremost OWL Components are ES6 Classes , if you are not familiar with ES6 classes, you can visit this Google presentation article. I would also refer you to the amazing book You Don't Know JS: ES6 and Beyond.

ES6 Classes are basically syntactical sugar over the existing prototype-based inheritance in JavaScript. At the most basic level, an ES6 Class is a constructor that conforms to prototype-based inheritance. ES6 classes still have Object.prototype!

To go deeper on this subject I would recommend this article about the difference between these ES6 classes and prototypes. This is a very confusing subject, but this quote from the article is very relevant

The most important difference between class- and prototype-based inheritance is that a class defines a type which can be instantiated at runtime, whereas a prototype is itself an object instance.

Anyway, to work with Odoo 14 existing OWL Components, you still have to know some general concepts. We will keep it to a bare minimum, so let's begin with what an ES6 Class look like!

class Component {
  constructor(name) {
    this.name = name;
  }

  render() {
    console.log(`${this.name} renders itself.`);
  }

  // Getter/setter methods are supported in classes,
  // similar to their ES5 equivalents
  get uniqueId() {
    return `${this.name}-test`;
  }
}
Enter fullscreen mode Exit fullscreen mode

You can inherit classes with the keyword extends and super to call parent function.

class MyBetterComponent extends Component {
  constructor(name) {
    super(name); // call the super class constructor and pass in the name parameter
  }

  render() {
    console.log(`${this.name} with id ${this.uniqueId} render itslef better.`);
  }
}

let comp = new MyBetterComponent('MyBetterComponent');
comp.render(); // MyBetterComponent with id MyBetterComponent-test renders itself better.
Enter fullscreen mode Exit fullscreen mode

This is the standard ES6 super keyword, don't confuse it with the Odoo _super function built inside the Framework.

Most of the patching, extending, overriding of OWL Components in Odoo will make use of this basic knowledge, so let's dive in. Everything will become clearer with examples.

Odoo OWL utils patch, and patchMixin functions.

Extending Odoo OWL components is done via a patch function that comes in 2 flavors. Either the Component itself exposes a patch function because it is wrapped around the patchMixin. Or you have to use the patch function directly (in the web.utils package) to apply a patch to an OWL Component.

With the patchMixin, the Component exposes a "patch" function.

Inside odoo/addons/web/static/src/js/core/patch_mixin.js we have this patchMixinfunction:

function patchMixin(OriginalClass) {
    let unpatchList = [];
    class PatchableClass extends OriginalClass {}

    PatchableClass.patch = function (name, patch) {
        if (unpatchList.find(x => x.name === name)) {
            throw new Error(`Class ${OriginalClass.name} already has a patch ${name}`);
        }
        if (!Object.prototype.hasOwnProperty.call(this, 'patch')) {
            throw new Error(`Class ${this.name} is not patchable`);
        }
        const SubClass = patch(Object.getPrototypeOf(this));
        unpatchList.push({
            name: name,
            elem: this,
            prototype: this.prototype,
            origProto: Object.getPrototypeOf(this),
            origPrototype: Object.getPrototypeOf(this.prototype),
            patch: patch,
        });
        Object.setPrototypeOf(this, SubClass);
        Object.setPrototypeOf(this.prototype, SubClass.prototype);
    };

    PatchableClass.unpatch = function (name) {
        if (!unpatchList.find(x => x.name === name)) {
            throw new Error(`Class ${OriginalClass.name} does not have any patch ${name}`);
        }
        const toUnpatch = unpatchList.reverse();
        unpatchList = [];
        for (let unpatch of toUnpatch) {
            Object.setPrototypeOf(unpatch.elem, unpatch.origProto);
            Object.setPrototypeOf(unpatch.prototype, unpatch.origPrototype);
        }
        for (let u of toUnpatch.reverse()) {
            if (u.name !== name) {
                PatchableClass.patch(u.name, u.patch);
            }
        }
    };
    return PatchableClass;
}
Enter fullscreen mode Exit fullscreen mode

A Component using this patchMixin is returned wrapped around the function, for example inside odoo/addons/mail/static/src/components/messaging_menu/messaging_menu.js the MessagingMenu is returned like that:

// ...
const patchMixin = require('web.patchMixin');

const { Component } = owl;

class MessagingMenu extends Component {
// ...
// content of the file
// ...
}
return patchMixin(MessagingMenu);
Enter fullscreen mode Exit fullscreen mode

Be careful, there are actually not that many Components that are returned with the patchMixin , you should always check first if that is the case. We will call these kinds of components "Patchable Components".

Import "web.utils", patch function for a "non-patchable" Component as a last resort.

When the Component doesn't use the patchMixin you will not be able to extend the ES6 class properly but with the patch function you will be able to override the regular functions of the Component.

The web.utils patchfunction is kind of limited, and will work on the " regular functions " of the component. Constructor, getters, setters won't be inherited with technique as we will see in examples later.

This is the patch function content:

/**
 * Patch a class and return a function that remove the patch
 * when called.
 *
 * This function is the last resort solution for monkey-patching an
 * ES6 Class, for people that do not control the code defining the Class
 * to patch (e.g. partners), and when that Class isn't patchable already
 * (i.e. when it doesn't have a 'patch' function, defined by the 'web.patchMixin').
 *
 * @param {Class} C Class to patch
 * @param {string} patchName
 * @param {Object} patch
 * @returns {Function}
 */
patch: function (C, patchName, patch) {
    let metadata = patchMap.get(C.prototype);
    if (!metadata) {
        metadata = {
            origMethods: {},
            patches: {},
            current: []
        };
        patchMap.set(C.prototype, metadata);
    }
    const proto = C.prototype;
    if (metadata.patches[patchName]) {
        throw new Error(`Patch [${patchName}] already exists`);
    }
    metadata.patches[patchName] = patch;
    applyPatch(proto, patch);
    metadata.current.push(patchName);

    function applyPatch(proto, patch) {
        Object.keys(patch).forEach(function (methodName) {
            const method = patch[methodName];
            if (typeof method === "function") {
                const original = proto[methodName];
                if (!(methodName in metadata.origMethods)) {
                    metadata.origMethods[methodName] = original;
                }
                proto[methodName] = function (...args) {
                    const previousSuper = this._super;
                    this._super = original;
                    const res = method.call(this, ...args);
                    this._super = previousSuper;
                    return res;
                };
            }
        });
    }

    return utils.unpatch.bind(null, C, patchName);
},
Enter fullscreen mode Exit fullscreen mode

As you may already see, the content of this function is problematic, it directly touches the prototype of the Object and do some checks on the typeof == "function" that can be misleading...

A Component returned from its module wrapped around the patchMixin has a patch function that you can use, if not your last resort is the web.utils general patch function.

In conclusion, this is what we have to work with. Now we will go through real world examples on how to apply this knowledge and see some specific cases.


Patchable Component (returned with "patchMixin"): Extend, monkey-patch, override.

Basic syntax

The basic syntax of extending a patchable component is:

PatchableComponent.patch("name_of_the_patch", (T) => {
    class NewlyPatchedComponent extends T {
        //... go wild 
    }
    return NewlyPatchedComponent
})
Enter fullscreen mode Exit fullscreen mode

With this patch, you really play with ES6 classes syntax. Your extended Component is also an ES6 class so you can touch the constructor, getters, setters, properties, and other functions.

Example: the ControlPanel Component.

In this example, we will extend the ControlPanel Component. This component is returned with the patchMixin function, original file:

// at the end of file...
ControlPanel.template = 'web.ControlPanel';

return patchMixin(ControlPanel);

Enter fullscreen mode Exit fullscreen mode

Describing the functionality.

The goal of our module is to be very obnoxious, we will be to display a message, under the ControlPanel (everywhere) that will call an API and show a random inspiring quote from some famous people.

Please don't use this code in a real project, everybody will hate you secretly.

To make our fetch request to our quotes API we will use the willUpdateProps hook so every time the user navigates on his WebClient it will fetch a new quote!

If you are not familiar with OWL hooks and particularly willUpdateProps, there is series about OWL from scratch here.

And the part talking about willUpdateProps is available here.

Implementing the code

First, let's extend the OWL XML template to add our div that will contain the quote.

<?xml version="1.0" encoding="UTF-8" ?>
<templates>
    <t t-inherit="web.ControlPanel" t-inherit-mode="extension" owl="1">
        <xpath expr="//div[hasclass('o_control_panel')]" position="inside">
            <div t-esc="state.customText" class="o_control_panel_random_quote"></div>
        </xpath>
    </t>
</templates>
Enter fullscreen mode Exit fullscreen mode
/static/src/xml/control_panel.xml

Inheriting an OWL XML Template is very similar to extending standard QWeb templates except that you should not forget to add owl="1". We will put our div inside the control panel and show the customText inside the state of our Component.

We will make it prettier by adding some custom SCSS for it, inside /src/scss/control_panel.scss.

.o_control_panel {
  .o_control_panel_random_quote {
    color: $text-muted;
    font-style: italic;
    align-items: center;
    justify-content: center;
    font-weight: bolder;
  }
}
Enter fullscreen mode Exit fullscreen mode

Now for the JavaScript module itself /src/js/control_panel.js

odoo.define("owl_tutorial.ControlPanel", function (require) {
  "use strict";
  const ControlPanel = require("web.ControlPanel");
  const { useState } = owl.hooks;

  // ConstrolPanel has a patch function thanks to the patchMixin 
  // This is the usual syntax, first argument is the name of our patch.
  ControlPanel.patch("owl_tutorial.ControlPanelCodingDodo", (T) => {
    class ControlPanelPatched extends T {
      constructor() {
        super(...arguments);
        this.state = useState({
          customText: "",
        });
        console.log(this.state);
      }

      async willUpdateProps(nextProps) {
        // Don't forget to call the super
        await super.willUpdateProps(nextProps);

        let self = this;
        fetch("https://type.fit/api/quotes")
          .then(function (response) {
            return response.json();
          })
          .then(function (data) {
            let quote = data[Math.floor(Math.random() * data.length)];
            // Update the state of the Component
            Object.assign(self.state, {
              customText: `${quote.text} - ${quote.author}`,
            });
          });
      }
    }
    return ControlPanelPatched;
  });
});

Enter fullscreen mode Exit fullscreen mode

As you can see, having the Component returned with patchMixin makes it very easy to extend it directly, patch its function and add features!

OWL in Odoo 14 - How to extend and patch existing OWL Components.

Now let's take a loot at non-patchable Components.


Non-Patchable Component: Override a regular function with "web.utils" patch.

As of Odoo 14, most of the Components aren't returned with patchMixin and if we want to override the content of some Component functions we will use the web.utils patch function.

Example: the FileUpload Component.

Inside the mail addon the component FileUpload is responsible of handling the input files and the function that interests us is this one:

/**
 * @param {FileList|Array} files
 * @returns {Promise}
 */
async uploadFiles(files) {
    await this._unlinkExistingAttachments(files);
    this._createTemporaryAttachments(files);
    await this._performUpload(files);
    this._fileInputRef.el.value = '';
}
Enter fullscreen mode Exit fullscreen mode

This Component isn't return wrapped with the patchMixin so we will have to use the "web.utils" function patch.

Describing the functionality

In this example we will change the behavior of the file upload inside the chatter send message box:

OWL in Odoo 14 - How to extend and patch existing OWL Components.

We will try to extend the behavior of the FileUpload so it doesn't even try to compute any file with a size over 10MB.

Implementing the code.

This is the content of our JavaScript module file.

odoo.define(
  "owl_tutorial/static/src/components/file_uploader/file_uploader.js",
  function (require) {
    "use strict";

    const components = {
      FileUploader: require("mail/static/src/components/file_uploader/file_uploader.js"),
    };

    const { patch } = require("web.utils");

    patch(
      components.FileUploader,
      "owl_tutorial/static/src/components/file_uploader/file_uploader.js",
      {
        // You can add your own functions to the Component.
        getMaxSize() {
          return 10000000;
        },

        /**
         * @override
         */
        async uploadFiles(files) {
          for (const file of files) {
            if (file.size > this.getMaxSize()) {
              // Files over 10MB are now rejected
              this.env.services["notification"].notify({
                type: "danger",
                message: owl.utils.escape(
                  `Max file size allowed is 10 MB, This file ${file.name} is too big!`
                ),
              });
              return false;
            }
          }
          return this._super(files);
        },
      }
    );
    console.log(components.FileUploader.prototype);
  }
);

Enter fullscreen mode Exit fullscreen mode

With that done we now have a limit of 10MB on the size of the file uploaded, and a little notification warning us. We return _super if no file reached the limit.

OWL in Odoo 14 - How to extend and patch existing OWL Components.


Non-patchable Component: Override the "getter" of an OWL Component.

Some time ago I saw a question on the Odoo forums asking to override the get avatar getter of the Message component.

I noticed a lot of confusion around that and unfortunately, as we saw in the introduction, there is also an architectural problem with the way the patch function is coded in Odoo core.

Describing the problem

This is the original get avatar getter function:

/**
 * @returns {string}
 */
get avatar() {
    if (
        this.message.author &&
        this.message.author === this.env.messaging.partnerRoot
    ) {
        return '/mail/static/src/img/odoobot.png';
    } else if (this.message.author) {
        // TODO FIXME for public user this might not be accessible. task-2223236
        // we should probably use the correspondig attachment id + access token
        // or create a dedicated route to get message image, checking the access right of the message
        return this.message.author.avatarUrl;
    } else if (this.message.message_type === 'email') {
        return '/mail/static/src/img/email_icon.png';
    }
    return '/mail/static/src/img/smiley/avatar.jpg';
}
Enter fullscreen mode Exit fullscreen mode

This syntax with a space between get and avatar is what we call a getter function.

To see the problem we have to look inside the content of the web.utils patch function and especially the applyPatch function. We can see this condition

if (typeof method === "function") {
    //...
}
Enter fullscreen mode Exit fullscreen mode

But doing typeof on avatar will give us string in that case and not function type! So the patch will never get applied, we will have to find another way to hard override this getter function.

We could try to patch the components.Message.prototype instead of the Message class itself but that would also throw an error because the patch function stores a WeakMap on top of the file:

  const patchMap = new WeakMap();
Enter fullscreen mode Exit fullscreen mode

To search and add patched prototype, the lookup is done via a WeakMap this way:

patch: function (C, patchName, patch) {
    let metadata = patchMap.get(C.prototype);
    if (!metadata) {
        metadata = {
            origMethods: {},
            patches: {},
            current: [],
        };
        patchMap.set(C.prototype, metadata);
    }
Enter fullscreen mode Exit fullscreen mode

So the C.prototype will throw an error if the C given is already SomeClass.prototype.

Solution 1 - Redefining prototype property.

To quickly solve this problem we will apply standard JavaScript knowledge with Object. defineProperty on the prototype and change the "avatar" property.

odoo.define(
  "owl_tutorial/static/src/components/message/message.js",
  function (require) {
    "use strict";

    const components = {
      Message: require("mail/static/src/components/message/message.js"),
    };

    Object.defineProperty(components.Message.prototype, "avatar", {
      get: function () {
        if (
          this.message.author &&
          this.message.author === this.env.messaging.partnerRoot
        ) {
          // Here we replace the Robot with the better CodingDodo Avatar
          return "https://avatars.githubusercontent.com/u/81346769?s=400&u=614004f5f4dace9b3cf743ee6aa3069bff6659a2&v=4";
        } else if (this.message.author) {
          return this.message.author.avatarUrl;
        } else if (this.message.message_type === "email") {
          return "/mail/static/src/img/email_icon.png";
        }
        return "/mail/static/src/img/smiley/avatar.jpg";
      },
    });
  }
);

Enter fullscreen mode Exit fullscreen mode

OWL in Odoo 14 - How to extend and patch existing OWL Components.

Note that this is pure JavaScript override and no "Odoo magic" will save you here, the super is not called for you and you have to be really careful when doing that. Any other override after yours on the same getter will override yours!

Using directly Object.defineProperty works but you are closing the door to future easy extensions from other modules. It is important to keep that in mind as Odoo is a module-driven Framework.

Solution 2 - Putting the defineProperty inside the Component setup function (overridable).

It would be better if the standard getter would call a regular function called _get_avatar that could be overrideable by other modules.

With the patch we also cannot override the constructor so we will use a function available on each OWL Component called setup.

setup is called at the end of the constructor of an OWL Component and can be overridden, patched, etc

const { patch } = require("web.utils");

patch(
  components.Message,
  "owl_tutorial/static/src/components/message/message.js",
  {
    /**
     * setup is run just after the component is constructed. This is the standard
     * location where the component can setup its hooks.
     */
    setup() {
      Object.defineProperty(this, "avatar", {
        get: function () {
          return this._get_avatar();
        },
      });
    },
    /**
     * Get the avatar of the user. This function can be overriden
     *
     * @returns {string}
     */
    _get_avatar() {
      if (
        this.message.author &&
        this.message.author === this.env.messaging.partnerRoot
      ) {
        // Here we replace the Robot with the better CodingDodo Avatar
        return "https://avatars.githubusercontent.com/u/81346769?s=400&u=614004f5f4dace9b3cf743ee6aa3069bff6659a2&v=4";
      } else if (this.message.author) {
        return this.message.author.avatarUrl;
      } else if (this.message.message_type === "email") {
        return "/mail/static/src/img/email_icon.png";
      }
      return "/mail/static/src/img/smiley/avatar.jpg";
    },
  }
);
Enter fullscreen mode Exit fullscreen mode

In that way, the function can now be overridden again by another patch in the future.

// Can be overriden again now
patch(
  components.Message,
  "another_module/static/src/components/message/message_another_patch.js",
  {
    _get_avatar() {
      let originAvatar = this._super(...arguments);
      console.log("originAvatar", originAvatar);
      if (originAvatar === "/mail/static/src/img/odoobot.png") {
        return "https://avatars.githubusercontent.com/u/81346769?s=400&u=614004f5f4dace9b3cf743ee6aa3069bff6659a2&v=4";
      }
      return originAvatar;
    },
  }
);
Enter fullscreen mode Exit fullscreen mode

Getter and setters are a cool feature of Classes but leave little space for extendability, it is sometimes better to make a getter as a shortcut to an actual classic function that can be extended later.

Solution 3 - Force apply the "patchMixin" on the Component and replace it in the Components tree.

The last solution is to create another Component equal to the old Component returned with patchMixin, then replace them where they are used in parent Components.

const { QWeb } = owl;
const patchMixin = require("web.patchMixin");

// Create patchable component from original Message
const PatchableMessage = patchMixin(components.Message);
// Get parent Component 
const MessageList = require("mail/static/src/components/message_list/message_list.js");

PatchableMessage.patch(
  "owl_tutorial/static/src/components/message/message.js",
  (T) => {
    class MessagePatched extends T {
      /**
       * @override property
       */
      get avatar() {
        if (
          this.message.author &&
          this.message.author === this.env.messaging.partnerRoot
        ) {
          // Here we replace the Robot with the better CodingDodo Avatar
          return "https://avatars.githubusercontent.com/u/81346769?s=400&u=614004f5f4dace9b3cf743ee6aa3069bff6659a2&v=4";
        } else if (this.message.author) {
          return this.message.author.avatarUrl;
        } else if (this.message.message_type === "email") {
          return "/mail/static/src/img/email_icon.png";
        }
        return "/mail/static/src/img/smiley/avatar.jpg";
      }
    }
    return MessagePatched;
  }
);
MessageList.components.Message = PatchableMessage;
Enter fullscreen mode Exit fullscreen mode

We had to import the parent MessageList component to redefine its own components and put our own PatchableMessage.

The good thing is that now, every other module can extend our PatchableMessage and override easily our function! 🥳

Conclusion

In this article, we reviewed the two main available methods of patching, overriding, and extending Odoo 14 OWL Components. The patchfunction available when the Component is returned with the patchMixin and the global patch function from "web.utils" when we want to override basic functions of a Component.

I hope this guide was helpful to you on your journey customizing OWL Components in Odoo 14. In another article, we will so how to create Odoo 14 OWL Components from scratch and take a look at all the adapters available to us to mix OWL Components with good old Odoo Widgets.

The repository for this tutorial is available here:

GitHub logo Coding-Dodo / owl_tutorial_extend_override_components

This is repository with examples on how to extend, override and monkey patch OWL Components in Odoo v14

Please consider subscribing to be alerted when new content is released here on Coding Dodo.

You can also follow me on Twitter and interact with me for requests about the content you would like to see here!

☕️ Buying me a Coffee

Top comments (1)

Collapse
 
guna_hawk profile image
Gunalan D

Thank you man