========== Web Client ========== .. highlight:: javascript .. default-domain:: js This guide is about creating modules for Odoo's web client. To create websites with Odoo, see :doc:`website`. .. warning:: This guide assumes knowledge of: * Javascript basics and good practices * jQuery_ * `Underscore.js`_ A Simple Module to Test the Web Framework ----------------------------------------- It's not really possible to include the multiple JavaScript files that constitute the Odoo web framework in a simple HTML file like we did in the previous chapter. So we will create a simple module in Odoo that contains some configuration to have a web component that will give us the possibility to test the web framework. To download the example module, use this bazaar command: .. code-block:: sh bzr branch lp:~niv-openerp/+junk/oepetstore -r 1 Now you must add that folder to your the addons path when you launch Odoo (``--addons-path`` parameter when you launch the ``odoo.py`` executable). Then create a new database and install the new module ``oepetstore``. Now let's see what files exist in that module: .. code-block:: text oepetstore |-- __init__.py |-- __openerp__.py |-- petstore_data.xml |-- petstore.py |-- petstore.xml `-- static `-- src |-- css | `-- petstore.css |-- js | `-- petstore.js `-- xml `-- petstore.xml This new module already contains some customization that should be easy to understand if you already coded an Odoo module like a new table, some views, menu items, etc... We'll come back to these elements later because they will be useful to develop some example web module. Right now let's concentrate on the essential: the files dedicated to web development. Please note that all files to be used in the web part of an Odoo module must always be placed in a ``static`` folder inside the module. This is mandatory due to possible security issues. The fact we created the folders ``css``, ``js`` and ``xml`` is just a convention. ``oepetstore/static/css/petstore.css`` is our CSS file. It is empty right now but we will add any CSS we need later. ``oepetstore/static/xml/petstore.xml`` is an XML file that will contain our QWeb templates. Right now it is almost empty too. Those templates will be explained later, in the part dedicated to QWeb templates. ``oepetstore/static/js/petstore.js`` is probably the most interesting part. It contains the JavaScript of our application. Here is what it looks like right now:: openerp.oepetstore = function(instance) { var _t = instance.web._t, _lt = instance.web._lt; var QWeb = instance.web.qweb; instance.oepetstore = {}; instance.oepetstore.HomePage = instance.web.Widget.extend({ start: function() { console.log("pet store home page loaded"); }, }); instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage'); } The multiple components of that file will explained progressively. Just know that it doesn't do much things right now except display a blank page and print a small message in the console. Like Odoo's XML files containing views or data, these files must be indicated in the ``__openerp__.py`` file. Here are the lines we added to explain to the web client it has to load these files: .. code-block:: python 'js': ['static/src/js/*.js'], 'css': ['static/src/css/*.css'], 'qweb': ['static/src/xml/*.xml'], These configuration parameters use wildcards, so we can add new files without altering ``__openerp__.py``: they will be loaded by the web client as long as they have the correct extension and are in the correct folder. .. warning:: In Odoo, all JavaScript files are, by default, concatenated in a single file. Then we apply an operation called the *minification* on that file. The minification will remove all comments, white spaces and line-breaks in the file. Finally, it is sent to the user's browser. That operation may seem complex, but it's a common procedure in big application like Odoo with a lot of JavaScript files. It allows to load the application a lot faster. It has the main drawback to make the application almost impossible to debug, which is very bad to develop. The solution to avoid this side-effect and still be able to debug is to append a small argument to the URL used to load Odoo: ``?debug``. So the URL will look like this: .. code-block:: text http://localhost:8069/?debug When you use that type of URL, the application will not perform all that concatenation-minification process on the JavaScript files. The application will take more time to load but you will be able to develop with decent debugging tools. Odoo JavaScript Module ------------------------- In the previous chapter, we explained that JavaScript do not have a correct mechanism to namespace the variables declared in different JavaScript files and we proposed a simple method called the Module pattern. In Odoo's web framework there is an equivalent of that pattern which is integrated with the rest of the framework. Please note that **an Odoo web module is a separate concept from an Odoo addon**. An addon is a folder with a lot of files, a web module is not much more than a namespace for JavaScript. The ``oepetstore/static/js/petstore.js`` already declare such a module:: openerp.oepetstore = function(instance) { instance.oepetstore = {}; instance.oepetstore.xxx = ...; } In Odoo's web framework, you declare a JavaScript module by declaring a function that you put in the global variable ``openerp``. The attribute you set in that object must have the exact same name than your Odoo addon (this addon is named ``oepetstore``, if I set ``openerp.petstore`` instead of ``openerp.oepetstore`` that will not work). That function will be called when the web client decides to load your addon. It is given a parameter named ``instance``, which represents the current Odoo web client instance and contains all the data related to the current session as well as the variables of all web modules. The convention is to create a new namespace inside the ``instance`` object which has the same name than you addon. That's why we set an empty dictionary in ``instance.oepetstore``. That dictionary is the namespace we will use to declare all classes and variables used inside our module. Classes ------- JavaScript doesn't have a class mechanism like most object-oriented programming languages. To be more exact, it provides language elements to make object-oriented programming but you have to define by yourself how you choose to do it. Odoo's web framework provide tools to simplify this and let programmers code in a similar way they would program in other languages like Java. That class system is heavily inspired by John Resig's `Simple JavaScript Inheritance `_. To define a new class, you need to extend the :class:`openerp.web.Class` class:: instance.oepetstore.MyClass = instance.web.Class.extend({ say_hello: function() { console.log("hello"); }, }); As you can see, you have to call :func:`instance.web.Class.extend` and give it a dictionary. That dictionary will contain the methods and class attributes of our new class. Here we simply put a method named ``say_hello()``. This class can be instantiated and used like this:: var my_object = new instance.oepetstore.MyClass(); my_object.say_hello(); // print "hello" in the console You can access the attributes of a class inside a method using ``this``:: instance.oepetstore.MyClass = instance.web.Class.extend({ say_hello: function() { console.log("hello", this.name); }, }); var my_object = new instance.oepetstore.MyClass(); my_object.name = "Nicolas"; my_object.say_hello(); // print "hello Nicolas" in the console Classes can have a constructor, it is just a method named ``init()``. You can pass parameters to the constructor like in most language:: instance.oepetstore.MyClass = instance.web.Class.extend({ init: function(name) { this.name = name; }, say_hello: function() { console.log("hello", this.name); }, }); var my_object = new instance.oepetstore.MyClass("Nicolas"); my_object.say_hello(); // print "hello Nicolas" in the console Classes can be inherited. To do so, use :func:`~openerp.web.Class.extend` directly on your class just like you extended :class:`~openerp.web.Class`:: instance.oepetstore.MySpanishClass = instance.oepetstore.MyClass.extend({ say_hello: function() { console.log("hola", this.name); }, }); var my_object = new instance.oepetstore.MySpanishClass("Nicolas"); my_object.say_hello(); // print "hola Nicolas" in the console When overriding a method using inheritance, you can use ``this._super()`` to call the original method. ``this._super()`` is not a normal method of your class, you can consider it's magic. Example:: instance.oepetstore.MySpanishClass = instance.oepetstore.MyClass.extend({ say_hello: function() { this._super(); console.log("translation in Spanish: hola", this.name); }, }); var my_object = new instance.oepetstore.MySpanishClass("Nicolas"); my_object.say_hello(); // print "hello Nicolas \n translation in Spanish: hola Nicolas" in the console Widgets Basics -------------- In previous chapter we discovered jQuery and its DOM manipulation tools. It's useful, but it's not sufficient to structure a real application. Graphical user interface libraries like Qt, GTK or Windows Forms have classes to represent visual components. In Odoo, we have the :class:`~openerp.web.Widget` class. A widget is a generic component dedicated to display content to the user. Your First Widget %%%%%%%%%%%%%%%%% The start module you installed already contains a small widget:: instance.oepetstore.HomePage = instance.web.Widget.extend({ start: function() { console.log("pet store home page loaded"); }, }); Here we create a simple widget by extending the :class:`openerp.web.Widget` class. This one defines a method named :func:`~openerp.web.Widget.start` that doesn't do anything really interesting right now. You may also have noticed this line at the end of the file:: instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage'); This last line registers our basic widget as a client action. Client actions will be explained in the next part of this guide. For now, just remember that this is what allows our widget to be displayed when we click on the :menuselection:`Pet Store --> Pet Store --> Home Page` menu element. Display Content %%%%%%%%%%%%%%% Widgets have a lot of methods and features, but let's start with the basics: display some data inside the widget and how to instantiate a widget and display it. The ``HomePage`` widget already has a :func:`~openerp.web.Widget.start` method. That method is automatically called after the widget has been instantiated and it has received the order to display its content. We will use it to display some content to the user. To do so, we will also use the :attr:`~openerp.web.Widget.$el` attribute that all widgets contain. That attribute is a jQuery object with a reference to the HTML element that represents the root of our widget. A widget can contain multiple HTML elements, but they must be contained inside one single element. By default, all widgets have an empty root element which is a ``
`` HTML element. A ``
`` element in HTML is usually invisible for the user if it does not have any content. That explains why when the ``instance.oepetstore.HomePage`` widget is displayed you can't see anything: it simply doesn't have any content. To show something, we will use some simple jQuery methods on that object to add some HTML in our root element:: instance.oepetstore.HomePage = instance.web.Widget.extend({ start: function() { this.$el.append("
Hello dear Odoo user!
"); }, }); That message will now appear when you go to the menu :menuselection:`Pet Store --> Pet Store --> Home Page` (remember you need to refresh your web browser, although there is not need to restart Odoo's server). Now you should learn how to instantiate a widget and display its content. To do so, we will create a new widget:: instance.oepetstore.GreetingsWidget = instance.web.Widget.extend({ start: function() { this.$el.append("
We are so happy to see you again in this menu!
"); }, }); Now we want to display the ``instance.oepetstore.GreetingsWidget`` inside the home page. To do so we can use the :func:`~openerp.web.Widget.append` method of ``Widget``:: instance.oepetstore.HomePage = instance.web.Widget.extend({ start: function() { this.$el.append("
Hello dear Odoo user!
"); var greeting = new instance.oepetstore.GreetingsWidget(this); greeting.appendTo(this.$el); }, }); Here, the ``HomePage`` instantiate a ``GreetingsWidget`` (the first argument of the constructor of ``GreetingsWidget`` will be explained in the next part). Then it asks the ``GreetingsWidget`` to insert itself inside the DOM, more precisely directly under the ``HomePage`` widget. When the :func:`~openerp.web.Widget.appendTo` method is called, it asks the widget to insert itself and to display its content. It's during the call to :func:`~openerp.web.Widget.appentTo` that the :func:`~openerp.web.Widget.start` method will be called. To check the consequences of that code, let's use Chrome's DOM explorer. But before that we will modify a little bit our widgets to have some classes on some of our ``
`` elements so we can clearly see them in the explorer:: instance.oepetstore.HomePage = instance.web.Widget.extend({ start: function() { this.$el.addClass("oe_petstore_homepage"); ... }, }); instance.oepetstore.GreetingsWidget = instance.web.Widget.extend({ start: function() { this.$el.addClass("oe_petstore_greetings"); ... }, }); The result will be this if you can find the correct DOM part in the DOM explorer: .. code-block:: html
Hello dear Odoo user!
We are so happy to see you again in this menu!
Here we can clearly see the two ``
`` created implicitly by :class:`~openerp.web.Widget`, because we added some classes on them. We can also see the two divs containing messages we created using the jQuery methods on ``$el``. Finally, note the ``
`` element which represents the ``GreetingsWidget`` instance is *inside* the ``
`` which represents the ``HomePage`` instance. Widget Parents and Children %%%%%%%%%%%%%%%%%%%%%%%%%%% In the previous part, we instantiated a widget using this syntax:: new instance.oepetstore.GreetingsWidget(this); The first argument is ``this``, which in that case was a ``HomePage`` instance. This serves to indicate the Widget what other widget is his parent. As we've seen, widgets are usually inserted in the DOM by another widget and *inside* that other widget. This means most widgets are always a part of another widget. We call the container the *parent*, and the contained widget the *child*. Due to multiple technical and conceptual reasons, it is necessary for a widget to know who is his parent and who are its children. This is why we have that first parameter in the constructor of all widgets. :func:`~openerp.web.Widget.getParent` can be used to get the parent of a widget:: instance.oepetstore.GreetingsWidget = instance.web.Widget.extend({ start: function() { console.log(this.getParent().$el ); // will print "div.oe_petstore_homepage" in the console }, }); :func:`~openerp.web.Widget.getChildren` can be used to get a list of its children:: instance.oepetstore.HomePage = instance.web.Widget.extend({ start: function() { var greeting = new instance.oepetstore.GreetingsWidget(this); greeting.appendTo(this.$el); console.log(this.getChildren()[0].$el); // will print "div.oe_petstore_greetings" in the console }, }); You should also remember that, when you override the :func:`~openerp.web.Widget.init` method of a widget you should always put the parent as first parameter are pass it to ``this._super()``:: instance.oepetstore.GreetingsWidget = instance.web.Widget.extend({ init: function(parent, name) { this._super(parent); this.name = name; }, }); Finally, if a widget does not logically have a parent (ie: because it's the first widget you instantiate in an application), you can give null as a parent instead:: new instance.oepetstore.GreetingsWidget(null); Destroying Widgets %%%%%%%%%%%%%%%%%% If you can display content to your users, you should also be able to erase it. This can simply be done using the :func:`~openerp.web.Widget.destroy` method: greeting.destroy(); When a widget is destroyed it will first call :func:`~openerp.web.Widget.destroy` on all its children. Then it erases itself from the DOM. The recursive call to destroy from parents to children is very useful to clean properly complex structures of widgets and avoid memory leaks that can easily appear in big JavaScript applications. .. _howtos/web/qweb: The QWeb Template Engine ------------------------ The previous part of the guide showed how to define widgets that are able to display HTML to the user. The example ``GreetingsWidget`` used a syntax like this:: this.$el.append("
Hello dear Odoo user!
"); This technically allow us to display any HTML, even if it is very complex and require to be generated by code. Although generating text using pure JavaScript is not very nice, that would necessitate to copy-paste a lot of HTML lines inside our JavaScript source file, add the ``"`` character at the beginning and the end of each line, etc... The problem is exactly the same in most programming languages needing to generate HTML. That's why they typically use template engines. Example of template engines are Velocity, JSP (Java), Mako, Jinja (Python), Smarty (PHP), etc... In Odoo we use a template engine developed specifically for Odoo's web client. Its name is QWeb. QWeb is an XML-based templating language, similar to `Genshi `_, `Thymeleaf `_ or `Facelets `_ with a few peculiarities: * It's implemented fully in JavaScript and rendered in the browser. * Each template file (XML files) contains multiple templates, where template engine usually have a 1:1 mapping between template files and templates. * It has special support in Odoo Web's :class:`~openerp.web.Widget`, though it can be used outside of Odoo's web client (and it's possible to use :class:`~openerp.web.Widget` without relying on QWeb). The rationale behind using QWeb instead of existing javascript template engines is that its extension mechanism is very similar to the Odoo view inheritance mechanism. Like Odoo views a QWeb template is an XML tree and therefore XPath or DOM manipulations are easy to perform on it. Using QWeb inside a Widget %%%%%%%%%%%%%%%%%%%%%%%%%% First let's define a simple QWeb template in ``oepetstore/static/src/xml/petstore.xml`` file, the exact meaning will be explained later: .. code-block:: xml
This is some simple HTML
Now let's modify the ``HomePage`` class. Remember that enigmatic line at the beginning the the JavaScript source file? :: var QWeb = instance.web.qweb; This is a line we recommend to copy-paste in all Odoo web modules. It is the object giving access to all templates defined in template files that were loaded by the web client. We can use the template we defined in our XML template file like this:: instance.oepetstore.HomePage = instance.web.Widget.extend({ start: function() { this.$el.append(QWeb.render("HomePageTemplate")); }, }); Calling the ``QWeb.render()`` method asks to render the template identified by the string passed as first parameter. Another possibility commonly seen in Odoo code is to use ``Widget``'s integration with QWeb:: instance.oepetstore.HomePage = instance.web.Widget.extend({ template: "HomePageTemplate", start: function() { ... }, }); When you put a ``template`` class attribute in a widget, the widget knows it has to call ``QWeb.render()`` to render that template. Please note there is a difference between those two syntaxes. When you use ``Widget``'s QWeb integration the ``QWeb.render()`` method is called *before* the widget calls :func:`~openerp.web.Widget.start`. It will also take the root element of the rendered template and put it as a replacement of the default root element generated by the :class:`~openerp.web.Widget` class. This will alter the behavior, so you should remember it. QWeb Context '''''''''''' Like with all template engines, QWeb templates can contain code able to manipulate data that is given to the template. To pass data to QWeb, use the second argument to ``QWeb.render()``: .. code-block:: xml
Hello
:: QWeb.render("HomePageTemplate", {name: "Nicolas"}); Result: .. code-block:: html
Hello Nicolas
When you use :class:`~openerp.web.Widget`'s integration you can not pass additional data to the template. Instead the template will have a unique ``widget`` variable which is a reference to the current widget: .. code-block:: xml
Hello
:: instance.oepetstore.HomePage = instance.web.Widget.extend({ template: "HomePageTemplate", init: function(parent) { this._super(parent); this.name = "Nicolas"; }, start: function() { }, }); Result: .. code-block:: html
Hello Nicolas
Template Declaration '''''''''''''''''''' Now that we know everything about rendering templates we can try to understand QWeb's syntax. All QWeb directives use XML attributes beginning with the prefix ``t-``. To declare new templates, we add a ```` element into the XML template file inside the root element ````::
This is some simple HTML
``t-name`` simply declares a template that can be called using ``QWeb.render()``. Escaping '''''''' To put some text in the HTML, use ``t-esc``: .. code-block:: xml
Hello
This will output the variable ``name`` and escape its content in case it contains some characters that looks like HTML. Please note the attribute ``t-esc`` can contain any type of JavaScript expression: .. code-block:: xml
Will render: .. code-block:: html
8
Outputting HTML ''''''''''''''' If you know you have some HTML contained in a variable, use ``t-raw`` instead of ``t-esc``: .. code-block:: xml
If '' The basic alternative block of QWeb is ``t-if``: .. code-block:: xml
true is true true is not true
Although QWeb does not contains any structure for else. Foreach ''''''' To iterate on a list, use ``t-foreach`` and ``t-as``: .. code-block:: xml
Hello
Setting the Value of an XML Attribute ''''''''''''''''''''''''''''''''''''' QWeb has a special syntax to set the value of an attribute. You must use ``t-att-xxx`` and replace ``xxx`` with the name of the attribute: .. code-block:: xml
Input your name:
To Learn More About QWeb '''''''''''''''''''''''' For a QWeb reference, see :ref:`reference/qweb`. Exercise '''''''' .. exercise:: Usage of QWeb in Widgets Create a widget whose constructor contains two parameters aside from ``parent``: ``product_names`` and ``color``. ``product_names`` is a list of strings, each one being a name of product. ``color`` is a string containing a color in CSS color format (ie: ``#000000`` for black). That widget should display the given product names one under the other, each one in a separate box with a background color with the value of ``color`` and a border. You must use QWeb to render the HTML. This exercise will necessitate some CSS that you should put in ``oepetstore/static/src/css/petstore.css``. Display that widget in the ``HomePage`` widget with a list of five products and green as the background color for boxes. .. only:: solutions :: openerp.oepetstore = function(instance) { var _t = instance.web._t, _lt = instance.web._lt; var QWeb = instance.web.qweb; instance.oepetstore = {}; instance.oepetstore.HomePage = instance.web.Widget.extend({ start: function() { var products = new instance.oepetstore.ProductsWidget(this, ["cpu", "mouse", "keyboard", "graphic card", "screen"], "#00FF00"); products.appendTo(this.$el); }, }); instance.oepetstore.ProductsWidget = instance.web.Widget.extend({ template: "ProductsWidget", init: function(parent, products, color) { this._super(parent); this.products = products; this.color = color; }, }); instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage'); } .. code-block:: xml

.. code-block:: css .oe_products_item { display: inline-block; padding: 3px; margin: 5px; border: 1px solid black; border-radius: 3px; } .. image:: web/qweb.* :align: center :width: 70% Widget Events and Properties ---------------------------- Widgets still have more helper to learn. One of the more complex (and useful) one is the event system. Events are also closely related to the widget properties. Events %%%%%% Widgets are able to fire events in a similar way most components in existing graphical user interfaces libraries (Qt, GTK, Swing,...) handle them. Example:: instance.oepetstore.ConfirmWidget = instance.web.Widget.extend({ start: function() { var self = this; this.$el.append("
Are you sure you want to perform this action?
" + "" + ""); this.$el.find("button.ok_button").click(function() { self.trigger("user_choose", true); }); this.$el.find("button.cancel_button").click(function() { self.trigger("user_choose", false); }); }, }); instance.oepetstore.HomePage = instance.web.Widget.extend({ start: function() { var widget = new instance.oepetstore.ConfirmWidget(this); widget.on("user_choose", this, this.user_choose); widget.appendTo(this.$el); }, user_choose: function(confirm) { if (confirm) { console.log("The user agreed to continue"); } else { console.log("The user refused to continue"); } }, }); First, we will explain what this example is supposed to do. We create a generic widget to ask the user if he really wants to do an action that could have important consequences (a type widget heavily used in Windows). To do so, we put two buttons in the widget. Then we bind jQuery events to know when the user click these buttons. .. note:: It could be hard to understand this particular line:: var self = this; Remember, in JavaScript the variable ``this`` is a variable that is passed implicitly to all functions. It allows us to know which is the object if function is used like a method. Each declared function has its own ``this``. So, when we declare a function inside a function, that new function will have its own ``this`` that could be different from the ``this`` of the parent function. If we want to remember the original object the simplest method is to store a reference in a variable. By convention in Odoo we very often name that variable ``self`` because it's the equivalent of ``this`` in Python. Since our widget is supposed to be generic, it should not perform any precise action by itself. So, we simply make it trigger and event named ``user_choose`` by using the :func:`~openerp.web.Widget.trigger` method. :func:`~openerp.web.Widget.trigger` takes as first argument the name of the event to trigger. Then it can takes any number of additional arguments. These arguments will be passed to all the event listeners. Then we modify the ``HomePage`` widget to instantiate a ``ConfirmWidget`` and listen to its ``user_choose`` event by calling the :func:`~openerp.web.Widget.on` method. :func:`~openerp.web.Widget.on` allows to bind a function to be called when the event identified by event_name is ``triggered``. The ``func`` argument is the function to call and ``object`` is the object to which that function is related if it is a method. The binded function will be called with the additional arguments of :func:`~openerp.web.Widget.trigger` if it has any. Example:: start: function() { var widget = ... widget.on("my_event", this, this.my_event_triggered); widget.trigger("my_event", 1, 2, 3); }, my_event_triggered: function(a, b, c) { console.log(a, b, c); // will print "1 2 3" } Properties %%%%%%%%%% Properties are very similar to normal object attributes. They allow to set data on an object but with an additional feature: it triggers events when a property's value has changed:: start: function() { this.widget = ... this.widget.on("change:name", this, this.name_changed); this.widget.set("name", "Nicolas"); }, name_changed: function() { console.log("The new value of the property 'name' is", this.widget.get("name")); } :func:`~openerp.web.Widget.set` allows to set the value of property. If the value changed (or it didn't had a value previously) the object will trigger a ``change:xxx`` where ``xxx`` is the name of the property. :func:`~openerp.web.Widget.get` allows to retrieve the value of a property. Exercise %%%%%%%% .. exercise:: Widget Properties and Events Create a widget ``ColorInputWidget`` that will display 3 ````. Each of these ```` is dedicated to type a hexadecimal number from 00 to FF. When any of these ```` is modified by the user the widget must query the content of the three ````, concatenate their values to have a complete CSS color code (ie: ``#00FF00``) and put the result in a property named ``color``. Please note the jQuery ``change()`` event that you can bind on any HTML ```` element and the ``val()`` method that can query the current value of that ```` could be useful to you for this exercise. Then, modify the ``HomePage`` widget to instantiate ``ColorInputWidget`` and display it. The ``HomePage`` widget should also display an empty rectangle. That rectangle must always, at any moment, have the same background color than the color in the ``color`` property of the ``ColorInputWidget`` instance. Use QWeb to generate all HTML. .. only:: solutions :: openerp.oepetstore = function(instance) { var _t = instance.web._t, _lt = instance.web._lt; var QWeb = instance.web.qweb; instance.oepetstore = {}; instance.oepetstore.ColorInputWidget = instance.web.Widget.extend({ template: "ColorInputWidget", start: function() { var self = this; this.$el.find("input").change(function() { self.input_changed(); }); self.input_changed(); }, input_changed: function() { var color = "#"; color += this.$el.find(".oe_color_red").val(); color += this.$el.find(".oe_color_green").val(); color += this.$el.find(".oe_color_blue").val(); this.set("color", color); }, }); instance.oepetstore.HomePage = instance.web.Widget.extend({ template: "HomePage", start: function() { this.colorInput = new instance.oepetstore.ColorInputWidget(this); this.colorInput.on("change:color", this, this.color_changed); this.colorInput.appendTo(this.$el); }, color_changed: function() { this.$el.find(".oe_color_div").css("background-color", this.colorInput.get("color")); }, }); instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage'); } .. code-block:: xml
Red:
Green:
Blue:
.. code-block:: css .oe_color_div { width: 100px; height: 100px; margin: 10px; } .. note:: jQuery's ``css()`` method allows setting a css property. Widget Helpers -------------- We've seen the basics of the :class:`~openerp.web.Widget` class, QWeb and the events/properties system. There are still some more useful methods proposed by this class. ``Widget``'s jQuery Selector %%%%%%%%%%%%%%%%%%%%%%%%%%%% It is very common to need to select a precise element inside a widget. In the previous part of this guide we've seen a lot of uses of the ``find()`` method of jQuery objects:: this.$el.find("input.my_input")... :class:`~openerp.web.Widget` provides a shorter syntax that does the same thing with the :func:`~openerp.web.Widget.$` method:: instance.oepetstore.MyWidget = instance.web.Widget.extend({ start: function() { this.$("input.my_input")... }, }); .. note:: We strongly advise you against using directly the global jQuery function ``$()`` like we did in the previous chapter were we explained the jQuery library and jQuery selectors. That type of global selection is sufficient for simple applications but is not a good idea in real, big web applications. The reason is simple: when you create a new type of widget you never know how many times it will be instantiated. Since the ``$()`` global function operates in *the whole HTML displayed in the browser*, if you instantiate a widget 2 times and use that function you will incorrectly select the content of another instance of your widget. That's why you must restrict the jQuery selections to HTML which is located *inside* your widget most of the time. Applying the same logic, you can also guess it is a very bad idea to try to use HTML ids in any widget. If the widget is instantiated 2 times you will have 2 different HTML element in the whole application that have the same id. And that is an error by itself. So you should stick to CSS classes to mark your HTML elements in all cases. Easier DOM Events Binding %%%%%%%%%%%%%%%%%%%%%%%%% In the previous part, we had to bind a lot of HTML element events like ``click()`` or ``change()``. Now that we have the ``$()`` method to simplify code a little, let's see how it would look like:: instance.oepetstore.MyWidget = instance.web.Widget.extend({ start: function() { var self = this; this.$(".my_button").click(function() { self.button_clicked(); }); }, button_clicked: function() { .. }, }); It's still a bit long to type. That's why there is an even more simple syntax for that:: instance.oepetstore.MyWidget = instance.web.Widget.extend({ events: { "click .my_button": "button_clicked", }, button_clicked: function() { .. } }); .. warning:: It's important to differentiate the jQuery events that are triggered on DOM elements and events of the widgets. The ``event`` class attribute *is a helper to help binding jQuery events*, it has nothing to do with the widget events that can be binded using the ``on()`` method. The ``event`` class attribute is a dictionary that allows to define jQuery events with a shorter syntax. The key is a string with 2 different parts separated with a space. The first part is the name of the event, the second one is the jQuery selector. So the key ``click .my_button`` will bind the event ``click`` on the elements matching the selector ``my_button``. The value is a string with the name of the method to call on the current object. Development Guidelines %%%%%%%%%%%%%%%%%%%%%% As explained in the prerequisites to read this guide, you should already know HTML and CSS. But developing web applications in JavaScript or developing web modules for Odoo require to be more strict than you will usually be when simply creating static web pages with CSS to style them. So these guidelines should be followed if you want to have manageable projects and avoid bugs or common mistakes: * Identifiers (``id`` attribute) should be avoided. In generic applications and modules, ``id`` limits the re-usability of components and tends to make code more brittle. Just about all the time, they can be replaced with nothing, with classes or with keeping a reference to a DOM node or a jQuery element around. .. note:: If it is absolutely necessary to have an ``id`` (because a third-party library requires one and can't take a DOM element), it should be generated with ``_.uniqueId()``. * Avoid predictable/common CSS class names. Class names such as "content" or "navigation" might match the desired meaning/semantics, but it is likely an other developer will have the same need, creating a naming conflict and unintended behavior. Generic class names should be prefixed with e.g. the name of the component they belong to (creating "informal" namespaces, much as in C or Objective-C). * Global selectors should be avoided. Because a component may be used several times in a single page (an example in Odoo is dashboards), queries should be restricted to a given component's scope. Unfiltered selections such as ``$(selector)`` or ``document.querySelectorAll(selector)`` will generally lead to unintended or incorrect behavior. Odoo Web's :class:`~openerp.web.Widget` has an attribute providing its DOM root (:attr:`~openerp.web.Widget.$el`), and a shortcut to select nodes directly (:func:`~openerp.web.Widget.$`). * More generally, never assume your components own or controls anything beyond its own personal :attr:`~openerp.web.Widget.$el` * html templating/rendering should use QWeb unless absolutely trivial. * All interactive components (components displaying information to the screen or intercepting DOM events) must inherit from Widget and correctly implement and use its API and life cycle. Modify Existent Widgets and Classes ----------------------------------- The class system of the Odoo web framework allows direct modification of existing classes using the :func:`~openerp.web.Widget.include` method of a class:: var TestClass = instance.web.Class.extend({ testMethod: function() { return "hello"; }, }); TestClass.include({ testMethod: function() { return this._super() + " world"; }, }); console.log(new TestClass().testMethod()); // will print "hello world" This system is similar to the inheritance mechanism, except it will directly modify the class. You can call ``this._super()`` to call the original implementation of the methods you are redefining. If the class already had sub-classes, all calls to ``this._super()`` in sub-classes will call the new implementations defined in the call to ``include()``. This will also work if some instances of the class (or of any of its sub-classes) were created prior to the call to :func:`~openerp.web.Widget.include`. .. warning:: Please note that, even if :func:`~openerp.web.Widget.include` can be a powerful tool, it's not considered a very good programming practice because it can easily create problems if used in a wrong way. So you should use it to modify the behavior of an existing component only when there are no other options, and try to limit its usages to the strict minimum. Translations ------------ The process to translate text in Python and JavaScript code is very similar. You could have noticed these lines at the beginning of the ``petstore.js`` file: var _t = instance.web._t, _lt = instance.web._lt; These lines are simply used to import the translation functions in the current JavaScript module. The correct to use them is this one:: this.$el.text(_t("Hello dear user!")); In Odoo, translations files are automatically generated by scanning the source code. All piece of code that calls a certain function are detected and their content is added to a translation file that will then be sent to the translators. In Python, the function is ``_()``. In JavaScript the function is :func:`~openerp.web._t` (and also :func:`~openerp.web._lt`). If the source file as never been scanned and the translation files does not contain any translation for the text given to ``_t()`` it will return the text as-is. If there is a translation it will return it. :func:`~openerp.web._lt` does almost the exact same thing but is a little bit more complicated. It does not return a text but returns a function that will return the text. It is reserved for very special cases:: var text_func = _lt("Hello dear user!"); this.$el.text(text_func()); To have more information about Odoo's translations, please take a look at the reference documentation: https://doc.openerp.com/contribute/translations/ . Communication with the Odoo Server ------------------------------------- Now you should know everything you need to display any type of graphical user interface with your Odoo modules. Still, Odoo is a database-centric application so it's still not very useful if you can't query data from the database. As a reminder, in Odoo you are not supposed to directly query data from the PostgreSQL database, you will always use the build-in ORM (Object-Relational Mapping) and more precisely the Odoo *models*. Contacting Models %%%%%%%%%%%%%%%%% In the previous chapter we explained how to send HTTP requests to the web server using the ``$.ajax()`` method and the JSON format. It is useful to know how to make a JavaScript application communicate with its web server using these tools, but it's still a little bit low-level to be used in a complex application like Odoo. When the web client contacts the Odoo server it has to pass additional data like the necessary information to authenticate the current user. There is also some more complexity due to Odoo models that need a higher-level communication protocol to be used. This is why you will not use directly ``$.ajax()`` to communicate with the server. The web client framework provides classes to abstract that protocol. To demonstrate this, the file ``petstore.py`` already contains a small model with a sample method: .. code-block:: python class message_of_the_day(osv.osv): _name = "message_of_the_day" def my_method(self, cr, uid, context=None): return {"hello": "world"} _columns = { 'message': fields.text(string="Message"), 'color': fields.char(string="Color", size=20), } If you know Odoo models that code should be familiar to you. This model declares a table named ``message_of_the_day`` with two fields. It also has a method ``my_method()`` that doesn't do much except return a dictionary. Here is a sample widget that calls ``my_method()`` and displays the result:: instance.oepetstore.HomePage = instance.web.Widget.extend({ start: function() { var self = this; var model = new instance.web.Model("message_of_the_day"); model.call("my_method", [], {context: new instance.web.CompoundContext()}).then(function(result) { self.$el.append("
Hello " + result["hello"] + "
"); // will show "Hello world" to the user }); }, }); The class used to contact Odoo models is ``instance.web.Model``. When you instantiate it, you must give as first argument to its constructor the name of the model you want to contact in Odoo. (Here it is ``message_of_the_day``, the model created for this example, but it could be any other model like ``res.partner``.) :func:`~openerp.web.Model.call` is the method of :class:`~openerp.web.Model` used to call any method of an Odoo server-side model. Here are its arguments: * ``name`` is the name of the method to call on the model. Here it is the method named ``my_method``. * ``args`` is a list of positional arguments to give to the method. The sample ``my_method()`` method does not contain any particular argument we want to give to it, so here is another example: .. code-block:: python def my_method2(self, cr, uid, a, b, c, context=None): ... .. code-block:: javascript model.call("my_method", [1, 2, 3], ... // with this a=1, b=2 and c=3 * ``kwargs`` is a list of named arguments to give to the method. In the example, we have one named argument which is a bit special: ``context``. It's given a value that may seem very strange right now: ``new instance.web.CompoundContext()``. The meaning of that argument will be explained later. Right now you should just know the ``kwargs`` argument allows to give arguments to the Python method by name instead of position. Example: .. code-block:: python def my_method2(self, cr, uid, a, b, c, context=None): ... .. code-block:: javascript model.call("my_method", [], {a: 1, b: 2, c: 3, ... // with this a=1, b=2 and c=3 .. note:: If you take a look at the ``my_method()``'s declaration in Python, you can see it has two arguments named ``cr`` and ``uid``: .. code-block:: python def my_method(self, cr, uid, context=None): You could have noticed we do not give theses arguments to the server when we call that method from JavaScript. That is because theses arguments that have to be declared in all models' methods are never sent from the Odoo client. These arguments are added implicitly by the Odoo server. The first one is an object called the *cursor* that allows communication with the database. The second one is the id of the currently logged in user. :func:`~openerp.web.Widget.call` returns a deferred resolved with the value returned by the model's method as first argument. If you don't know what deferreds are, take a look at the previous chapter (the part about HTTP requests in jQuery). CompoundContext %%%%%%%%%%%%%%% In the previous part, we avoided to explain the strange ``context`` argument in the call to our model's method: .. code-block:: javascript model.call("my_method", [], {context: new instance.web.CompoundContext()}) In Odoo, models' methods should always have an argument named ``context``: .. code-block:: python def my_method(self, cr, uid, context=None): ... The context is like a "magic" argument that the web client will always give to the server when calling a method. The context is a dictionary containing multiple keys. One of the most important key is the language of the user, used by the server to translate all the messages of the application. Another one is the time zone of the user, used to compute correctly dates and times if Odoo is used by people in different countries. The ``argument`` is necessary in all methods, because if we forget it bad things could happen (like the application not being translated correctly). That's why, when you call a model's method, you should always give it to that argument. The solution to achieve that is to use :class:`openerp.web.CompoundContext`. :class:`~openerp.web.CompoundContext` is a class used to pass the user's context (with language, time zone, etc...) to the server as well as adding new keys to the context (some models' methods use arbitrary keys added to the context). It is created by giving to its constructor any number of dictionaries or other :class:`~openerp.web.CompoundContext` instances. It will merge all those contexts before sending them to the server. .. code-block:: javascript model.call("my_method", [], {context: new instance.web.CompoundContext({'new_key': 'key_value'})}) .. code-block:: python def display_context(self, cr, uid, context=None): print context // will print: {'lang': 'en_US', 'new_key': 'key_value', 'tz': 'Europe/Brussels', 'uid': 1} You can see the dictionary in the argument ``context`` contains some keys that are related to the configuration of the current user in Odoo plus the ``new_key`` key that was added when instantiating :class:`~openerp.web.CompoundContext`. To resume, you should always add an instance of :class:`~openerp.web.CompoundContext` in all calls to a model's method. Queries %%%%%%% If you know Odoo module development, you should already know everything necessary to communicate with models and make them do what you want. But there is still a small helper that could be useful to you : :func:`~openerp.web.Model.query`. :func:`~openerp.web.Model.query` is a shortcut for the usual combination of :py:meth:`~openerp.models.Model.search` and ::py:meth:`~openerp.models.Model.read` methods in Odoo models. It allows to :search records and get their data with a shorter syntax. Example:: model.query(['name', 'login', 'user_email', 'signature']) .filter([['active', '=', true], ['company_id', '=', main_company]]) .limit(15) .all().then(function (users) { // do work with users records }); :func:`~openerp.web.Model.query` takes as argument a list of fields to query in the model. It returns an instance of the :class:`openerp.web.Query` class. :class:`~openerp.web.Query` is a class representing the query you are trying to construct before sending it to the server. It has multiple methods you can call to customize the query. All these methods will return the current instance of :class:`~openerp.web.Query`: * :func:`~openerp.web.Query.filter` allows to specify an Odoo *domain*. As a reminder, a domain in Odoo is a list of conditions, each condition is a list it self. * :func:`~openerp.web.Query.limit` sets a limit to the number of records returned. When you have customized you query, you can call the :func:`~openerp.web.Query.all` method. It will performs the real query to the server and return a deferred resolved with the result. The result is the same thing return by the model's method :py:meth:`~openerp.models.Model.read` (a list of dictionaries containing the asked fields). Exercises --------- .. exercise:: Message of the Day Create a widget ``MessageOfTheDay`` that will display the message contained in the last record of the ``message_of_the_day``. The widget should query the message as soon as it is inserted in the DOM and display the message to the user. Display that widget on the home page of the Odoo Pet Store module. .. only:: solutions .. code-block:: javascript openerp.oepetstore = function(instance) { var _t = instance.web._t, _lt = instance.web._lt; var QWeb = instance.web.qweb; instance.oepetstore = {}; instance.oepetstore.HomePage = instance.web.Widget.extend({ template: "HomePage", start: function() { var motd = new instance.oepetstore.MessageOfTheDay(this); motd.appendTo(this.$el); }, }); instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage'); instance.oepetstore.MessageOfTheDay = instance.web.Widget.extend({ template: "MessageofTheDay", init: function() { this._super.apply(this, arguments); }, start: function() { var self = this; new instance.web.Model("message_of_the_day").query(["message"]).first().then(function(result) { self.$(".oe_mywidget_message_of_the_day").text(result.message); }); }, }); } .. code-block:: xml

.. code-block:: css .oe_petstore_motd { margin: 5px; padding: 5px; border-radius: 3px; background-color: #F0EEEE; } .. exercise:: Pet Toys List Create a widget ``PetToysList`` that will display 5 toys on the home page with their names and their images. In this Odoo addon, the pet toys are not stored in a new table like for the message of the day. They are in the table ``product.product``. If you click on the menu item :menuselection:`Pet Store --> Pet Store --> Pet Toys` you will be able to see them. Pet toys are identified by the category named ``Pet Toys``. You could need to document yourself on the model ``product.product`` to be able to create a domain to select pet toys and not all the products. To display the images of the pet toys, you should know that images in Odoo can be queried from the database like any other fields, but you will obtain a string containing Base64-encoded binary. There is a little trick to display images in Base64 format in HTML: .. code-block:: html The ``PetToysList`` widget should be displayed on the home page on the right of the ``MessageOfTheDay`` widget. You will need to make some layout with CSS to achieve this. .. only:: solutions .. code-block:: javascript openerp.oepetstore = function(instance) { var _t = instance.web._t, _lt = instance.web._lt; var QWeb = instance.web.qweb; instance.oepetstore = {}; instance.oepetstore.HomePage = instance.web.Widget.extend({ template: "HomePage", start: function() { var pettoys = new instance.oepetstore.PetToysList(this); pettoys.appendTo(this.$(".oe_petstore_homepage_left")); var motd = new instance.oepetstore.MessageOfTheDay(this); motd.appendTo(this.$(".oe_petstore_homepage_right")); }, }); instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage'); instance.oepetstore.MessageOfTheDay = instance.web.Widget.extend({ template: "MessageofTheDay", init: function() { this._super.apply(this, arguments); }, start: function() { var self = this; new instance.web.Model("message_of_the_day").query(["message"]).first().then(function(result) { self.$(".oe_mywidget_message_of_the_day").text(result.message); }); }, }); instance.oepetstore.PetToysList = instance.web.Widget.extend({ template: "PetToysList", start: function() { var self = this; new instance.web.Model("product.product").query(["name", "image"]) .filter([["categ_id.name", "=", "Pet Toys"]]).limit(5).all().then(function(result) { _.each(result, function(item) { var $item = $(QWeb.render("PetToy", {item: item})); self.$el.append($item); }); }); }, }); } .. code-block:: xml

.. code-block:: css .oe_petstore_homepage { display: table; } .oe_petstore_homepage_left { display: table-cell; width : 300px; } .oe_petstore_homepage_right { display: table-cell; width : 300px; } .oe_petstore_motd { margin: 5px; padding: 5px; border-radius: 3px; background-color: #F0EEEE; } .oe_petstore_pettoyslist { padding: 5px; } .oe_petstore_pettoy { margin: 5px; padding: 5px; border-radius: 3px; background-color: #F0EEEE; } Existing web components ----------------------- In the previous part, we explained the Odoo web framework, a development framework to create and architecture graphical JavaScript applications. The current part is dedicated to the existing components of the Odoo web client and most notably those containing entry points for developers to create new widgets that will be inserted inside existing views or components. The Action Manager %%%%%%%%%%%%%%%%%% To display a view or show a popup, as example when you click on a menu button, Odoo use the concept of actions. Actions are pieces of information explaining what the web client should do. They can be loaded from the database or created on-the-fly. The component handling actions in the web client is the *Action Manager*. Using the Action Manager '''''''''''''''''''''''' A way to launch an action is to use a menu element targeting an action registered in the database. As a reminder, here is how is defined a typical action and its associated menu item: .. code-block:: xml Message of the day message_of_the_day form tree,form It is also possible to ask the Odoo client to load an action from a JavaScript code. To do so you have to create a dictionary explaining the action and then to ask the action manager to re-dispatch the web client to the new action. To send a message to the action manager, :class:`~openerp.web.Widget` has a shortcut that will automatically find the current action manager and execute the action. Here is an example call to that method:: instance.web.TestWidget = instance.web.Widget.extend({ dispatch_to_new_action: function() { this.do_action({ type: 'ir.actions.act_window', res_model: "product.product", res_id: 1, views: [[false, 'form']], target: 'current', context: {}, }); }, }); The method to call to ask the action manager to execute a new action is :func:`~openerp.web.Widget.do_action`. It receives as argument a dictionary defining the properties of the action. Here is a description of the most usual properties (not all of them may be used by all type of actions): * ``type``: The type of the action, which means the name of the model in which the action is stored. As example, use ``ir.actions.act_window`` to show views and ``ir.actions.client`` for client actions. * ``res_model``: For ``act_window`` actions, it is the model used by the views. * ``res_id``: The ``id`` of the record to display. * ``views``: For ``act_window`` actions, it is a list of the views to display. This argument must be a list of tuples with two components. The first one must be the identifier of the view (or ``false`` if you just want to use the default view defined for the model). The second one must be the type of the view. * ``target``: If the value is ``current``, the action will be opened in the main content part of the web client. The current action will be destroyed before loading the new one. If it is ``new``, the action will appear in a popup and the current action will not be destroyed. * ``context``: The context to use. .. exercise:: Jump to Product Modify the ``PetToysList`` component developed in the previous part to jump to a form view displaying the shown item when we click on the item in the list. .. only:: solutions .. code-block:: javascript instance.oepetstore.PetToysList = instance.web.Widget.extend({ template: "PetToysList", start: function() { var self = this; new instance.web.Model("product.product").query(["name", "image"]) .filter([["categ_id.name", "=", "Pet Toys"]]).limit(5).all().then(function(result) { _.each(result, function(item) { var $item = $(QWeb.render("PetToy", {item: item})); self.$el.append($item); $item.click(function() { self.item_clicked(item); }); }); }); }, item_clicked: function(item) { this.do_action({ type: 'ir.actions.act_window', res_model: "product.product", res_id: item.id, views: [[false, 'form']], target: 'current', context: {}, }); }, }); Client Actions %%%%%%%%%%%%%% In the module installed during the previous part of this guide, we defined a simple widget that was displayed when we clicked on a menu element. This is because this widget was registered as a *client action*. Client actions are a type of action that are completely defined by JavaScript code. Here is a reminder of the way we defined this client action:: instance.oepetstore.HomePage = instance.web.Widget.extend({ start: function() { console.log("pet store home page loaded"); }, }); instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage'); ``instance.web.client_actions`` is an instance of the :class:`~openerp.web.Registry` class. Registries are not very different to simple dictionaries, except they assign strings to class names. Adding the ``petstore.homepage`` key to this registry simply tells the web client "If someone asks you to open a client action with key ``petstore.homepage``, instantiate the ``instance.oepetstore.HomePage`` class and show it to the user". Here is how the menu element to show this client action was defined: .. code-block:: xml petstore.homepage Client actions do not need a lot of information except their type, which is stored in the ``tag`` field. When the web client wants to display a client action, it will simply show it in the main content block of the web client. This is completely sufficient to allow the widget to display anything and so create a completely new feature for the web client. Architecture of the Views %%%%%%%%%%%%%%%%%%%%%%%%% Most of the complexity of the web client resides in views. They are the basic tools to display the data in the database. The part will explain the views and how those are displayed in the web client. The View Manager '''''''''''''''' Previously we already explained the purpose of the *Action Manager*. It is a component, whose class is ``ActionManager``, that will handle the Odoo actions (notably the actions associated with menu buttons). When an ``ActionManager`` instance receive an action with type ``ir.actions.act_window``, it knows it has to show one or more views associated with a precise model. To do so, it creates a *View Manager* that will create one or multiple *Views*. See this diagram: .. image:: web/viewarchitecture.* :align: center :width: 40% The ``ViewManager`` instance will instantiate each view class corresponding to the views indicated in the ``ir.actions.act_window`` action. As example, the class corresponding to the view type ``form`` is ``FormView``. Each view class inherits the ``View`` abstract class. The Views ''''''''' All the typical type of views in Odoo (all those you can switch to using the small buttons under the search input text) are represented by a class extending the ``View`` abstract class. Note the *Search View* (the search input text on the top right of the screen that typically appear in kanban and list views) is also considered a type of view even if it doesn't work like the others (you can not "switch to" the search view and it doesn't take the full screen). A view has the responsibility to load its XML view description from the server and display it. Views are also given an instance of the ``DataSet`` class. That class contains a list of identifiers corresponding to records that the view should display. It is filled by the search view and the current view is supposed to display the result of each search after it was performed by the search view. The Form View Fields %%%%%%%%%%%%%%%%%%%% A typical need in the web client is to extend the form view to display more specific widgets. One of the possibilities to do this is to define a new type of *Field*. A field, in the form view, is a type of widget designed to display and edit the content of *one (and only one) field* in a single record displayed by the form view. All data types available in models have a default implementation to display and edit them in the form view. As example, the ``FieldChar`` class allows to edit the ``char`` data type. Other field classes simply provide an alternative widget to represent an existing data type. A good example of this is the ``FieldEmail`` class. There is no ``email`` type in the models of Odoo. That class is designed to display a ``char`` field assuming it contains an email (it will show a clickable link to directly send a mail to the person and will also check the validity of the mail address). Also note there is nothing that disallow a field class to work with more than one data type. As example, the ``FieldSelection`` class works with both ``selection`` and ``many2one`` field types. As a reminder, to indicate a precise field type in a form view XML description, you just have to specify the ``widget`` attribute: .. code-block:: xml It is also a good thing to notice that the form view field classes are also used in the editable list views. So, by defining a new field class, it make this new widget available in both views. Another type of extension mechanism for the form view is the *Form Widget*, which has fewer restrictions than the fields (even though it can be more complicated to implement). Form widgets will be explained later in this guide. Fields are instantiated by the form view after it has read its XML description and constructed the corresponding HTML representing that description. After that, the form view will communicate with the field objects using some methods. Theses methods are defined by the ``FieldInterface`` interface. Almost all fields inherit the ``AbstractField`` abstract class. That class defines some default mechanisms that need to be implemented by most fields. Here are some of the responsibilities of a field class: * The field class must display and allow the user to edit the value of the field. * It must correctly implement the 3 field attributes available in all fields of Odoo. The ``AbstractField`` class already implements an algorithm that dynamically calculates the value of these attributes (they can change at any moment because their value change according to the value of other fields). Their values are stored in *Widget Properties* (the widget properties were explained earlier in this guide). It is the responsibility of each field class to check these widget properties and dynamically adapt depending of their values. Here is a description of each of these attributes: * ``required``: The field must have a value before saving. If ``required`` is ``true`` and the field doesn't have a value, the method ``is_valid()`` of the field must return ``false``. * ``invisible``: When this is ``true``, the field must be invisible. The ``AbstractField`` class already has a basic implementation of this behavior that fits most fields. * ``readonly``: When ``true``, the field must not be editable by the user. Most fields in Odoo have a completely different behavior depending on the value of ``readonly``. As example, the ``FieldChar`` displays an HTML ```` when it is editable and simply displays the text when it is read-only. This also means it has much more code it would need to implement only one behavior, but this is necessary to ensure a good user experience. * Fields have two methods, ``set_value()`` and ``get_value()``, which are called by the form view to give it the value to display and get back the new value entered by the user. These methods must be able to handle the value as given by the Odoo server when a ``read()`` is performed on a model and give back a valid value for a ``write()``. Remember that the JavaScript/Python data types used to represent the values given by ``read()`` and given to ``write()`` is not necessarily the same in Odoo. As example, when you read a many2one, it is always a tuple whose first value is the id of the pointed record and the second one is the name get (ie: ``(15, "Agrolait")``). But when you write a many2one it must be a single integer, not a tuple anymore. ``AbstractField`` has a default implementation of these methods that works well for simple data type and set a widget property named ``value``. Please note that, to better understand how to implement fields, you are strongly encouraged to look at the definition of the ``FieldInterface`` interface and the ``AbstractField`` class directly in the code of the Odoo web client. Creating a New Type of Field '''''''''''''''''''''''''''' In this part we will explain how to create a new type of field. The example here will be to re-implement the ``FieldChar`` class and explain progressively each part. Simple Read-Only Field """""""""""""""""""""" Here is a first implementation that will only be able to display a text. The user will not be able to modify the content of the field. .. code-block:: javascript instance.oepetstore.FieldChar2 = instance.web.form.AbstractField.extend({ init: function() { this._super.apply(this, arguments); this.set("value", ""); }, render_value: function() { this.$el.text(this.get("value")); }, }); instance.web.form.widgets.add('char2', 'instance.oepetstore.FieldChar2'); In this example, we declare a class named ``FieldChar2`` inheriting from ``AbstractField``. We also register this class in the registry ``instance.web.form.widgets`` under the key ``char2``. That will allow us to use this new field in any form view by specifying ``widget="char2"`` in the ```` tag in the XML declaration of the view. In this example, we define a single method: ``render_value()``. All it does is display the widget property ``value``. Those are two tools defined by the ``AbstractField`` class. As explained before, the form view will call the method ``set_value()`` of the field to set the value to display. This method already has a default implementation in ``AbstractField`` which simply sets the widget property ``value``. ``AbstractField`` also watch the ``change:value`` event on itself and calls the ``render_value()`` when it occurs. So, ``render_value()`` is a convenience method to implement in child classes to perform some operation each time the value of the field changes. In the ``init()`` method, we also define the default value of the field if none is specified by the form view (here we assume the default value of a ``char`` field should be an empty string). Read-Write Field """""""""""""""" Fields that only display their content and don't give the possibility to the user to modify it can be useful, but most fields in Odoo allow edition too. This makes the field classes more complicated, mostly because fields are supposed to handle both and editable and non-editable mode, those modes are often completely different (for design and usability purpose) and the fields must be able to switch from one mode to another at any moment. To know in which mode the current field should be, the ``AbstractField`` class sets a widget property named ``effective_readonly``. The field should watch the changes in that widget property and display the correct mode accordingly. Example:: instance.oepetstore.FieldChar2 = instance.web.form.AbstractField.extend({ init: function() { this._super.apply(this, arguments); this.set("value", ""); }, start: function() { this.on("change:effective_readonly", this, function() { this.display_field(); this.render_value(); }); this.display_field(); return this._super(); }, display_field: function() { var self = this; this.$el.html(QWeb.render("FieldChar2", {widget: this})); if (! this.get("effective_readonly")) { this.$("input").change(function() { self.internal_set_value(self.$("input").val()); }); } }, render_value: function() { if (this.get("effective_readonly")) { this.$el.text(this.get("value")); } else { this.$("input").val(this.get("value")); } }, }); instance.web.form.widgets.add('char2', 'instance.oepetstore.FieldChar2'); .. code-block:: xml
In the ``start()`` method (which is called right after a widget has been appended to the DOM), we bind on the event ``change:effective_readonly``. That will allow use to redisplay the field each time the widget property ``effective_readonly`` changes. This event handler will call ``display_field()``, which is also called directly in ``start()``. This ``display_field()`` was created specifically for this field, it's not a method defined in ``AbstractField`` or any other class. This is the method we will use to display the content of the field depending we are in read-only mode or not. From now on the conception of this field is quite typical, except there is a lot of verifications to know the state of the ``effective_readonly`` property: * In the QWeb template used to display the content of the widget, it displays an ```` if we are in read-write mode and nothing in particular in read-only mode. * In the ``display_field()`` method, we have to bind on the ``change`` event of the ```` to know when the user has changed the value. When it happens, we call the ``internal_set_value()`` method with the new value of the field. This is a convenience method provided by the ``AbstractField`` class. That method will set a new value in the ``value`` property but will not trigger a call to ``render_value()`` (which is not necessary since the ```` already contains the correct value). * In ``render_value()``, we use a completely different code to display the value of the field depending if we are in read-only or in read-write mode. .. exercise:: Create a Color Field Create a ``FieldColor`` class. The value of this field should be a string containing a color code like those used in CSS (example: ``#FF0000`` for red). In read-only mode, this color field should display a little block whose color corresponds to the value of the field. In read-write mode, you should display an ````. That type of ```` is an HTML5 component that doesn't work in all browsers but works well in Google Chrome. So it's OK to use as an exercise. You can use that widget in the form view of the ``message_of_the_day`` model for its field named ``color``. As a bonus, you can change the ``MessageOfTheDay`` widget created in the previous part of this guide to display the message of the day with the background color indicated in the ``color`` field. .. only:: solutions .. code-block:: javascript instance.oepetstore.FieldColor = instance.web.form.AbstractField.extend({ init: function() { this._super.apply(this, arguments); this.set("value", ""); }, start: function() { this.on("change:effective_readonly", this, function() { this.display_field(); this.render_value(); }); this.display_field(); return this._super(); }, display_field: function() { var self = this; this.$el.html(QWeb.render("FieldColor", {widget: this})); if (! this.get("effective_readonly")) { this.$("input").change(function() { self.internal_set_value(self.$("input").val()); }); } }, render_value: function() { if (this.get("effective_readonly")) { this.$(".oe_field_color_content").css("background-color", this.get("value") || "#FFFFFF"); } else { this.$("input").val(this.get("value") || "#FFFFFF"); } }, }); instance.web.form.widgets.add('color', 'instance.oepetstore.FieldColor'); .. code-block:: xml
.. code-block:: css .oe_field_color_content { height: 20px; width: 50px; border: 1px solid black; } The Form View Custom Widgets %%%%%%%%%%%%%%%%%%%%%%%%%%%% Form fields can be useful, but their purpose is to edit a single field. To interact with the whole form view and have more liberty to integrate new widgets in it, it is recommended to create a custom form widget. Custom form widgets are widgets that can be added in any form view using a specific syntax in the XML definition of the view. Example: .. code-block:: xml This type of widget will simply be created by the form view during the creation of the HTML according to the XML definition. They have properties in common with the fields (like the ``effective_readonly`` property) but they are not assigned a precise field. And so they don't have methods like ``get_value()`` and ``set_value()``. They must inherit from the ``FormWidget`` abstract class. The custom form widgets can also interact with the fields of the form view by getting or setting their values using the ``field_manager`` attribute of ``FormWidget``. Here is an example usage:: instance.oepetstore.WidgetMultiplication = instance.web.form.FormWidget.extend({ start: function() { this._super(); this.field_manager.on("field_changed:integer_a", this, this.display_result); this.field_manager.on("field_changed:integer_b", this, this.display_result); this.display_result(); }, display_result: function() { var result = this.field_manager.get_field_value("integer_a") * this.field_manager.get_field_value("integer_b"); this.$el.text("a*b = " + result); } }); instance.web.form.custom_widgets.add('multiplication', 'instance.oepetstore.WidgetMultiplication'); This example custom widget is designed to take the values of two existing fields (those must exist in the form view) and print the result of their multiplication. It also refreshes each time the value of any of those fields changes. The ``field_manager`` attribute is in fact the ``FormView`` instance representing the form view. The methods that widgets can call on that form view are documented in the code of the web client in the ``FieldManagerMixin`` interface. The most useful features are: * The method ``get_field_value()`` which returns the value of a field. * When the value of a field is changed, for any reason, the form view will trigger an event named ``field_changed:xxx`` where ``xxx`` is the name of the field. * Also, it is possible to change the value of the fields using the method ``set_values()``. This method takes a dictionary as first and only argument whose keys are the names of the fields to change and values are the new values. .. exercise:: Show Coordinates on Google Map In this exercise we would like to add two new fields on the ``product.product`` model: ``provider_latitude`` and ``provider_longitude``. Those would represent coordinates on a map. We also would like you to create a custom widget able to display a map showing these coordinates. To display that map, you can simply use the Google Map service using an HTML code similar to this: .. code-block:: html Just replace ``XXX`` with the latitude and ``YYY`` with the longitude. You should display those two new fields as well as the map widget in a new page of the notebook displayed in the product form view. .. only:: solutions .. code-block:: javascript instance.oepetstore.WidgetCoordinates = instance.web.form.FormWidget.extend({ start: function() { this._super(); this.field_manager.on("field_changed:provider_latitude", this, this.display_map); this.field_manager.on("field_changed:provider_longitude", this, this.display_map); this.display_map(); }, display_map: function() { this.$el.html(QWeb.render("WidgetCoordinates", { "latitude": this.field_manager.get_field_value("provider_latitude") || 0, "longitude": this.field_manager.get_field_value("provider_longitude") || 0, })); } }); instance.web.form.custom_widgets.add('coordinates', 'instance.oepetstore.WidgetCoordinates'); .. code-block:: xml .. exercise:: Get the Current Coordinate Now we would like to display an additional button to automatically set the coordinates to the location of the current user. To get the coordinates of the user, an easy way is to use the geolocation JavaScript API. `See the online documentation to know how to use it`_. .. _See the online documentation to know how to use it: http://www.w3schools.com/html/html5_geolocation.asp Please also note that it wouldn't be very logical to allow the user to click on that button when the form view is in read-only mode. So, this custom widget should handle correctly the ``effective_readonly`` property just like any field. One way to do this would be to make the button disappear when ``effective_readonly`` is true. .. only:: solutions .. code-block:: javascript instance.oepetstore.WidgetCoordinates = instance.web.form.FormWidget.extend({ start: function() { this._super(); this.field_manager.on("field_changed:provider_latitude", this, this.display_map); this.field_manager.on("field_changed:provider_longitude", this, this.display_map); this.on("change:effective_readonly", this, this.display_map); this.display_map(); }, display_map: function() { var self = this; this.$el.html(QWeb.render("WidgetCoordinates", { "latitude": this.field_manager.get_field_value("provider_latitude") || 0, "longitude": this.field_manager.get_field_value("provider_longitude") || 0, })); this.$("button").toggle(! this.get("effective_readonly")); this.$("button").click(function() { navigator.geolocation.getCurrentPosition(_.bind(self.received_position, self)); }); }, received_position: function(obj) { var la = obj.coords.latitude; var lo = obj.coords.longitude; this.field_manager.set_values({ "provider_latitude": la, "provider_longitude": lo, }); }, }); instance.web.form.custom_widgets.add('coordinates', 'instance.oepetstore.WidgetCoordinates'); .. code-block:: xml .. _jQuery: http://jquery.org .. _Underscore.js: http://underscorejs.org