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.
Coding Dodo@codingdodo_#Odoo WebClient rewrite in OWL has been merged for future v15 ! 🤩
I wrote a big tutorial on OWL, creating an app from scratch with it and now is the best time to learn: codingdodo.com/realworld-app-…
github.com/odoo/odoo/pull…07:46 AM - 19 Jun 2021
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`;
}
}
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.
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 patchMixin
function:
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;
}
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);
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
patch
function 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);
},
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 apatch
function that you can use, if not your last resort is theweb.utils
generalpatch
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
})
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);
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.
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;
}
}
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;
});
});
As you can see, having the Component returned with patchMixin
makes it very easy to extend it directly, patch its function and add features!
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 = '';
}
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:
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);
}
);
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.
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';
}
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") {
//...
}
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();
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);
}
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";
},
});
}
);
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";
},
}
);
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;
},
}
);
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;
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 patch
function 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:
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
Coding Dodo - OWL Tutorial Extend/Override Components
This addon is a companion piece to the article about Extending, Overriding, Monkey-Patching Odoo 14 OWL Components.
Author
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!
Top comments (1)
Thank you man