Creating JavaScript classes and widgets in Odoo

< / / / / / >
Framework JavaScript
V 15.0
+/- 31 minutes
Written by Géry Debongnie

Quick scroll

1. Introduction

This tutorial will teach you the fundamentals of the two basic building blocks provided by the Odoo framework: classes and widgets.

This is a large topic, and a tutorial such as this one is not enough to cover it properly. To make the most of these topics we'll do a few tasks that explain the most important parts. We will complete the following task: make a quiz application, in which the user will be able to select a topic, answer a few multiple choices questions and then get a score.

In this tutorial you will learn how to create your own JavaScript classes, your own widgets, how to use them and add the end a detailed explanation about how communication between widgets works.

Tip: this tutorial is rather long and covers multiple subjects. Take your time to finish it or even plan a break in between. If the tutorial gets confusing - as there is a lot of code - you can download the example app for easier understanding of the structure.

2. Classes introduction

Before working on our Quiz, let us talk a little bit about Odoo classes. Odoo was made before the javascript ecosystem introduced the class system (sometimes known as ES6 classes), so Odoo had to make its own custom class system. Odoo started using ES6 classes (starting in version 14), but Odoo classes are still around. The odoo framework provide the Class abstraction under the web.Class javascript module (see previous tutorial for a discussion on odoo modules). Like most JS abstractions, they can be tested easily in the browser: in a Odoo web client tab in your favorite browser, open the developer tools (usually by pressing F12). In the console, you can type the following lines:

                    const Class =["web.Class"]                                    

This line simply gets a reference to the base class object exported by web.Class. Most things in Odoo are just subclasses from this one (and in particular, all widgets are!). We can now create subclasses with the .extend method. Paste the following in the console:

                    const Car = Class.extend({    speed: 0,    accelerate() {        this.speed += 5;        console.log('accelerating... Current speed: ' + this.speed);    },    stop() {        this.speed = 0;        console.log('stopped');    },});const car = new Car();car.accelerate();car.stop();                                    

As you can see our "car" class will print out that it is accelerating and that it stopped: Example result in developer tools console

Note that this is a different syntax from standard javascript classes. In particular, this is a normal object, so each method need a comma at the end! We can now make a fancy car. Paste the following into the browser console:

                    const FancyCar = Car.extend({    honk() {        console.log('honk honk')    },    accelerate() {        console.log('vroom');        this._super();    },});const myCar = new FancyCar();myCar.honk();                                    

Notice that the call to "this._super()" is why "FancyCar" can call the method with the same name in the "Car" class. Finally, it is certainly important to mention that we can also define a constructor with the "init" function:

                    const ColoredCar = FancyCar.extend({    init(color) {        this.color = color;        console.log('creating a ' + color + ' car');    }});const redCar = new ColoredCar('red');                                    

Odoo classes also have a mixins system (see the official documentation if you are interested), but we won't need more than simple inheritance for this tutorial. Odoo classes have a very important functionality though: widgets! One of the classes is 'Widget' and it is often used in Odoo.

3. Widgets

3.1 Introduction to widgets

Widgets are the basic UI building blocks that are used everywhere in the Odoo web client to display something to the screen. They are similar to React or Vue components, but also different. Most importantly, they are a lower level abstraction, which means that they are quite powerful, but requires more work from the developer to properly use and coordinate them.

