diff --git a/doc/howtos/web.rst b/doc/howtos/web.rst new file mode 100644 index 00000000000..0460a791d1d --- /dev/null +++ b/doc/howtos/web.rst @@ -0,0 +1,2348 @@ +========== +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 diff --git a/doc/howtos/web/qweb.png b/doc/howtos/web/qweb.png new file mode 100644 index 00000000000..2e2ca376f3c Binary files /dev/null and b/doc/howtos/web/qweb.png differ diff --git a/doc/howtos/web/viewarchitecture.dia b/doc/howtos/web/viewarchitecture.dia new file mode 100644 index 00000000000..0c28960a1ed Binary files /dev/null and b/doc/howtos/web/viewarchitecture.dia differ diff --git a/doc/howtos/web/viewarchitecture.png b/doc/howtos/web/viewarchitecture.png new file mode 100644 index 00000000000..5fa403bb6ec Binary files /dev/null and b/doc/howtos/web/viewarchitecture.png differ diff --git a/doc/howtos/web/viewarchitecture.svg b/doc/howtos/web/viewarchitecture.svg new file mode 100644 index 00000000000..7e5b97ccc86 --- /dev/null +++ b/doc/howtos/web/viewarchitecture.svg @@ -0,0 +1,89 @@ + + + + + + + ActionManager + + + + + + + + + View + + + + + + + + + ViewManager + + + + + + + + + + + + 1 + + + + + + + + 1..* + + + + + FormView + + + + + + + + + ListView + + + + + + + + + KanbanView + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/tutorials.rst b/doc/tutorials.rst index 14607cfa050..5cbfec9bae6 100644 --- a/doc/tutorials.rst +++ b/doc/tutorials.rst @@ -7,3 +7,4 @@ Tutorials howtos/website howtos/backend + howtos/web