Odoo 14 JavaScript Tutorial - Part 2_ Create an OWL View
Odoo 14 JavaScript Tutorial - Part 2_ Create an OWL View
Table of Contents
What are we building?
Overview of the Module architecture
Registering a new view type in the ir.ui.view Model.
Adding the assets to the assets_backend view
Creating the View, Model, Controller and, Renderer.
The Controller
The Model
The OWL Renderer
The View
The TreeItem OWL Component
The Template
The TreeItem.js Component
Adding the SCSS Styles
What's Next
Clicking on a TreeItem to fetch and display the Children
Introduction
Updating the TreeItem template
Adding "toggleChildren" function to the TreeItem Component.
Catching the custom event in the Controller
Fetching children from the Model and placing it in the tree.
Making the "count badge pill field" dynamic
Getting XML attrs from templates into the Renderer.
Using the count_field in the TreeItem Components
Conclusion
In this second part about Odoo 14 JavaScript basics, we will now review how to
create a new View from scratch with OWL. It's very important that you understood
clearly the concepts seen in part 1 of this tutorial because we will not explain each
MVC Components that we will create here.
Or, better you can follow along with this tutorial, create and modify files one by one
as we go.
├── LICENSE
├── README.md
├── README.rst
├── __init__.py
├── __manifest__.py
├── models
│ ├── __init__.py
│ └── ir_ui_view.py
├── static
│ ├── description
│ │ └── icon.png
│ └── src
│ ├── components
│ │ └── tree_item
│ │ ├── TreeItem.js
│ │ ├── TreeItem.xml
│ │ └── tree_item.scss
│ ├── owl_tree_view
│ │ ├── owl_tree_controller.js
│ │ ├── owl_tree_model.js
│ │ ├── owl_tree_renderer.js
│ │ ├── owl_tree_view.js
│ │ └── owl_tree_view.scss
│ └── xml
│ └── owl_tree_view.xml
└── views
└── assets.xml
class View(models.Model):
_inherit = "ir.ui.view"
We expand the Selection Field called type with a new tuple of values. Our type of
view will be called "owl_tree", feel free to choose anything you'd like, but try to stay
descriptive and simple. Don't forget to add and update your __init__.py files
inside the models' folder and the root folder.
Create a assets.xml file inside the views folder of the module with that content:
We added the JavaScript file with the XPath expression "." (root) and the SCSS files
are added via the XPath expression link[last()] meaning that we will search for
other <link rel="stylesheet" src="..."/> declaration and place ours after the
last one.
For the XML Qweb templates for the OWL Components and the OWL Renderer, we
need to add them inside the __manifest__.py of our module:
{
"name": "Coding Dodo - OWL Tutorial Views",
"summary": "Tutorial about Creating an OWL View from scratch.",
"author": "Coding Dodo",
"website": "https://codingdodo.com",
"category": "Tools",
"version": "14.0.1",
"depends": ["base", "web", "mail", "product"],
"qweb": [
"static/src/components/tree_item/TreeItem.xml",
"static/src/xml/owl_tree_view.xml",
],
"data": [
"views/assets.xml",
"views/product_views.xml",
],
}
__manifest__.py
You can see that we inherited the product module and also added
product_views.xml . This is not necessary, it will only help us see our module in
action:
</odoo>
├── owl_tree_view
│ ├── owl_tree_controller.js
│ ├── owl_tree_model.js
│ ├── owl_tree_renderer.js
│ ├── owl_tree_view.js
│ └── owl_tree_view.scss
└── xml
└── owl_tree_view.xml
The Controller
Inside the file owl_tree_controller.js we will create our OWLTreeController that
extends AbastractController that we saw in part 1 of the Tutorial:
/**
* @override
* @param parent
* @param model
* @param renderer
* @param {Object} params
*/
init: function (parent, model, renderer, params) {
this._super.apply(this, arguments);
}
});
return OWLTreeController;
});
For now, this Controller does nothing really, init just calls the parent function and no
custom_events are created for now, but we will get to it later.
The Model
Inside the file owl_tree_model.js , we will create our OWLTreeModel, which will
make the call to the server.
The model inherits the AbstractModel Class and will implement the basic essential
functions to make our view works:
The __load function (called the first time) will fetch data from the server.
The __reload function called by the Controller when any change in the state
of the UI occurs. This will also fetch data from the server.
The __get function to give data back to the Controller and be passed to our
OWL Renderer.
__get: function () {
return this.data;
},
_fetchData: function () {
var self = this;
return this._rpc({
model: this.modelName,
method: "search_read",
kwargs: {
domain: this.domain,
},
}).then(function (result) {
self.data.items = result;
});
},
});
return OWLTreeModel;
});
owl_tree_model.js
Since we want to make RPC calls in the load and the reload function we decided to
extract that logic to a fetchData function that will do the actual rpc call.
The params argument of the load and reload functions contains a lot of info,
notably the domain that we could use. But we have to be careful because without
enough logic it could break our code. We will keep it simple right now, the view
needs to show the Root category and then show the child under so the domain is
set explicitly to [["parent_id", "=", false]] for now.
Notice that we store the result of that server request to data.items . This is
important because later you will see that the OWL Renderer gets access to multiple
data via props that get merged into a big JS Object. So it will make our life easier
later to directly store the result of the RPC call into the item key of the data.
willUpdateProps(nextProps) {
Object.assign(this.state, {
localItems: nextProps.items,
});
}
}
const components = {
TreeItem: require("owl_tutorial_views/static/src/components/tree_ite
};
Object.assign(OWLTreeRenderer, {
components,
defaultProps: {
items: [],
},
props: {
arch: {
type: Object,
optional: true,
},
items: {
type: Array,
},
isEmbedded: {
type: Boolean,
optional: true,
},
noContentHelp: {
type: String,
optional: true,
},
},
template: "owl_tutorial_views.OWLTreeRenderer",
});
return patchMixin(OWLTreeRenderer);
});
This Renderer will be instantiated with props that will contain the items fetched
from the server by the Model. The other props passed and present are examples of
what can be passed.
In our Component, we declare a local state via the useState hook that contains a
"local version" of the items. This is not necessary in that situation but this is an
example to show you that your Renderer can have a local state copied from the
props and then independent!
Inside the willUpdateProps function, we update the localItems with the new
value of items fetched from the server. The willUpdateProps function will be called
by the Controller every time the UI change or new data has been loaded from the
server.
✓
This willUpdateProps is the key piece of reactivity that will make our OWL
View react to different elements of the UI like the Control Panel, the reload
button, etc...
</templates>
This is a basic template with a foreach loop on the items, here you can see that we
use a custom TreeItem OWL Component that we will create later.
The View
Finally, we will create the View that extends the AbstractView. It will be responsible
for instantiating and connecting the Model, Renderer, and Controller.
Create the file owl_tree_view.js with that content:
/**
* @override
*/
init: function () {
this._super.apply(this, arguments);
},
getRenderer(parent, state) {
state = Object.assign(state || {}, this.rendererParams);
return new RendererWrapper(parent, this.config.Renderer, state);
},
});
return OWLTreeView;
});
As you can see, this is a very classic definition of a View in JavaScript Odoo. The
special part is inside the getRenderer function where we will return our OWL
Component wrapped with the RendererWrapper
getRenderer(parent, state) {
state = Object.assign(state || {}, this.rendererParams);
// this state will arrive as "props" inside the OWL Component
return new RendererWrapper(parent, this.config.Renderer, state);
},
Notice also that, again we use the same view_type name that is owl_tree that we
choose at the beginning and add it to the view registry.
.owl-tree-root {
width: 1200px;
height: 1200px;
}
.
├── components
│ └── tree_item
│ ├── TreeItem.js
│ ├── TreeItem.xml
│ └── tree_item.scss
The Template
We will begin with the Template, which will indicate how we would like the
information to be displayed before coding the JavaScript component. It is
sometimes beneficial to code the desired end result first so our JavaScript
implementation will then follow that wishful thinking.
As you can see, this is a recursive component. If the TreeItem has child_id array
set and with items, it will loop over the children and call itself:
<t t-foreach="props.item.children" t-as="child_item">
<TreeItem item="child_item"/>
</t>
The rest of the Component template is not that interesting, it is standard bootstrap
4 classes and markup.
Object.assign(TreeItem, {
components: { TreeItem },
props: {
item: {},
},
template: "owl_tutorial_views.TreeItem",
});
return patchMixin(TreeItem);
}
);
Here also you can see that the Component is recursive because its own components
are made of itself.
We declared a state here but it is not used... yet! The rest of the Component is
very classic OWL Component definition in Odoo.
If you are coming from the tutorial about using OWL as a standalone modern
JavaScript library you may notice some differences, like not defining static
properties on the Component. This is due to the fact that from Odoo we don't have
access to Babel that will transpile our ES6/ESNext JavaScript syntax to standard
output. So we use Object.assign to assign the usual static properties of the
component like components , template or props .
Adding the SCSS Styles
We have to create another SCSS file for that component: tree_item.scss that will
contain this class style rules:
.tree-item-wrapper {
min-width: 50em;
}
What's Next
Now we should have a working module already. If you followed this tutorial, you can
go into Purchase / Configuration / Products / Products Categories to see our view in
action:
But the problem is that only the Root category is shown and nothing else. If we take
a look at what is given as a prop to our TreeItem we see that:
{
"id": 1,
"name": "All",
"complete_name": "All",
"parent_id": false,
"parent_path": "1/",
"child_id": [
9,
12,
3,
4,
2
],
"product_count": 69,
"display_name": "All",
// Other odoo fields
}
There is a child_id property containing the IDs of the child items but it is just an
array of integers for now.
What we will do is create another property called children that will be an array of
Objects like the parent that we will fetch from the Server. The goal is to fill this same
global items object that will contain all the nested children.
We also declared the function toggleChildren as a handler for the click action with
t-on-click.stop.prevent="toggleChildren" . The .stop and .prevent are OWL
little useful shortcuts directives so we don't have to write in our function the usual
event.preventDefault or stopPropagation .
We also update the wrapper around the for each loop for the children to only show if
the boolean childrenVisible is at true :
odoo.define(
"owl_tutorial_views/static/src/components/tree_item/TreeItem.js",
function (require) {
"use strict";
const { Component } = owl;
const patchMixin = require("web.patchMixin");
toggleChildren() {
if (
this.props.item.child_id.length > 0 &&
this.props.item.children == undefined
) {
this.trigger("tree_item_clicked", { data: this.props.item });
}
Object.assign(this.state, {
childrenVisible: !this.state.childrenVisible,
});
}
}
Object.assign(TreeItem, {
components: { TreeItem },
props: {
item: {},
},
template: "owl_tutorial_views.TreeItem",
});
return patchMixin(TreeItem);
}
);
The toggleChildren function first checks if the children are already filled. If not, it
will trigger an event named "tree_item_clicked". Then it toggles the state
"childrenVisible".
i
/**
* @override
* @param parent
* @param model
* @param renderer
* @param {Object} params
*/
init: function (parent, model, renderer, params) {
this._super.apply(this, arguments);
},
/**
* When an item is clicked the controller call the Model to fetch
* the item's children and display them in the tree via the call
* to the update() function.
*
* @param {Object} ev
* @param {Object} ev.data contains the payload
*/
_onTreeItemClicked: async function (ev) {
ev.stopPropagation();
await this.model.expandChildrenOf(
ev.data.data.id,
ev.data.data.parent_path
);
this.update({}, { reload: false });
},
});
return OWLTreeController;
});
We registered custom events with exactly the same name as the event we fired from
the OWL TreeItem Component, and bind it to a function.
_onTreeItemClicked is an async function but it will wait for the model RPC call via
the await keyword before it triggers its rendering update with the update call.
This Model function expandChildrenOf doesn't exist yet on our Model so we will
create it.
/**
* Search for the Node corresponding to the given path.
* Paths are present in the property `parent_path` of any nested ite
* in the form "1/3/32/123/" we have to split the string to manipula
* Each item in the Array will correspond to an item ID in the tree,
* level deeper than the last.
*
* @private
* @param {Array} path for example ["1", "3", "32", "123"]
* @param {Array} items the items to search in
* @param {integer} n The current index of deep inside the tree
* @returns {Object|undefined} the tree Node corresponding to the pa
**/
__target_parent_node_with_path: function (path, items, n = 0) {
for (const item of items) {
if (item.id == parseInt(path[n])) {
if (n < path.length - 1) {
return this.__target_parent_node_with_path(
path,
item.children,
n + 1
);
} else {
return item;
}
}
}
return undefined;
},
The function to expand children is very straightforward, it will fetch items from the
model with the domain containing parent_id as the current item. Then the tricky part
is to "open/expand" the node inside self.data.items .
The function __target_parent_node_with_path will actually explore the
self.data.items until it finds the item we are currently opening. When it finds that
node it will return it.
This returned item is a direct reference to the item inside the global data.items of
the Model.
So when we fill the children with target_node.children = children we are
actually updating the global this.data.items of the Model class. Meaning that
when the Controller updates, it will also pass the new opened items to the OWL
Renderer as props.
With that done, we connected every piece necessary to handle the click on the
Item, try your module and check if it is working:
We would like to declare the field use for "counting" like that
You actually have access to the attributes passed inside the XML like that from the
Renderer. To do so we will edit our owl_tree_renderer.js :
The props contain the XML arch data already transformed into a JavaScript object
and filled with the attrs key.
The attrs are the attributes that we would add inside our XML Markup, so here we
check if a count_field is defined, and if so, we assign it to the state.
We also update the owl_tree_view.xml file to pass the newly created countField.
Object.assign(TreeItem, {
components: { TreeItem },
props: {
item: {},
countField: "", // Comming from the view arch
},
template: "owl_tutorial_views.TreeItem",
});
And now we replace the usage inside the QWeb template TreeItem.xml to use the
actual dynamic field from the attrs.
<span
t-if="props.countField !== '' and props.item.hasOwnProperty(props.co
class="badge badge-primary badge-pill"
t-esc="props.item[props.countField]">
</span>
We first check if countField is filled with something else than an empty string.
Then, we check if it also is present as a property on our item Object to avoid errors
with the hasOwnProperty object method.
If everything is okay, we can access the property dynamically via the [] operator
on a JS Object.
!
UPDATE: Some people have encountered errors at this step due to the fact that
the Odoo 14 version they are using is too old. Odoo 14 regularly updates the
underlying OWL Library, and it is moving fast with huge improvements. To check
the owl version open a browser console and type owl.__info__ . For this
tutorial, the code is working on any OWL version >= 1.2.4. 1.2.3 Is known to not
work so please consider updating your Odoo or changing the owl.js file.
Conclusion
We now have a functional OWL View that we created from scratch. The problem is
that this view doesn't really do anything and is purely presentation for now. We
have to add some interactivity to that View!
But that's enough for that part of the tutorial. The source code for this chapter is
available here and you can clone directly that branch to continue exactly where we
stopped here with git:
As I showed you in the introductory screenshot there will be drag and drop to
handle the parent_id change of items in a pleasant manner. You can already try to
do it by yourself, and if you are very curious the code is actually already here on the
main branch.
In the next part we will:
Integrate the drag and drop functionality with the help of the standard HTML
Drag and Drop API. We will see how to listen to these events in OWL
Component and handle data fetching.
You may have noticed some weird behavior when you reload the View, like the
tree node staying opened. We will implement a resetState function to handle
these cases.
Finally, we may expand the functionalities of our View to let the user do more
editing.
The next part will be subscriber-only, so consider joining us!
Featured posts
Odoo 15 JavaScript Tutorial: OWL View migration guide.
26 Oct 2021
Odoo JavaScript Tutorial 101 - Part 3: Adding Drag and Drop to our OWL View
11 Oct 2021
Odoo JavaScript Tutorial 101 - Part 1: Classes and MVC architecture overview
06 Sep 2021
Latest posts
Install and Deploy Odoo 15 from source on DigitalOcean
07 Oct 2021
Set up your dev environment on MacOs and install Odoo 14 from source.
24 May 2021
Navigation
Collections Odoo SaleOrder DB Schema
🔥 OWL Tutorials
Portal
Contact
Subscribe
Your name
Your email address
JOIN NOW
©Coding Dodo - Odoo, Python & JavaScript Tutorials 2022. All rights reserved. Proudly hosted with DigitalOcean