Let's create a client action in JS - which is a widget - with a simple template key:

                    odoo.define('tutorial.ClientAction', function (require) {  const AbstractAction = require('web.AbstractAction');  const core = require('web.core');  const QuizAction = AbstractAction.extend({    template: "tutorial.ClientAction",  });  // Adds it to the registry so that the action is loaded by Odoo itself  core.action_registry.add('tutorial.quiz', QuizAction);});                                    

Now let's also add an XML template for it:


3.2 Creating a counter widget

A widget with a template is not much more than a simple template. But obviously, we can define additional properties or methods to this template.

As a learning exercise, let us define and use a simple "Counter" widget that displays an initial value, and allows the user to increment it. First, let us add a QWeb template (which should be located in a static xml file, in our case, tutorial_quiz.xml under the 'views' folder):


We can now define a "Counter" widget and add it to our client action:

                    odoo.define('tutorial.ClientAction', function (require) {  // We use the require() to import the widgets we need for our own widget.  const AbstractAction = require('web.AbstractAction');  const Widget = require('web.Widget');  const core = require('web.core');  // Extend the AbstractAction (base class) to define our own template  const QuizAction = AbstractAction.extend({    template: "tutorial.ClientAction",    start() {      const counter = new Counter(this, 47);      counter.appendTo(this.$el);    }  });  // Create the Counter widget by extending the default web widget.  const Counter = Widget.extend({    template: "tutorial.Counter",    init(parent, initialValue) {      this._super(parent);      this.value = initialValue;    },  });  // Adds it to the registry so that the action is loaded by Odoo  core.action_registry.add('tutorial.quiz', QuizAction);});                                    

If you would now update your module and reload the client action you would see an already working action. So, a lot of code was added but let us take a minute to understand it. We've defined two widgets: our "QuizAction" widget, which is a client action (as it inherits from "AbstractAction") and a "Counter" widget. The "Counter" widget defines a init method: this is its constructor. Since it is a widget it ALWAYS needs to have a parent defined in its first argument (with one exception: the root widget itself). This parent argument has to be given to the _super method, to let the framework properly maintain the tree structure defined by widgets. After the first argument a constructor can have any amount of arguments though. We just set an initial value in the value property. Note that this value property is used by the template.

Another very important point is the way we added our "Counter" widget to the client action: this is done in the start method, where we instantiate a new counter widget (lowercase, since it is an instance) and then we add it to the client action DOM with the following line:


Once a widget is started (so, in the "start" method or after), we have a reference to the root element of the widget: "this.$el" (this is a jQuery element!) or this.el (as an HTMLElement). So, the above line tells the counter to append itself inside the $el element of the client action. Notice that there are some widget lifecycle methods but they're out of the scope of this tutorial. If you want to you can read more about them here.

If you would now click on our "Counter" widget, you will probably notice that the button does not do anything. This makes sense, since we did not write the code for that yet! Let us do that right now by adding this logic inside our "Counter" widget:

                    const Counter = Widget.extend({    template: "tutorial.Counter",    events: {      'click .increment': 'increment'    },    init(parent, value) {      this._super(parent);      this.value = value;    },    increment() {      this.value++;      this.$el.find('span.o-value').text(this.value);    },  });                                    

In this code, we now have a new "events" property and an "increment" method. The events property is used to describe event handlers that we want set up by the Odoo framework. It is a simple object that maps an event type and selector to the name of a method that will be called. Here, we just express the fact that we want to listen on all click events on elements with the increment CSS class, and when it happens, the increment method should be called. This simply means that if somebody clicks on our button "Increment" that Odoo will detect the click because of the "events" definition. It will then trigger the function "increment" to execute code.

Because of the way events are handled in Odoo, it is common to add CSS classes in a widget template to make it easy to define an handler on the desired HTML element.

The increment method need to do two things: update the widget it's internal state and then update the DOM (its representation on the screen). This is necessary because our Odoo widgets are not reactive. In our case, updating the DOM is done by first getting a reference to the span element which contains the value (with the jquery find method defined on this.$el), and then, setting its content. That's it! Our widget is now completely interactive. If we now click on the button we can see that the value is being updated!

4. Creating a menu

Now let us first create a menuitem so that we can actually access our content from the main Odoo screen. This can be easily done by creating an action and a menuitem. The menuitem should trigger the action, which is then being handled by the JavaScript code (that we wrote in the Widgets chapter). Create a new XML file under '/views' in your custom app and add the following code to it:


5. Structuring our code

Even before writing the first line of code, it is very useful to take a step back and to think about what we want, and how we will organize the code. This is by the way not only the case for JavaScript code but for any code! Our OdooQuiz application will have the following screens:

  • A welcome screen with a category selection list.
  • A quiz screen which displays a running counter (something like "2/10") and which shows the current question with the possible answers.
  • A summary screen with the final score and the list of each questions with the correct answer.
When this tutorial is done we should have multiple screens, starting with this screen: Example result of quiz screen

There are many ways to organize the code in various widgets, each with some pros and cons. Usually, more widgets means that each widgets is simpler, individually, but then we have more complexity in the way they need to communicate. On the opposite, less widgets means that communication is simpler, but they need to be more complex, since they have to do more work.

For our task, we will choose a structure with more widgets. This is probably a little bit overkill for this tutorial, but this will be a good opportunity to explain how we can handle communication.

The parent widget will be our client action. Inside, we will have the following sub widgets:

  • A welcome/category selection widget, named "QuizSelection".
  • A quiz widget, simply named "Quiz".
  • A summary screen widget named "QuizSummary".

6. Creating demo data

Before starting working on these widgets, let us define some demo data, to make it easier to test our code. This is very useful when working on a project to have some data. It allows us to focus on the code without having to solve the issue of fetching/storing the data. Add the following demo data at the top of your JavaScript file (right after your 'require' imports and within your odoo.define function):

                    const DINOSAUR_QUIZ = [  {    question: "Which is the best dinosaur?",    choices: {      a: "tyrannosaurus",      b: "diplodocus",      c: "ankylosaurus",      d: "iguanodon"    },    answer: "d"  },  {    question: "Which of these animals is not a dinosaur?",    choices: {      a: "a triceratops",      b: "a cow"    },    answer: "b"  },];const COLOR_QUIZ = [  {    question: "What is your favorite color?",    choices: {      a: "red",      b: "blue",      c: "green",    },    answer: "c"  },  {    question: "How many colors are there?",    choices: {      a: "a lot",      b: "100",      c: "over 9000",    },    answer: "c"  }];const QUIZZES = {  dinosaurs: DINOSAUR_QUIZ,  colors: COLOR_QUIZ,};                                    

7. Creating the Quiz selection widget

Now that you know the basics about classes and widgets it is time to create our very first real widget. This widget has one job: presenting the user a list of possible quizzes and sending the choice from the user to the parent it's widget. Let's first change our "Counter" widget into a "QuizSelection" widget:

                    const QuizSelection = Widget.extend({  template: "tutorial.QuizSelection",  events: {    "click button": "selectQuiz",  },  quizzes: Object.keys(QUIZZES),  selectQuiz(ev) {    const quiz =;    console.log(quiz);    // todo: communicate here to our parent the user selection  },});                                    

Now let us also create a new XML template in our XML file:


Let's also create a CSS file to add some basic styling for our quiz. Create a new CSS file in your custom module under static/src/css named "tutorial_quiz.css" and add this CSS:

                    .tutorial-quiz {    margin: auto;    margin-top: 50px;    padding: 30px;    width: 450px;    min-height: 300px;    border: 1px solid gray;    background-color: #ddd;}                                    

We obviously also need to update our ClientAction widget to instantiate our "QuizSelection" widget instead of a "Counter":

                    const QuizAction = AbstractAction.extend({  template: "tutorial.ClientAction",  start() {    const quizSelection = new QuizSelection(this);    quizSelection.appendTo(this.$el);  },});                                    

Notice that we bound an event handler to the click event. In that handler, we read the data attribute on the html button element. This is a common pattern, sometimes named event delegation: there is one handler for a list of elements, and we simply get the required information from the event target. QWeb makes it easy to add a dynamic data-quiz attribute.

8. Communication between widgets

Now, this leads to a big question: how will this sub widget communicate with its parent? There are many possible answers to that question, including:

  • Giving a callback from the parent to the QuizSelection widget.
  • Triggering an event on a bus with the QuizWidget.
  • Trigger an event on itself with the QuizWidget.
  • Using a custom class/model/store implementation to manage the state, which is more advanced.
  • Trigger an OdooEvent on itself, which will then bubble up on the widget tree from Odoo.
Out of these solutions, only number 2 is really bad, because it would lead to strong coupling between the parent and the child widget. All the other possibilities makes sense though. Solution 1 would be what you might expect from a (small) React application. In React, it is common to give callback as props to a sub component. In the Odoo world, it is more common to trigger an event on a widget. There are some subtle differences between these communication patterns, but they all solve the same problem.

For this tutorial, we will trigger a special Odoo event on a widget (solution 6). This is done by using the method trigger_up, then listening to the event with the special key custom_event. More information can be found in the official documentation. Let us use this way. Look at this code first and I'll explain it afterwards:

                    const QuizAction = AbstractAction.extend({  template: "tutorial.ClientAction",  custom_events: {    quiz_selected: "selectQuiz",  },  start() {    const quizSelection = new QuizSelection(this, 47);    quizSelection.appendTo(this.$el);  },  selectQuiz(ev) {    console.log(  }});const QuizSelection = Widget.extend({  template: "tutorial.QuizSelection",  events: {    "click button": "selectQuiz",  },  quizzes: Object.keys(QUIZZES),  selectQuiz(ev) {    const quiz =;    this.trigger_up("quiz_selected", {quiz})  }});                                    

The "selectQuiz" method in the "QuizSelection" widget simply triggers an event with the trigger_up method. It takes an event name and an optional object argument. The event name can be anything, but we usually choose a short description of what happened.

Now, a parent can listen to an event by defining a custom_events object, which maps event names to handler names. The "selectQuiz" method of "QuizAction" then gets the event as his unique argument, which holds the desired information in its data property. Clicking on the quiz button should now log its name in the console.

Events dispatched by the trigger_up method bubble up the widget tree, just like DOM events bubble up the DOM tree. This means that they are first dispatched on the target component, then its parent, then its parent's parent, ..., until they are stopped or they reach the root widget.

9. Displaying the quiz

Now that the parent widget is aware of the desired user action, we need to perform a transition: remove the QuizSelection widget and display the Quiz widget in its place. A good first step is to create our Quiz widget:

                      // In your JavaScript file  const Quiz = Widget.extend({    template: "tutorial.Quiz",    init(parent, quizName) {      this._super(parent);      // here we have the widget "static" state:      this.quizName = quizName;      this.questions = QUIZZES[quizName];      // and here the widget "running" state:      this.current = 0; // index of current question      this.answers = []; // list of user answers    },  });                                    

Currently, this Quiz widget has only a constructor. We will add more methods later on. For now, notice that I separated the initialization in two parts: a static part (the information received by the parent that will not change), and a dynamic part: the actual widget state. If you are familiar with a component system such as React, you will probably recognize the concept of props: state owned by a parent. Now, let's display that widget in the place of QuizSelection:

                      selectQuiz(ev) {    const quizWidget = new Quiz(this,;    // add Quiz widget to DOM    quizWidget.appendTo(this.$el);    // remove previous QuizSelection widget;  },                                    

This is done in three steps:

  1. Create our Quiz subwidget. The widget is given this (the QuizAction instance) as a parent, and then the name of the selected quiz.
  2. Adding the Quiz widget in the DOM (this.$el is the root element for the current widget),
  3. Removing the previous QuizSelection widget. This is done by calling the destroy method. Here, we use a convenient (but not very well known) feature of the Odoo framework: when a OdooEvent is dispatched by the trigger_up method, the target widget is attached to it in the target key. So, we can simply look it up in to destroy it.
After a browser refresh, selecting a quiz will now dynamically update the interface!

10. Making the Quiz widget interactive

The next step - and almost the final one - is to display the current question, give a way to the user to select an answer, and display the next questions. All of this should be done in our "Quiz" widget. Have a look at this code and I'll explain it a bit after:


This code should now be mostly clear. Let us however highlight the way we handle a question transition in the "selectChoice" method: the "this.current" counter is incremented, and then the widget "renderElement" method is called. This is a Widget method that will rerender the template and replace the content of the widget with the result of that rendering.

Replacing the content of the widget is sometimes not a good idea, since it may lose some user state. For example, if a widget has an input, replacing its content with a new content will remove the content of the input. Modern frameworks solve this issue with a virtual dom algorithm. In our case this works perfect though. We just need to replace the content and the renderElement does exactly that.

Another noteworthy thing is that we communicate the result of the quiz by triggering an event. This event "quiz_completed" will be useful for the parent widget.

11. Displaying a summary

Phew, we already did quite a lot now! We're almost done, this is the final step. Let us finally display a summary of the quiz taken by the user. We already covered all the necessary material to perform this task: creating a new widget with a constructor, rendering a template, binding events and triggering custom events. If you feel confident you've got this then give it a try first and otherwise you can look at the code. We all need to learn ;-)
The JavaScript:

                      // add this in your JS file  const QuizSummary = Widget.extend({    template: "tutorial.QuizSummary",    events: {      "click button": "backToSelection",    },    init(parent, result) {      this._super(parent);      this.result = result;      this.questions = QUIZZES[];    },    backToSelection() {      this.trigger_up("back_to_selection");    },  });                                    

The XML template:


Let us also add a tiny bit more CSS for styling purpose:

                    .quiz-correction {    margin: 15px;}.quiz-correction div:first-child {    font-weight: bold;}                                    

And finally, we need to modify "QuizAction" to properly instantiate "QuizSummary" and react to events:

                      const QuizAction = AbstractAction.extend({    template: "tutorial.ClientAction",    custom_events: {      quiz_selected: "selectQuiz",      quiz_completed: "quizCompleted",      back_to_selection: "backToSelection",    },    ...    backToSelection(ev) {      const quizSelection = new QuizSelection(this);      quizSelection.appendTo(this.$el);;    },  });                                    

That's it, we are done! If you now install or update your module you will see a fully working Quiz app: Example result of quiz screen

12. Conclusion

In this tutorial, we covered a LOT of material. How Odoo classes and widgets work, how they interact with QWeb templates, how to bind event handlers, how to manage states, to transition from one widget to another, how to communicate between widgets and more! We built a fully functional Quiz application along the way. It displays a quiz selection interface, then transitions to a quiz, and then to a summary. All of it in about 100 lines of code!

It is not perfect however: to make it simple, we added all the code in a single file. In a real application, we will probably split our code in a few files. There is also a little bit of code duplication in the way we managed the screen transitions. Another shortcoming is that our application only use a static definition for our quiz, and does not interact with the server in any way. As this tutorial was already big and pretty long we decided to keep this part sample. We will explain server interactions, also known as RPC widgets, in another tutorial.

This tutorial should show you a lot about the JavaScript framework in Odoo and it also shows how flexible the framework is. The things you can do are incredible but with great power comes a great responsibility. Take your time to master the JavaScript framework as it can be a very valuable asset.