Introduction To Owl
Beginner
OWL is a frontend development framework created by Odoo. It was a framework they introduced in the latest version of Odoo (version 14) to make the frontend code a bit better. If you're familiar with other frontend frameworks like React, Vue, Angular, Backbone, etc. then you'll understand OWL quickly. It follows many of the same patterns and ideas.
If you are not familiar with the idea of frontend frameworks, the most recent frameworks all revolve around the idea of removing the nitty gritty transactional work typically done with the javascript of the past.
I'm sure you're familiar with hundreds of lines of javascript that find or manipulate HTML elements. While also doing things like keeping track of event listener binding logic. These frameworks remove pretty much all of that grunt work and tightly couple your HTML and your javascript more closely.
Hold on. Don't run away. I know I said "tightly coupled" and I know that's a red flag.
But at some point we collectively agreed as frontend developers that coupling HTML and javascript actually makes a ton of sense! They inherently depend on each other. There's very little pure, no side-effects javascript code that you could write and re-use in other projects that doesn't still depend on HTML and browsers. So if we're stuck with these two technologies let's embrace it and focus on usability here instead of modularity.
Take a look at one very basic example of how these two ideas compare:
Here we have some simple HTML. It's 2 buttons. One button increments a count and displays it. One button clears the count back to 0.
The way we used to write javascript will look pretty close to something like this:
// ew, gross
const clicks = 0;
const countButton = document.querySelector("#countButton");
myButton.addEventListener("click", function() {
clicks += 1;
const results = document.querySelector("#results");
results.innerHTML = clicks;
});
const clearButton = document.querySelector("#clearButton");
clearButton.addEventListener("click", function() {
clicks = 0;
const results = document.querySelector("#results");
results.innerHTML = click;
});
We were all used to seeing this type of code for a long time, but this is a small example. It quickly get scary as application grow. Here's what something a modern framework like OWL look like:
const { Component, useState } = owl;
class ClickComponent extends Component {
state = useState({ count: 0 });
}
That's it. We track some data and all of the event listener, DOM manipulation messiness is automatically handled for us. Pretty cool.
I don't work for Odoo and didn't have any part in creating OWL, so I'm not going to speak on this too much. In general, I know that they wanted to make their frontend code better and implement a modern framework.
If you are wondering why they created a brand new framework instead of going with an existing one, I'd recommend you read through their write up addressing that exact question.
Personally, I see 3 main benefits to OWL vs the old way of handling frontend development. All of these features have been implemented in a more elegant way and I'm a huge fan of anything that allows me to write simpler, more elegant, more readable code.
A lot of code I've written in the past in javascript has a ton of boilerplate code to essentially manage the state of the DOM. We constantly need to think about all of the things that can happen to the frontend elements to make sure our code won't break.
Having components that have a lifecycle and state baked in is a huge benefit. We know that when a page loads our component boots up and when the page redirects our component is probably going to break down and go away.
And we can have hooks for all of those things. No more $(document).ready
of the past.
If you reference the small example I started with in the introduction, you'll see an example of reactive binding. We just need to think about the data, where the data is going to be displayed, and when the data is going to be manipulated. All of the DOM manipulation code has been abstracted out. Change the data and the frontend automatically updates. That's reactive binding in a nutshell.
Somewhat related, many modern frontend framework provide a virtual DOM. This means that they track the frontend structure within javascript, mostly behind the scenes. This can be hugely helpful to developers for things like debugging. I'll get into that a bit in future articles.
We already have enough to keep track of when writing javascript. Adding the need to manually keep track of things like event listeners and callbacks just to simply update the frontend, can be a mess. But removing that burden allows us to write better code, more readable code, and handle more complex cases.
This can seem trivial to developers, but throughout my career in software, successfully producing easy to understand code gives you a leg up on everything else. Code is easier to test. Code is easier to maintain. Code is less buggy. The list goes on and on just by focusing on easy to understand code.
PS: If you haven't listened to DHH's talk on software writing I highly recommend it.
This guide assumes some basic knowledge of Odoo development. If you have never used Odoo before, you may need to check out some of our other articles, get a local development environment setup, and get a feel for the system.
OWL is packaged into Odoo version 14 and up. In this article I want to focus on using OWL within Odoo specifically, not as a standalone frontend framework.
As always with Odoo development, we gotta have a new module to work with. Let's setup the simplest possible one so we can to try out some OWL features.
# __manifest__.py
{
"name": "Introduction to OWL in Odoo - Part 1",
"summary": "Provides an example module for OWL.",
"description": "Provides an example module for OWL.",
"author": "Oocademy",
"website": "http://www.oocademy.com",
"category": "Tutorials",
"version": "14.0.0.1",
"depends": ["base"],
"demo": [],
"data": [],
}
OWL works by defining Component
classes which can be thought of like web components. Each component starts with a template, data binded to that template, and any subcomponents.
In HTML we have all of these tags like header
, div
, span
, textarea
, etc. When working with OWL you need to think "if I could create a brand new tag, what would be useful for the project?". So maybe you need a project
and task
tag that display information for a project management system. Or a contact tag to show a user/customer in your software system.
In this article, we want to create a component that displays underneath the customer on a sales order which shows a couple of details about their order history.
To add a new component to a module you need 2 steps:
Our new component file is going to be called PartnerOrderSummary.js
and we are going to add it to static/src/js/components/
.
odoo.define("intro_to_owl_part_1.PartnerOrderSummary", function (require) {
const { Component } = owl;
class PartnerOrderSummary extends Component {
//
};
Object.assign(PartnerOrderSummary, {
template: "intro_to_owl_part_1.PartnerOrderSummary"
});
});
All javascript files in Odoo need to be registered from extending the assets template and adding a script
tag. In this case, let's create a file called assets.xml
in the root of the module that looks like:
Then that assets.xml
needs to be added to our module __manifest__.py
.
{
...
"data": [
"assets.xml"
]
}
Now let's create our template XML. I glazed over some of the details of the js class (which I promise I'll loop back around to), but I'm sure you noticed the template
property added to our class.
class PartnerOrderSummary extends Component {
//
};
Object.assign(PartnerOrderSummary, {
template: "intro_to_owl_part_1.PartnerOrderSummary"
});
The template
references an XML template name. I like to organize my components within the same folder, so let's add a file at static/src/js/components/PartnerOrderSummary.xml
right next to our js class with a basic template:
And similarly to our js class we need to register it with the module. For these templates we just add it to our qweb
config in our __manifest__.py
file.
{
...
"qweb": [
"static/src/js/components/PartnerOrderSummary.xml"
]
}
So we've got a little module here with a component that displays some text for now. But we need that to actually show up on a sales order view form, right?
First off we need to update the module dependencies to include the sales modules:
{
..
"depends": ["sale", "sale_management"],
}
After updating your module so that Sales is installed, let's add that component to the sales order form.
There are multiple ways to go about this. I'm going to show you the way that I think is simplest, by mounting the component to the view when the page loads.
We are going to update our js
file to extend the core Odoo form renderer to look for a certain html class and to automatically mount our component to that element.
odoo.define("intro_to_owl_part_1.PartnerOrderSummary", function (require) {
const FormRenderer = require("web.FormRenderer");
const { Component } = owl;
const { ComponentWrapper } = require("web.OwlCompatibility");
/**
* OWL component responsible for displaying a partner order summary widget
* which will show order history details about a specific customer.
*/
class PartnerOrderSummary extends Component {
//
};
/**
* Register properties to our widget.
*/
Object.assign(PartnerOrderSummary, {
template: "intro_to_owl_part_1.PartnerOrderSummary"
});
/**
* Override the form renderer so that we can mount the component on render
* to any div with the class o_partner_order_summary.
*/
FormRenderer.include({
async _render() {
await this._super(...arguments);
for(const element of this.el.querySelectorAll(".o_partner_order_summary")) {
(new ComponentWrapper(this, PartnerOrderSummary))
.mount(element)
}
}
});
});
There's a lot to take in here, especially if you've not done much frontend work in Odoo. I'm not going to dig too deeply into how these things work behind the scenes for this article. The main takeaway here is that there is a way to mount an OWL component to an element. In our case here, we're mounting to an element with the class o_partner_order_summary
.
We have the ability to mount a component to any element object pretty simply via:
(new ComponentWrapper(this, PartnerOrderSummary))
.mount(element)
There's multiple places where we could hook into core Odoo classes, but since we are adding the component to the sales order form view, I found it simplest to override the web.FormRendererclass
.
Let's override the sales order view and add our div now. I'm doing that by creating a views.xml
in the root of our module:
And as usual with XML files, update our __manifest__.py
.
{
...
"data": [
...
"views.xml",
],
}
At this point you should see your cool new module! Even though it's not too cool quite yet, at least the hard part is done.
Let's get our widget looking better. We can mock out the frontend before linking up the data.
We are obviously using dummy data at this point, but this is how our widget should end up looking after a reload with our updated XML. There is nothing fancy going on with these current changes since it's pretty much just HTML and inline styles (I recommend creating a separate stylesheet in Odoo if you know how).
The final step at this point is to actually link up our partner data. OWL tracks all of the data on a component through a state object. So we'll start by adding a partner property and allowing a way to set the data on construction.
const { useState } = owl.hooks;
class PartnerOrderSummary extends Component {
partner = useState({});
constructor(self, partner) {
super();
this.partner = partner;
}
}
The component object actually gets initialized when the form view renders. At that point, we can grab the partner information off of the sales order and pass it into our constructor:
FormRenderer.include({
async _renderView() {
await this._super(...arguments);
for(const element of this.el.querySelectorAll(".o_partner_order_summary")) {
this._rpc({
model: "res.partner",
method: "read",
args: [[this.state.data.partner_id.res_id]]
}).then(data => {
(new ComponentWrapper(
this,
PartnerOrderSummary,
useState(data[0])
)).mount(element);
});
}
}
});
Let's break down what's going on here. First we are looking through the page for elements that have our class o_partner_order_summary. Then we make an rpc call to the backend to grab all of the data from the partner linked to the current order (with this.state.data
which represents the current record). Once that data returns, then we are mounting our component.
At this point, we have all of the data from the partner record available to us via the partner variable in the XML view.
Our new view goes through and replaces all of our dummy data with t-
statements to pull whatever data we need off of the partner. The simplest attribute to use is t-esc
which just prints data to a div.
To modify an attribute, we can add t-attf-
before the attribute name and then access variables within that string.
For example, we can display the image like:
Or we can display the ciy and zip of the partner like this:
When we now update the module and go to a sale order we can see our custom widget with the customer details:
I've tried to walk you through all of the absolute basics of getting an OWL component set up in Odoo, registering all of the files and classes you need, creating a simple component, linking that component into an existing Odoo view, storing component data, and rendering component data.
There's much more to OWL and frontend development in Odoo that we plan on addressing in future articles. Feel free to reach out and let us know what else you'd like to know!
I hope this helps you Odoo developers out there and best of luck coding!