documentation/content/developer/reference/javascript_reference.rst
Victor Feyens e96bc804d3 [FIX] content/developer/*: typos
Found with codespell
2021-05-31 14:44:18 +02:00

2231 lines
78 KiB
ReStructuredText

.. highlight:: javascript
.. default-domain:: js
=====================
Javascript Reference
=====================
This document presents the Odoo Javascript framework. This
framework is not a large application in term of lines of code, but it is quite
generic, because it is basically a machine to turn a declarative interface
description into a live application, able to interact with every model and
records in the database. It is even possible to use the web client to modify
the interface of the web client.
Overview
=========
The Javascript framework is designed to work with three main use cases:
- the *web client*: this is the private web application, where one can view and
edit business data. This is a single page application (the page is never
reloaded, only the new data is fetched from the server whenever it is needed)
- the *website*: this is the public part of Odoo. It allows an unidentified
user to browse some content, to shop or to perform many actions, as a client.
This is a classical website: various routes with controllers and some
javascript to make it work.
- the *point of sale*: this is the interface for the point of sale. It is a
specialized single page application.
Some javascript code is common to these three use cases, and is bundled together
(see below in the assets section). This document will focus mostly on the web
client design.
Web client
==========
Single Page Application
-----------------------
In short, the *webClient*, instance of *WebClient* is the root component of the
whole user interface. Its responsibility is to orchestrate all various
subcomponents, and to provide services, such as rpcs, local storage and more.
In runtime, the web client is a single page application. It does not need to
request a full page from the server each time the user perform an action. Instead,
it only requests what it needs, and then replaces/updates the view. Also, it
manages the url: it is kept in sync with the web client state.
It means that while a user is working on Odoo, the web client class (and the
action manager) actually creates and destroys many sub components. The state is
highly dynamic, and each widget could be destroyed at any time.
Overview of web client JS code
-------------------------------------
Here, we give a very quick overview on the web client code, in
the *web/static/src/js* addon. Note that it is deliberately not exhaustive.
We only cover the most important files/folders.
- *boot.js*: this is the file that defines the module system. It needs to be
loaded first.
- *core/*: this is a collection of lower level building blocks. Notably, it
contains the class system, the widget system, concurrency utilities, and many
other class/functions.
- *chrome/*: in this folder, we have most large widgets which make up most of
the user interface.
- *chrome/abstract_web_client.js* and *chrome/web_client.js*: together, these
files define the WebClient widget, which is the root widget for the web client.
- *chrome/action_manager.js*: this is the code that will convert an action into
a widget (for example a kanban or a form view)
- *chrome/search_X.js* all these files define the search view (it is not a view
in the point of view of the web client, only from the server point of view)
- *fields*: all main view field widgets are defined here
- *views*: this is where the views are located
Assets Management
=================
Managing assets in Odoo is not as straightforward as it is in some other apps.
One of the reason is that we have a variety of situations where some, but not all
the assets are required. For example, the needs of the web client, the point of
sale, the website or even the mobile application are different. Also, some
assets may be large, but are seldom needed. In that case, we sometimes want them
to be loaded lazily.
The main idea is that we define a set of *bundles* in xml. A bundle is here defined as
a collection of files (javascript, css, scss). In Odoo, the most important
bundles are defined in the file *addons/web/views/webclient_templates.xml*. It looks
like this:
.. code-block:: xml
<template id="web.assets_common" name="Common Assets (used in backend interface and website)">
<link rel="stylesheet" type="text/css" href="/web/static/lib/jquery.ui/jquery-ui.css"/>
...
<script type="text/javascript" src="/web/static/src/js/boot.js"></script>
...
</template>
The files in a bundle can then be inserted into a template by using the *t-call-assets*
directive:
.. code-block:: xml
<t t-call-assets="web.assets_common" t-js="false"/>
<t t-call-assets="web.assets_common" t-css="false"/>
Here is what happens when a template is rendered by the server with these directives:
- all the *scss* files described in the bundle are compiled into css files. A file
named *file.scss* will be compiled in a file named *file.scss.css*.
- if we are in *debug=assets* mode
- the *t-call-assets* directive with the *t-js* attribute set to false will
be replaced by a list of stylesheet tags pointing to the css files
- the *t-call-assets* directive with the *t-css* attribute set to false will
be replaced by a list of script tags pointing to the js files
- if we are not in *debug=assets* mode
- the css files will be concatenated and minified, then splits into files
with no more than 4096 rules (to get around an old limitation of IE9). Then,
we generate as many stylesheet tags as necessary
- the js files are concatenated and minified, then a script tag is generated
Note that the assets files are cached, so in theory, a browser should only load
them once.
Main bundles
------------
When the Odoo server is started, it checks the timestamp of each file in a bundle,
and if necessary, will create/recreate the corresponding bundles.
Here are some important bundles that most developers will need to know:
- *web.assets_common*: this bundle contains most assets which are common to the
web client, the website, and also the point of sale. This is supposed to contain
lower level building blocks for the odoo framework. Note that it contains the
*boot.js* file, which defines the odoo module system.
- *web.assets_backend*: this bundle contains the code specific to the web client
(notably the web client/action manager/views)
- *web.assets_frontend*: this bundle is about all that is specific to the public
website: ecommerce, forum, blog, event management, ...
Adding files in an asset bundle
-------------------------------
The proper way to add a file located in *addons/web* to a bundle is simple:
it is just enough to add a *script* or a *stylesheet* tag to the bundle in the
file *webclient_templates.xml*. But when we work in a different addon, we need
to add a file from that addon. In that case, it should be done in three steps:
1. add a *assets.xml* file in the *views/* folder
2. add the string 'views/assets.xml' in the 'data' key in the manifest file
3. create an inherited view of the desired bundle, and add the file(s) with an
xpath expression. For example,
.. code-block:: xml
<template id="assets_backend" name="helpdesk assets" inherit_id="web.assets_backend">
<xpath expr="//script[last()]" position="after">
<link rel="stylesheet" type="text/scss" href="/helpdesk/static/src/scss/helpdesk.scss"/>
<script type="text/javascript" src="/helpdesk/static/src/js/helpdesk_dashboard.js"></script>
</xpath>
</template>
.. note ::
Note that the files in a bundle are all loaded immediately when the user loads the
odoo web client. This means that the files are transferred through the network
every time (except when the browser cache is active). In some cases, it may be
better to lazyload some assets. For example, if a widget requires a large
library, and that widget is not a core part of the experience, then it may be
a good idea to only load the library when the widget is actually created. The
widget class has actually builtin support just for this use case. (see section
:ref:`reference/javascript_reference/qweb`)
What to do if a file is not loaded/updated
------------------------------------------
There are many different reasons why a file may not be properly loaded. Here
are a few things you can try to solve the issue:
- once the server is started, it does not know if an asset file has been
modified. So, you can simply restart the server to regenerate the assets.
- check the console (in the dev tools, usually opened with F12) to make sure
there are no obvious errors
- try to add a console.log at the beginning of your file (before any module
definition), so you can see if a file has been loaded or not
- in the user interface, in debug mode (INSERT LINK HERE TO DEBUG MODE), there
is an option to force the server to update its assets files.
- use the *debug=assets* mode. This will actually bypass the asset bundles (note
that it does not actually solve the issue. The server still uses outdated bundles)
- finally, the most convenient way to do it, for a developer, is to start the
server with the *--dev=all* option. This activates the file watcher options,
which will automatically invalidate assets when necessary. Note that it does
not work very well if the OS is Windows.
- remember to refresh your page!
- or maybe to save your code file...
.. note::
Once an asset file has been recreated, you need to refresh the page, to reload
the proper files (if that does not work, the files may be cached).
Javascript Module System
========================
Once we are able to load our javascript files into the browser, we need to make
sure they are loaded in the correct order. In order to do that, Odoo has defined
a small module system (located in the file *addons/web/static/src/js/boot.js*,
which needs to be loaded first).
The Odoo module system, inspired by AMD, works by defining the function *define*
on the global odoo object. We then define each javascript module by calling that
function. In the Odoo framework, a module is a piece of code that will be executed
as soon as possible. It has a name and potentially some dependencies. When its
dependencies are loaded, a module will then be loaded as well. The value of the
module is then the return value of the function defining the module.
As an example, it may look like this:
.. code-block:: javascript
// in file a.js
odoo.define('module.A', function (require) {
"use strict";
var A = ...;
return A;
});
// in file b.js
odoo.define('module.B', function (require) {
"use strict";
var A = require('module.A');
var B = ...; // something that involves A
return B;
});
An alternative way to define a module is to give explicitly a list of dependencies
in the second argument.
.. code-block:: javascript
odoo.define('module.Something', ['module.A', 'module.B'], function (require) {
"use strict";
var A = require('module.A');
var B = require('module.B');
// some code
});
If some dependencies are missing/non ready, then the module will simply not be
loaded. There will be a warning in the console after a few seconds.
Note that circular dependencies are not supported. It makes sense, but it means that one
needs to be careful.
Defining a module
-----------------
The *odoo.define* method is given three arguments:
- *moduleName*: the name of the javascript module. It should be a unique string.
The convention is to have the name of the odoo addon followed by a specific
description. For example, 'web.Widget' describes a module defined in the *web*
addon, which exports a *Widget* class (because the first letter is capitalized)
If the name is not unique, an exception will be thrown and displayed in the
console.
- *dependencies*: the second argument is optional. If given, it should be a list
of strings, each corresponding to a javascript module. This describes the
dependencies that are required to be loaded before the module is executed. If
the dependencies are not explicitly given here, then the module system will
extract them from the function by calling toString on it, then using a regexp
to find all *require* statements.
- finally, the last argument is a function which defines the module. Its return
value is the value of the module, which may be passed to other modules requiring
it. Note that there is a small exception for asynchronous modules, see the
next section.
If an error happens, it will be logged (in debug mode) in the console:
* ``Missing dependencies``:
These modules do not appear in the page. It is possible that the JavaScript
file is not in the page or that the module name is wrong
* ``Failed modules``:
A javascript error is detected
* ``Rejected modules``:
The module returns a rejected deferred. It (and its dependent modules) is not
loaded.
* ``Rejected linked modules``:
Modules who depend on a rejected module
* ``Non loaded modules``:
Modules who depend on a missing or a failed module
Asynchronous modules
---------------------
It can happen that a module needs to perform some work before it is ready. For
example, it could do a rpc to load some data. In that case, the module can
simply return a deferred (promise). In that case, the module system will simply
wait for the deferred to complete before registering the module.
.. code-block:: javascript
odoo.define('module.Something', ['web.ajax'], function (require) {
"use strict";
var ajax = require('web.ajax');
return ajax.rpc(...).then(function (result) {
// some code here
return something;
});
});
Best practices
----------------
- remember the convention for a module name: *addon name* suffixed with *module
name*.
- declare all your dependencies at the top of the module. Also, they should be
sorted alphabetically by module name. This makes it easier to understand your module.
- declare all exported values at the end
- try to avoid exporting too much things from one module. It is usually better
to simply export one thing in one (small/smallish) module.
- asynchronous modules can be used to simplify some use cases. For example,
the *web.dom_ready* module returns a deferred which will be resolved when the
dom is actually ready. So, another module that needs the DOM could simply have
a `require('web.dom_ready')` statement somewhere, and the code will only be
executed when the DOM is ready.
- try to avoid defining more than one module in one file. It may be convenient
in the short term, but this is actually harder to maintain.
Class System
============
Odoo was developed before ECMAScript 6 classes were available. In Ecmascript 5,
the standard way to define a class is to define a function and to add methods
on its prototype object. This is fine, but it is slightly complex when we want
to use inheritance, mixins.
For these reasons, Odoo decided to use its own class system, inspired by John
Resig. The base Class is located in *web.Class*, in the file *class.js*.
Creating a subclass
-------------------
Let us discuss how classes are created. The main mechanism is to use the
*extend* method (this is more or less the equivalent of *extend* in ES6 classes).
.. code-block:: javascript
var Class = require('web.Class');
var Animal = Class.extend({
init: function () {
this.x = 0;
this.hunger = 0;
},
move: function () {
this.x = this.x + 1;
this.hunger = this.hunger + 1;
},
eat: function () {
this.hunger = 0;
},
});
In this example, the *init* function is the constructor. It will be called when
an instance is created. Making an instance is done by using the *new* keyword.
Inheritance
-----------
It is convenient to be able to inherit an existing class. This is simply done
by using the *extend* method on the superclass. When a method is called, the
framework will secretly rebind a special method: *_super* to the currently
called method. This allows us to use *this._super* whenever we need to call a
parent method.
.. code-block:: javascript
var Animal = require('web.Animal');
var Dog = Animal.extend({
move: function () {
this.bark();
this._super.apply(this, arguments);
},
bark: function () {
console.log('woof');
},
});
var dog = new Dog();
dog.move()
Mixins
------
The odoo Class system does not support multiple inheritance, but for those cases
when we need to share some behaviour, we have a mixin system: the *extend*
method can actually take an arbitrary number of arguments, and will combine all
of them in the new class.
.. code-block:: javascript
var Animal = require('web.Animal');
var DanceMixin = {
dance: function () {
console.log('dancing...');
},
};
var Hamster = Animal.extend(DanceMixin, {
sleep: function () {
console.log('sleeping');
},
});
In this example, the *Hamster* class is a subclass of Animal, but it also mix
the DanceMixin in.
Patching an existing class
--------------------------
It is not common, but we sometimes need to modify another class *in place*. The
goal is to have a mechanism to change a class and all future/present instances.
This is done by using the *include* method:
.. code-block:: javascript
var Hamster = require('web.Hamster');
Hamster.include({
sleep: function () {
this._super.apply(this, arguments);
console.log('zzzz');
},
});
This is obviously a dangerous operation and should be done with care. But with
the way Odoo is structured, it is sometimes necessary in one addon to modify
the behavior of a widget/class defined in another addon. Note that it will
modify all instances of the class, even if they have already been created.
Widgets
=======
The *Widget* class is really an important building block of the user interface.
Pretty much everything in the user interface is under the control of a widget.
The Widget class is defined in the module *web.Widget*, in *widget.js*.
In short, the features provided by the Widget class include:
* parent/child relationships between widgets (*PropertiesMixin*)
* extensive lifecycle management with safety features (e.g. automatically
destroying children widgets during the destruction of a parent)
* automatic rendering with :ref:`qweb <reference/qweb>`
* various utility functions to help interacting with the outside environment.
Here is an example of a basic counter widget:
.. code-block:: javascript
var Widget = require('web.Widget');
var Counter = Widget.extend({
template: 'some.template',
events: {
'click button': '_onClick',
},
init: function (parent, value) {
this._super(parent);
this.count = value;
},
_onClick: function () {
this.count++;
this.$('.val').text(this.count);
},
});
For this example, assume that the template *some.template* (and is properly
loaded: the template is in a file, which is properly defined in the *qweb* key
in the module manifest) is given by:
.. code-block:: xml
<div t-name="some.template">
<span class="val"><t t-esc="widget.count"/></span>
<button>Increment</button>
</div>
This example widget can be used in the following manner:
.. code-block:: javascript
// Create the instance
var counter = new Counter(this, 4);
// Render and insert into DOM
counter.appendTo(".some-div");
This example illustrates a few of the features of the *Widget* class, including
the event system, the template system, the constructor with the initial *parent* argument.
Widget Lifecycle
----------------
Like many component systems, the widget class has a well defined lifecycle. The
usual lifecycle is the following: *init* is called, then *willStart*, then the
rendering takes place, then *start* and finally *destroy*.
.. function:: Widget.init(parent)
this is the constructor. The init method is supposed to initialize the
base state of the widget. It is synchronous and can be overridden to
take more parameters from the widget's creator/parent
:param parent: the new widget's parent, used to handle automatic
destruction and event propagation. Can be ``null`` for
the widget to have no parent.
:type parent: :class:`~Widget`
.. function:: Widget.willStart()
this method will be called once by the framework when a widget is created
and in the process of being appended to the DOM. The *willStart* method is a
hook that should return a deferred. The JS framework will wait for this deferred
to complete before moving on to the rendering step. Note that at this point,
the widget does not have a DOM root element. The *willStart* hook is mostly
useful to perform some asynchronous work, such as fetching data from the server
.. function:: [Rendering]
This step is automatically done by the framework. What happens is
that the framework checks if a template key is defined on the widget. If that is
the case, then it will render that template with the *widget* key bound to the
widget in the rendering context (see the example above: we use *widget.count*
in the QWeb template to read the value from the widget). If no template is
defined, we read the *tagName* key and create a corresponding DOM element.
When the rendering is done, we set the result as the $el property of the widget.
After this, we automatically bind all events in the events and custom_events
keys.
.. function:: Widget.start()
when the rendering is complete, the framework will automatically call
the *start* method. This is useful to perform some specialized post-rendering
work. For example, setting up a library.
Must return a deferred to indicate when its work is done.
:returns: deferred object
.. function:: Widget.destroy()
This is always the final step in the life of a widget. When a
widget is destroyed, we basically perform all necessary cleanup operations:
removing the widget from the component tree, unbinding all events, ...
Automatically called when the widget's parent is destroyed,
must be called explicitly if the widget has no parent or if it is
removed but its parent remains.
Note that the willStart and start method are not necessarily called. A widget
can be created (the *init* method will be called) and then destroyed (*destroy*
method) without ever having been appended to the DOM. If that is the case, the
willStart and start will not even be called.
Widget API
----------
.. attribute:: Widget.tagName
Used if the widget has no template defined. Defaults to ``div``,
will be used as the tag name to create the DOM element to set as
the widget's DOM root. It is possible to further customize this
generated DOM root with the following attributes:
.. attribute:: Widget.id
Used to generate an ``id`` attribute on the generated DOM
root. Note that this is rarely needed, and is probably not a good idea
if a widget can be used more than once.
.. attribute:: Widget.className
Used to generate a ``class`` attribute on the generated DOM root. Note
that it can actually contain more than one css class:
*'some-class other-class'*
.. attribute:: Widget.attributes
Mapping (object literal) of attribute names to attribute
values. Each of these k:v pairs will be set as a DOM attribute
on the generated DOM root.
.. attribute:: Widget.el
raw DOM element set as root to the widget (only available after the start
lifecycle method)
.. attribute:: Widget.$el
jQuery wrapper around :attr:`~Widget.el`. (only available after the start
lifecycle method)
.. attribute:: Widget.template
Should be set to the name of a :ref:`QWeb template <reference/qweb>`.
If set, the template will be rendered after the widget has been
initialized but before it has been started. The root element generated by
the template will be set as the DOM root of the widget.
.. attribute:: Widget.xmlDependencies
List of paths to xml files that need to be loaded before the
widget can be rendered. This will not induce loading anything that has already
been loaded. This is useful when you want to load your templates lazily,
or if you want to share a widget between the website and the web client
interface.
.. code-block:: javascript
var EditorMenuBar = Widget.extend({
xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],
...
.. attribute:: Widget.events
Events are a mapping of an event selector (an event name and an optional
CSS selector separated by a space) to a callback. The callback can
be the name of a widget's method or a function object. In either case, the
``this`` will be set to the widget:
.. code-block:: javascript
events: {
'click p.oe_some_class a': 'some_method',
'change input': function (e) {
e.stopPropagation();
}
},
The selector is used for jQuery's event delegation, the
callback will only be triggered for descendants of the DOM root
matching the selector. If the selector is left out
(only an event name is specified), the event will be set directly on the
widget's DOM root.
Note: the use of an inline function is discouraged, and will probably be
removed sometimes in the future.
.. attribute:: Widget.custom_events
this is almost the same as the *events* attribute, but the keys
are arbitrary strings. They represent business events triggered by
some sub widgets. When an event is triggered, it will 'bubble up' the widget
tree (see the section on component communication for more details).
.. function:: Widget.isDestroyed()
:returns: ``true`` if the widget is being or has been destroyed, ``false``
otherwise
.. function:: Widget.$(selector)
Applies the CSS selector specified as parameter to the widget's
DOM root:
.. code-block:: javascript
this.$(selector);
is functionally identical to:
.. code-block:: javascript
this.$el.find(selector);
:param String selector: CSS selector
:returns: jQuery object
.. note:: this helper method is similar to ``Backbone.View.$``
.. function:: Widget.setElement(element)
Re-sets the widget's DOM root to the provided element, also
handles re-setting the various aliases of the DOM root as well as
unsetting and re-setting delegated events.
:param Element element: a DOM element or jQuery object to set as
the widget's DOM root
Inserting a widget in the DOM
-----------------------------
.. function:: Widget.appendTo(element)
Renders the widget and inserts it as the last child of the target, uses
`.appendTo()`_
.. function:: Widget.prependTo(element)
Renders the widget and inserts it as the first child of the target, uses
`.prependTo()`_
.. function:: Widget.insertAfter(element)
Renders the widget and inserts it as the preceding sibling of the target,
uses `.insertAfter()`_
.. function:: Widget.insertBefore(element)
Renders the widget and inserts it as the following sibling of the target,
uses `.insertBefore()`_
All of these methods accept whatever the corresponding jQuery method accepts
(CSS selectors, DOM nodes or jQuery objects). They all return a deferred_
and are charged with three tasks:
* rendering the widget's root element via :func:`~Widget.renderElement`
* inserting the widget's root element in the DOM using whichever jQuery
method they match
* starting the widget, and returning the result of starting it
Widget Guidelines
----------------------
* 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. Most of the time, they can be replaced with nothing,
classes or keeping a reference to a DOM node or jQuery element.
If an ``id`` is absolutely necessary (because a third-party library requires
one), the id should be partially generated using ``_.uniqueId()`` e.g.:
.. code-block:: javascript
this.id = _.uniqueId('my-widget-');
* 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:`~Widget` has an attribute providing its DOM root
(:attr:`~Widget.$el`), and a shortcut to select nodes directly
(:func:`~Widget.$`).
* More generally, never assume your components own or controls anything beyond
its own personal :attr:`~Widget.$el` (so, avoid using a reference to the
parent widget)
* 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 :class:`~Widget`
and correctly implement and use its API and life cycle.
.. _reference/javascript_reference/qweb:
QWeb Template Engine
====================
The web client uses the :doc:`qweb` template engine to render widgets (unless they
override the *renderElement* method to do something else).
The Qweb JS template engine is based on XML, and is mostly compatible with the
python implementation.
Now, let us explain how the templates are loaded. Whenever the web client
starts, a rpc is made to the */web/webclient/qweb* route. The server will then
return a list of all templates defined in data files for each installed modules.
The correct files are listed in the *qweb* entry in each module manifest.
The web client will wait for that list of template to be loaded, before starting
its first widget.
This mechanism works quite well for our needs, but sometimes, we want to lazy
load a template. For example, imagine that we have a widget which is rarely
used. In that case, maybe we prefer to not load its template in the main file,
in order to make the web client slightly lighter. In that case, we can use the
*xmlDependencies* key of the Widget:
.. code-block:: javascript
var Widget = require('web.Widget');
var Counter = Widget.extend({
template: 'some.template',
xmlDependencies: ['/myaddon/path/to/my/file.xml'],
...
});
With this, the *Counter* widget will load the xmlDependencies files in its
*willStart* method, so the template will be ready when the rendering is performed.
Event system
============
There are currently two event systems supported by Odoo: a simple system which
allows adding listeners and triggering events, and a more complete system that
also makes events 'bubble up'.
Both of these event systems are implemented in the *EventDispatcherMixin*, in
the file *mixins.js*. This mixin is included in the *Widget* class.
Base Event system
-----------------
This event system was historically the first. It implements a simple bus
pattern. We have 4 main methods:
- *on*: this is used to register a listener on an event.
- *off*: useful to remove events listener.
- *once*: this is used to register a listener that will only be called once.
- *trigger*: trigger an event. This will cause each listeners to be called.
Here is an example on how this event system could be used:
.. code-block:: javascript
var Widget = require('web.Widget');
var Counter = require('myModule.Counter');
var MyWidget = Widget.extend({
start: function () {
this.counter = new Counter(this);
this.counter.on('valuechange', this, this._onValueChange);
var def = this.counter.appendTo(this.$el);
return $.when(def, this._super.apply(this, arguments);
},
_onValueChange: function (val) {
// do something with val
},
});
// in Counter widget, we need to call the trigger method:
... this.trigger('valuechange', someValue);
.. warning::
the use of this event system is discouraged, we plan to replace each
*trigger* method by the *trigger_up* method from the extended event system
Extended Event System
---------------------
The custom event widgets is a more advanced system, which mimic the DOM events
API. Whenever an event is triggered, it will 'bubble up' the component tree,
until it reaches the root widget, or is stopped.
- *trigger_up*: this is the method that will create a small *OdooEvent* and
dispatch it in the component tree. Note that it will start with the component
that triggered the event
- *custom_events*: this is the equivalent of the *event* dictionary, but for
odoo events.
The OdooEvent class is very simple. It has three public attributes: *target*
(the widget that triggered the event), *name* (the event name) and *data* (the
payload). It also has 2 methods: *stopPropagation* and *is_stopped*.
The previous example can be updated to use the custom event system:
.. code-block:: javascript
var Widget = require('web.Widget');
var Counter = require('myModule.Counter');
var MyWidget = Widget.extend({
custom_events: {
valuechange: '_onValueChange'
},
start: function () {
this.counter = new Counter(this);
var def = this.counter.appendTo(this.$el);
return $.when(def, this._super.apply(this, arguments);
},
_onValueChange: function(event) {
// do something with event.data.val
},
});
// in Counter widget, we need to call the trigger_up method:
... this.trigger_up('valuechange', {value: someValue});
Registries
===========
A common need in the Odoo ecosystem is to extend/change the behaviour of the
base system from the outside (by installing an application, i.e. a different
module). For example, one may need to add a new widget type in some views. In
that case, and many others, the usual process is to create the desired component,
then add it to a registry (registering step), to make the rest of the web client
aware of its existence.
There are a few registries available in the system:
field registry (exported by :js:data:`web.field_registry`)
The field registry contains all field widgets known to the web client.
Whenever a view (typically form or list/kanban) needs a field widget, this
is where it will look. A typical use case look like this:
.. code-block:: javascript
var fieldRegistry = require('web.field_registry');
var FieldPad = ...;
fieldRegistry.add('pad', FieldPad);
Note that each value should be a subclass of *AbstractField*
view registry
This registry contains all JS views known to the web client
(and in particular, the view manager). Each value of this registry should
be a subclass of *AbstractView*.
action registry
We keep track of all client actions in this registry. This
is where the action manager looks up whenever it needs to create a client
action. In version 11, each value should simply be a subclass of *Widget*.
However, in version 12, the values are required to be *AbstractAction*.
Communication between widgets
=============================
There are many ways to communicate between components.
From a parent to its child
This is a simple case. The parent widget can simply call a method on its
child:
.. code-block:: javascript
this.someWidget.update(someInfo);
From a widget to its parent/some ancestor
In this case, the widget's job is simply to notify its environment that
something happened. Since we do not want the widget to have a reference to
its parent (this would couple the widget with its parent's implementation),
the best way to proceed is usually to trigger an event, which will bubble up
the component tree, by using the ``trigger_up`` method:
.. code-block:: javascript
this.trigger_up('open_record', { record: record, id: id});
This event will be triggered on the widget, then will bubble up and be
eventually caught by some upstream widget:
.. code-block:: javascript
var SomeAncestor = Widget.extend({
custom_events: {
'open_record': '_onOpenRecord',
},
_onOpenRecord: function (event) {
var record = event.data.record;
var id = event.data.id;
// do something with the event.
},
});
Cross component
Cross component communication can be achieved by using a bus. This is not
the preferred form of communication, because it has the disadvantage of
making the code harder to maintain. However, it has the advantage of
decoupling the components. In that case, this is simply done by triggering
and listening to events on a bus. For example:
.. code-block:: javascript
// in WidgetA
var core = require('web.core');
var WidgetA = Widget.extend({
...
start: function () {
core.bus.on('barcode_scanned', this, this._onBarcodeScanned);
},
});
// in WidgetB
var WidgetB = Widget.extend({
...
someFunction: function (barcode) {
core.bus.trigger('barcode_scanned', barcode);
},
});
In this example, we use the bus exported by *web.core*, but this is not
required. A bus could be created for a specific purpose.
Services
========
In version 11.0, we introduced the notion of *service*. The main idea is to
give to sub components a controlled way to access their environment, in a way
that allow the framework enough control, and which is testable.
The service system is organized around three ideas: services, service providers
and widgets. The way it works is that widgets trigger (with *trigger_up*)
events, these events bubble up to a service provider, which will ask a service
to perform a task, then maybe return an answer.
Service
--------
A service is an instance of the *AbstractService* class. It basically only has
a name and a few methods. Its job is to perform some work, typically something
depending on the environment.
For example, we have the *ajax* service (job is to perform a rpc), the
*localStorage* (interact with the browser local storage) and many others.
Here is a simplified example on how the ajax service is implemented:
.. code-block:: javascript
var AbstractService = require('web.AbstractService');
var AjaxService = AbstractService.extend({
name: 'ajax',
rpc: function (...) {
return ...;
},
});
This service is named 'ajax' and define one method, *rpc*.
Service Provider
----------------
For services to work, it is necessary that we have a service provider ready to
dispatch the custom events. In the *backend* (web client), this is done by the
main web client instance. Note that the code for the service provider comes from
the *ServiceProviderMixin*.
Widget
------
The widget is the part that requests a service. In order to do that, it simply
triggers an event *call_service* (typically by using the helper function *call*).
This event will bubble up and communicate the intent to the rest of the system.
In practice, some functions are so frequently called that we have some helpers
functions to make them easier to use. For example, the *_rpc* method is a helper
that helps making a rpc.
.. code-block:: javascript
var SomeWidget = Widget.extend({
_getActivityModelViewID: function (model) {
return this._rpc({
model: model,
method: 'get_activity_view_id'
});
},
});
.. warning::
If a widget is destroyed, it will be detached from the main component tree
and will not have a parent. In that case, the events will not bubble up, which
means that the work will not be done. This is usually exactly what we want from
a destroyed widget.
RPCs
----
The rpc functionality is supplied by the ajax service. But most people will
probably only interact with the *_rpc* helpers.
There are typically two usecases when working on Odoo: one may need to call a
method on a (python) model (this goes through a controller *call_kw*), or one
may need to directly call a controller (available on some route).
* Calling a method on a python model:
.. code-block:: javascript
return this._rpc({
model: 'some.model',
method: 'some_method',
args: [some, args],
});
* Directly calling a controller:
.. code-block:: javascript
return this._rpc({
route: '/some/route/',
params: { some: kwargs},
});
Notifications
==============
The Odoo framework has a standard way to communicate various information to the
user: notifications, which are displayed on the top right of the user interface.
There are two types of notifications:
- *notification*: useful to display some feedback. For example, whenever a user
unsubscribed to a channel.
- *warning*: useful to display some important/urgent information. Typically
most kind of (recoverable) errors in the system.
Also, notifications can be used to ask a question to the user without disturbing
its workflow. Imagine a phone call received through VOIP: a sticky notification
could be displayed with two buttons *Accept* and *Decline*.
Notification system
-------------------
The notification system in Odoo is designed with the following components:
- a *Notification* widget: this is a simple widget that is meant to be created
and displayed with the desired information
- a *NotificationService*: a service whose responsibility is to create and
destroy notifications whenever a request is done (with a custom_event). Note
that the web client is a service provider.
- two helper functions in *ServiceMixin*: *do_notify* and *do_warn*
Displaying a notification
-------------------------
The most common way to display a notification is by using two methods that come
from the *ServiceMixin*:
- *do_notify(title, message, sticky, className)*:
Display a notification of type *notification*.
- *title*: string. This will be displayed on the top as a title
- *message*: string, the content of the notification
- *sticky*: boolean, optional. If true, the notification will stay until the
user dismisses it. Otherwise, the notification will be automatically
closed after a short delay.
- *className*: string, optional. This is a css class name that will be
automatically added to the notification. This could be useful for styling
purpose, even though its use is discouraged.
- *do_warn(title, message, sticky, className)*:
Display a notification of type *warning*.
- *title*: string. This will be displayed on the top as a title
- *message*: string, the content of the notification
- *sticky*: boolean, optional. If true, the notification will stay until the
user dismisses it. Otherwise, the notification will be automatically
closed after a short delay.
- *className*: string, optional. This is a css class name that will be
automatically added to the notification. This could be useful for styling
purpose, even though its use is discouraged.
Here are two examples on how to use these methods:
.. code-block:: javascript
// note that we call _t on the text to make sure it is properly translated.
this.do_notify(_t("Success"), _t("Your signature request has been sent."));
this.do_warn(_t("Error"), _t("Filter name is required."));
Systray
=======
The Systray is the right part of the menu bar in the interface, where the web
client displays a few widgets, such as a messaging menu.
When the SystrayMenu is created by the menu, it will look for all registered
widgets and add them as a sub widget at the proper place.
There is currently no specific API for systray widgets. They are supposed to
be simple widgets, and can communicate with their environment just like other
widgets with the *trigger_up* method.
Adding a new Systray Item
-------------------------
There is no systray registry. The proper way to add a widget is to add it to
the class variable SystrayMenu.items.
.. code-block:: javascript
var SystrayMenu = require('web.SystrayMenu');
var MySystrayWidget = Widget.extend({
...
});
SystrayMenu.Items.push(MySystrayWidget);
Ordering
--------
Before adding the widget to himself, the Systray Menu will sort the items by
a sequence property. If that property is not present on the prototype, it will
use 50 instead. So, to position a systray item to be on the right, one can
set a very high sequence number (and conversely, a low number to put it on the
left).
.. code-block:: javascript
MySystrayWidget.prototype.sequence = 100;
Translation management
======================
Some translations are made on the server side (basically all text strings rendered or
processed by the server), but there are strings in the static files that need
to be translated. The way it currently works is the following:
- each translatable string is tagged with the special function *_t* (available in
the JS module *web.core*
- these strings are used by the server to generate the proper PO files
- whenever the web client is loaded, it will call the route */web/webclient/translations*,
which returns a list of all translatable terms
- in runtime, whenever the function *_t* is called, it will look up in this list
in order to find a translation, and return it or the original string if none
is found.
Note that translations are explained in more details, from the server point of
view, in the document :doc:`translations`.
There are two important functions for the translations in javascript: *_t* and
*_lt*. The difference is that *_lt* is lazily evaluated.
.. code-block:: javascript
var core = require('web.core');
var _t = core._t;
var _lt = core._lt;
var SomeWidget = Widget.extend({
exampleString: _lt('this should be translated'),
...
someMethod: function () {
var str = _t('some text');
...
},
});
In this example, the *_lt* is necessary because the translations are not ready
when the module is loaded.
Note that translation functions need some care. The string given in argument
should not be dynamic.
Session
=======
There is a specific module provided by the web client which contains some
information specific to the user current *session*. Some notable keys are
- uid: the current user ID (its ID as a *res.users*)
- user_name: the user name, as a string
- the user context (user ID, language and timezone)
- partner_id: the ID of the partner associated to the current user
- db: the name of the database currently being in use
Adding information to the session
---------------------------------
When the /web route is loaded, the server will inject some session information
in the template a script tag. The information will be read from the method
*session_info* of the model *ir.http*. So, if one wants to add a specific
information, it can be done by overriding the session_info method and adding it
to the dictionary.
.. code-block:: python
from odoo import models
from odoo.http import request
class IrHttp(models.AbstractModel):
_inherit = 'ir.http'
def session_info(self):
result = super(IrHttp, self).session_info()
result['some_key'] = get_some_value_from_db()
return result
Now, the value can be obtained in javascript by reading it in the session:
.. code-block:: javascript
var session = require('web.session');
var myValue = session.some_key;
...
Note that this mechanism is designed to reduce the amount of communication
needed by the web client to be ready. It is more appropriate for data which is
cheap to compute (a slow session_info call will delay the loading for the web
client for everyone), and for data which is required early in the initialization
process.
Views
======
The word 'view' has more than one meaning. This section is about the design of
the javascript code of the views, not the structure of the *arch* or anything
else.
In 2017, Odoo replaced the previous view code with a new architecture. The
main need was to separate the rendering logic from the model logic.
Views (in a generic sense) are now described with 4 pieces: a View, a
Controller, a Renderer and a Model. The API of these 4 pieces is described in
the AbstractView, AbstractController, AbstractRenderer and AbstractModel classes.
.. raw:: html
<svg width="550" height="173">
<!-- Created with Method Draw - https://github.com/duopixel/Method-Draw/ -->
<path id="svg_1" d="m147.42498,79.79206c0.09944,-8.18859 -0.06363,-16.38812 0.81774,-24.5623c21.65679,2.68895 43.05815,7.08874 64.35,11.04543c1.14304,-4.01519 0.60504,-7.34585 1.59817,-11.05817c13.67878,7.81176 27.23421,15.73476 40.23409,24.03505c-12.47212,9.41539 -26.77809,17.592 -40.82272,25.96494c-0.4548,-3.89916 -0.90967,-7.79828 -1.36448,-11.69744c-20.69972,3.77225 -42.59036,7.6724 -63.42391,11.12096c-1.41678,-7.95741 -1.37514,-16.62327 -1.38888,-24.84846z" stroke-width="1.5" stroke="#000" fill="#fff"/>
<rect id="svg_3" height="41" width="110" y="57.5" x="7" fill-opacity="null" stroke-opacity="null" stroke-width="1.5" stroke="#000" fill="#fff"/>
<rect stroke="#000" id="svg_5" height="41" width="135" y="20.5" x="328" fill-opacity="null" stroke-opacity="null" stroke-width="1.5" fill="#fff"/>
<rect stroke="#000" id="svg_6" height="41" width="128" y="102.5" x="262" fill-opacity="null" stroke-opacity="null" stroke-width="1.5" fill="#fff"/>
<rect stroke="#000" id="svg_7" height="41" width="119" y="100.5" x="417" fill-opacity="null" stroke-opacity="null" stroke-width="1.5" fill="#fff"/>
<line stroke-linecap="null" stroke-linejoin="null" id="svg_8" y2="96.5" x2="317" y1="65.5" x1="364" fill-opacity="null" stroke-opacity="null" stroke-width="1.5" stroke="#000" fill="none"/>
<line stroke-linecap="null" stroke-linejoin="null" id="svg_9" y2="96.5" x2="467" y1="63.5" x1="425" fill-opacity="null" stroke-opacity="null" stroke-width="1.5" stroke="#000" fill="none"/>
<text xml:space="preserve" text-anchor="start" font-family="Helvetica, Arial, sans-serif" font-size="24" id="svg_10" y="83.5" x="38" fill-opacity="null" stroke-opacity="null" stroke-width="0" stroke="#000" fill="#000000">View</text>
<text xml:space="preserve" text-anchor="start" font-family="Helvetica, Arial, sans-serif" font-size="24" id="svg_11" y="44.5" x="346" fill-opacity="null" stroke-opacity="null" stroke-width="0" stroke="#000" fill="#000000">Controller</text>
<text xml:space="preserve" text-anchor="start" font-family="Helvetica, Arial, sans-serif" font-size="24" id="svg_12" y="128.5" x="276" fill-opacity="null" stroke-opacity="null" stroke-width="0" stroke="#000" fill="#000000">Renderer</text>
<text xml:space="preserve" text-anchor="start" font-family="Helvetica, Arial, sans-serif" font-size="24" id="svg_13" y="127.5" x="442" fill-opacity="null" stroke-opacity="null" stroke-width="0" stroke="#000" fill="#000000">Model</text>
</svg>
- the View is the factory. Its job is to get a set of fields, arch, context and
some other parameters, then to construct a Controller/Renderer/Model triplet.
The view's role is to properly setup each piece of the MVC pattern, with the correct
information. Usually, it has to process the arch string and extract the
data necessary for each other parts of the view.
Note that the view is a class, not a widget. Once its job has been done, it
can be discarded.
- the Renderer has one job: representing the data being viewed in a DOM element.
Each view can render the data in a different way. Also, it should listen on
appropriate user actions and notify its parent (the Controller) if necessary.
The Renderer is the V in the MVC pattern.
- the Model: its job is to fetch and hold the state of the view. Usually, it
represents in some way a set of records in the database. The Model is the
owner of the 'business data'. It is the M in the MVC pattern.
- the Controller: its job is to coordinate the renderer and the model. Also, it
is the main entry point for the rest of the web client. For example, when
the user changes something in the search view, the *update* method of the
controller will be called with the appropriate information.
It is the C in the MVC pattern.
.. note::
The JS code for the views has been designed to be usable outside of the
context of a view manager/action manager. They could be used in a client action,
or, they could be displayed in the public website (with some work on the assets).
.. _reference/js/widgets:
Field Widgets
=============
A good part of the web client experience is about editing and creating data. Most
of that work is done with the help of field widgets, which are aware of the field
type and of the specific details on how a value should be displayed and edited.
AbstractField
-------------
The *AbstractField* class is the base class for all widgets in a view, for all
views that support them (currently: Form, List, Kanban).
There are many differences between the v11 field widgets and the previous versions.
Let us mention the most important ones:
- the widgets are shared between all views (well, Form/List/Kanban). No need to
duplicate the implementation anymore. Note that it is possible to have a
specialized version of a widget for a view, by prefixing it with the view name
in the view registry: *list.many2one* will be chosen in priority over *many2one*.
- the widgets are no longer the owner of the field value. They only represent
the data and communicate with the rest of the view.
- the widgets do no longer need to be able to switch between edit and readonly
mode. Now, when such a change is necessary, the widget will be destroyed and
rerendered again. It is not a problem, since they do not own their value
anyway
- the field widgets can be used outside of a view. Their API is slightly
awkward, but they are designed to be standalone.
Decorations
-----------
Like the list view, field widgets have a simple support for decorations. The
goal of decorations is to have a simple way to specify a text color depending on
the record current state. For example,
.. code-block:: xml
<field name="state" decoration-danger="amount &lt; 10000"/>
The valid decoration names are:
- decoration-bf
- decoration-it
- decoration-danger
- decoration-info
- decoration-muted
- decoration-primary
- decoration-success
- decoration-warning
Each decoration *decoration-X* will be mapped to a css class *text-X*, which is
a standard bootstrap css class (except for *text-it* and *text-bf*, which are
handled by odoo and correspond to italic and bold, respectively). Note that the
value of the decoration attribute should be a valid python expression, which
will be evaluated with the record as evaluation context.
Non relational fields
---------------------
We document here all non relational fields available by default, in no particular
order.
integer (FieldInteger)
This is the default field type for fields of type *integer*.
- Supported field types: *integer*
Options:
- type: setting the input type (*text* by default, can be set on *number*)
On edit mode, the field is rendered as an input with the HTML attribute type
set on *number* (so user can benefit the native support, especially on
mobile). In this case, the default formatting is disabled to avoid incompability.
.. code-block:: xml
<field name="int_value" options='{"type": "number"}'/>
- step: set the step to the value up and down when the user click on buttons
(only for input of type number, 1 by default)
.. code-block:: xml
<field name="int_value" options='{"type": "number", "step": 100}'/>
float (FieldFloat)
This is the default field type for fields of type *float*.
- Supported field types: *float*
Attributes:
- digits: displayed precision
.. code-block:: xml
<field name="factor" digits="[42,5]"/>
Options:
- type: setting the input type (*text* by default, can be set on *number*)
On edit mode, the field is rendered as an input with the HTML attribute type
set on *number* (so user can benefit the native support, especially on
mobile). In this case, the default formatting is disabled to avoid incompability.
.. code-block:: xml
<field name="int_value" options='{"type": "number"}'/>
- step: set the step to the value up and down when the user click on buttons
(only for input of type number, 1 by default)
.. code-block:: xml
<field name="int_value" options='{"type": "number", "step": 0.1}'/>
float_time (FieldFloatTime)
The goal of this widget is to display properly a float value that represents
a time interval (in hours). So, for example, 0.5 should be formatted as 0:30,
or 4.75 correspond to 4:45.
- Supported field types: *float*
float_factor (FieldFloatFactor)
This widget aims to display properly a float value that converted using a factor
given in its options. So, for example, the value saved in database is 0.5 and the
factor is 3, the widget value should be formatted as 1.5.
- Supported field types: *float*
float_toggle (FieldFloatToggle)
The goal of this widget is to replace the input field by a button containing a
range of possible values (given in the options). Each click allows the user to loop
in the range. The purpose here is to restrict the field value to a predefined selection.
Also, the widget support the factor conversion as the *float_factor* widget (Range values
should be the result of the conversion).
- Supported field types: *float*
.. code-block:: xml
<field name="days_to_close" widget="float_toggle" options='{"factor": 2, "range": [0, 4, 8]}'/>
boolean (FieldBoolean)
This is the default field type for fields of type *boolean*.
- Supported field types: *boolean*
char (FieldChar)
This is the default field type for fields of type *char*.
- Supported field types: *char*
date (FieldDate)
This is the default field type for fields of type *date*. Note that it also
works with datetime fields. It uses the session timezone when formatting
dates.
- Supported field types: *date*, *datetime*
Options:
- datepicker: extra settings for the datepicker_ widget.
.. code-block:: xml
<field name="datefield" options='{"datepicker": {"daysOfWeekDisabled": [0, 6]}}'/>
datetime (FieldDateTime)
This is the default field type for fields of type *datetime*.
- Supported field types: *date*, *datetime*
Options:
- datepicker: extra settings for the datepicker_ widget.
.. code-block:: xml
<field name="datetimefield" options='{"datepicker": {"daysOfWeekDisabled": [0, 6]}}'/>
monetary (FieldMonetary)
This is the default field type for fields of type 'monetary'. It is used to
display a currency. If there is a currency fields given in option, it will
use that, otherwise it will fall back to the default currency (in the session)
- Supported field types: *monetary*, *float*
Options:
- currency_field: another field name which should be a many2one on currency.
.. code-block:: xml
<field name="value" widget="monetary" options="{'currency_field': 'currency_id'}"/>
text (FieldText)
This is the default field type for fields of type *text*.
- Supported field types: *text*
handle (HandleWidget)
This field's job is to be displayed as a *handle* in a list view, and allows
reordering the various records by drag and dropping lines.
.. warning:: Having more than one field with a handle widget on the same list is not supported.
- Supported field types: *integer*
email (FieldEmail)
This field displays email address. The main reason to use it is that it
is rendered as an anchor tag with the proper href, in readonly mode.
- Supported field types: *char*
phone (FieldPhone)
This field displays a phone number. The main reason to use it is that it
is rendered as an anchor tag with the proper href, in readonly mode, but
only in some cases: we only want to make it clickable if the device can
call this particular number.
- Supported field types: *char*
url (UrlWidget)
This field displays an url (in readonly mode). The main reason to use it is
that it is rendered as an anchor tag with the proper css classes and href.
- Supported field types: *char*
Also, the text of the anchor tag can be customized with the *text* attribute
(it won't change the href value).
.. code-block:: xml
<field name="foo" widget="url" text="Some URL"/>
domain (FieldDomain)
The "Domain" field allows the user to construct a technical-prefix domain
thanks to a tree-like interface and see the selected records in real time.
In debug mode, an input is also there to be able to enter the prefix char
domain directly (or to build advanced domains the tree-like interface does
not allow to).
Note that this is limited to 'static' domain (no dynamic expression, or access
to context variable).
- Supported field types: *char*
link_button (LinkButton)
The LinkButton widget actually simply displays a span with an icon and the
text value as content. The link is clickable and will open a new browser
window with its value as url.
- Supported field types: *char*
image (FieldBinaryImage)
This widget is used to represent a binary value as an image. In some cases,
the server returns a 'bin_size' instead of the real image (a bin_size is a
string representing a file size, such as 6.5kb). In that case, the widget
will make an image with a source attribute corresponding to an image on the
server.
- Supported field types: *binary*
Options:
- preview_image: if the image is only loaded as a 'bin_size', then this
option is useful to inform the web client that the default field name is
not the name of the current field, but the name of another field.
.. code-block:: xml
<field name="image" widget='image' options='{"preview_image":"image_medium"}'/>
binary (FieldBinaryFile)
Generic widget to allow saving/downloading a binary file.
- Supported field types: *binary*
Attribute:
- filename: saving a binary file will lose its file name, since it only
saves the binary value. The filename can be saved in another field. To do
that, an attribute filename should be set to a field present in the view.
.. code-block:: xml
<field name="datas" filename="datas_fname"/>
priority (PriorityWidget)
This widget is rendered as a set of stars, allowing the user to click on it
to select a value or not. This is useful for example to mark a task as high
priority.
Note that this widget also works in 'readonly' mode, which is unusual.
- Supported field types: *selection*
attachment_image (AttachmentImage)
Image widget for many2one fields. If the field is set, this widget will be
rendered as an image with the proper src url. This widget does not have a
different behaviour in edit or readonly mode, it is only useful to view an
image.
- Supported field types: *many2one*
.. code-block:: xml
<field name="displayed_image_id" widget="attachment_image"/>
image_selection (ImageSelection)
Allow the user to select a value by clicking on an image.
- Supported field types: *selection*
Options: a dictionary with a mapping from a selection value to an object with
the url for an image (*image_link*) and a preview image (*preview_link*).
Note that this option is not optional!
.. code-block:: xml
<field name="external_report_layout" widget="image_selection" options="{
'background': {
'image_link': '/base/static/img/preview_background.png',
'preview_link': '/base/static/pdf/preview_background.pdf'
},
'standard': {
'image_link': '/base/static/img/preview_standard.png',
'preview_link': '/base/static/pdf/preview_standard.pdf'
}
}"/>
label_selection (LabelSelection)
This widget renders a simple non-editable label. This is only useful to
display some information, not to edit it.
- Supported field types: *selection*
Options:
- classes: a mapping from a selection value to a css class
.. code-block:: xml
<field name="state" widget="label_selection" options="{
'classes': {'draft': 'default', 'cancel': 'default', 'none': 'danger'}
}"/>
state_selection (StateSelectionWidget)
This is a specialized selection widget. It assumes that the record has some
hardcoded fields, present in the view: *stage_id*, *legend_normal*,
*legend_blocked*, *legend_done*. This is mostly used to display and change
the state of a task in a project, with additional information displayed in
the dropdown.
- Supported field types: *selection*
.. code-block:: xml
<field name="kanban_state" widget="state_selection"/>
kanban_state_selection (StateSelectionWidget)
This is exactly the same widget as state_selection
- Supported field types: *selection*
boolean_favorite (FavoriteWidget)
This widget is displayed as an empty (or not) star, depending on a boolean
value. Note that it also can be edited in readonly mode.
- Supported field types: *boolean*
boolean_button (FieldBooleanButton)
The Boolean Button widget is meant to be used in a stat button in a form view.
The goal is to display a nice button with the current state of a boolean
field (for example, 'Active'), and allow the user to change that field when
clicking on it.
Note that it also can be edited in readonly mode.
- Supported field types: *boolean*
Options:
- terminology: it can be either 'active', 'archive', 'close' or a customized
mapping with the keys *string_true*, *string_false*, *hover_true*, *hover_false*
.. code-block:: xml
<field name="active" widget="boolean_button" options='{"terminology": "archive"}'/>
boolean_toggle (BooleanToggle)
Displays a toggle switch to represent a boolean. This is a subfield of
FieldBoolean, mostly used to have a different look.
statinfo (StatInfo)
This widget is meant to represent statistical information in a *stat button*.
It is basically just a label with a number.
- Supported field types: *integer, float*
Options:
- label_field: if given, the widget will use the value of the label_field as
text.
.. code-block:: xml
<button name="%(act_payslip_lines)d"
icon="fa-money"
type="action">
<field name="payslip_count" widget="statinfo"
string="Payslip"
options="{'label_field': 'label_tasks'}"/>
</button>
percentpie (FieldPercentPie)
This widget is meant to represent statistical information in a *stat button*.
This is similar to a statinfo widget, but the information is represented in
a *pie* chart (empty to full). Note that the value is interpreted as a
percentage (a number between 0 and 100).
- Supported field types: *integer, float*
.. code-block:: xml
<field name="replied_ratio" string="Replied" widget="percentpie"/>
progressbar (FieldProgressBar)
Represent a value as a progress bar (from 0 to some value)
- Supported field types: *integer, float*
Options:
- editable: boolean if value is editable
- current_value: get the current_value from the field that must be present in the view
- max_value: get the max_value from the field that must be present in the view
- edit_max_value: boolean if the max_value is editable
- title: title of the bar, displayed on top of the bar --> not translated,
use parameter (not option) "title" instead
.. code-block:: xml
<field name="absence_of_today" widget="progressbar"
options="{'current_value': 'absence_of_today', 'max_value': 'total_employee', 'editable': false}"/>
toggle_button (FieldToggleBoolean)
This widget is intended to be used on boolean fields. It toggles a button
switching between a green bullet / gray bullet. It also set up a tooltip,
depending on the value and some options.
- Supported field types: *boolean*
Options:
- active: the string for the tooltip that should be set when boolean is true
- inactive: the tooltip that should be set when boolean is false
.. code-block:: xml
<field name="payslip_status" widget="toggle_button"
options='{"active": "Reported in last payslips", "inactive": "To Report in Payslip"}'
/>
dashboard_graph (JournalDashboardGraph)
This is a more specialized widget, useful to display a graph representing a
set of data. For example, it is used in the accounting dashboard kanban view.
It assumes that the field is a JSON serialization of a set of data.
- Supported field types: *char*
Attribute
- graph_type: string, can be either 'line' or 'bar'
.. code-block:: xml
<field name="dashboard_graph_data"
widget="dashboard_graph"
graph_type="line"/>
ace (AceEditor)
This widget is intended to be used on Text fields. It provides Ace Editor
for editing XML and Python.
- Supported field types: *char, text*
Relational fields
-----------------
.. class:: web.relational_fields.FieldSelection
Supported field types: *selection*
.. attribute:: placeholder
a string which is used to display some info when no value is selected
.. code-block:: xml
<field name="tax_id" widget="selection" placeholder="Select a tax"/>
radio (FieldRadio)
This is a subfield of FielSelection, but specialized to display all the
valid choices as radio buttons.
Note that if used on a many2one records, then more rpcs will be done to fetch
the name_gets of the related records.
- Supported field types: *selection, many2one*
Options:
- horizontal: if true, radio buttons will be displayed horizontally.
.. code-block:: xml
<field name="recommended_activity_type_id" widget="radio"
options="{'horizontal':true}"/>
selection_badge (FieldSelectionBadge)
This is a subfield of FieldSelection, but specialized to display all the
valid choices as rectangular badges.
- Supported field types: *selection, many2one*
.. code-block:: xml
<field name="recommended_activity_type_id" widget="selection_badge"/>
many2one (FieldMany2One)
Default widget for many2one fields.
- Supported field types: *many2one*
Attributes:
- can_create: allow the creation of related records (take precedence over no_create
option)
- can_write: allow the edition of related records (default: true)
Options:
- no_create: prevent the creation of related records
- quick_create: allow the quick creation of related records (default: true)
- no_quick_create: prevent the quick creation of related records (don't ask me)
- no_create_edit: same as no_create, maybe...
- create_name_field: when creating a related record, if this option is set, the value of the *create_name_field* will be filled with the value of the input (default: *name*)
- always_reload: boolean, default to false. If true, the widget will always
do an additional name_get to fetch its name value. This is used for the
situations where the name_get method is overridden (please do not do that)
- no_open: boolean, default to false. If set to true, the many2one will not
redirect on the record when clicking on it (in readonly mode)
.. code-block:: xml
<field name="currency_id" options="{'no_create': True, 'no_open': True}"/>
list.many2one (ListFieldMany2One)
Default widget for many2one fields (in list view).
Specialization of many2one field for list views. The main reason is that we
need to render many2one fields (in readonly mode) as a text, which does not
allow opening the related records.
- Supported field types: *many2one*
kanban.many2one (KanbanFieldMany2One)
Default widget for many2one fields (in kanban view). We need to disable all
edition in kanban views.
- Supported field types: *many2one*
many2many (FieldMany2Many)
Default widget for many2many fields.
- Supported field types: *many2many*
Attributes:
- mode: string, default view to display
- domain: restrict the data to a specific domain
Options:
- create_text: allow the customization of the text displayed when adding a
new record
many2many_binary (FieldMany2ManyBinaryMultiFiles)
This widget helps the user to upload or delete one or more files at the same
time.
Note that this widget is specific to the model 'ir.attachment'.
- Supported field types: *many2many*
many2many_tags (FieldMany2ManyTags)
Display many2many as a list of tags.
- Supported field types: *many2many*
Options:
- color_field: the name of a numeric field, which should be present in the
view. A color will be chosen depending on its value.
.. code-block:: xml
<field name="category_id" widget="many2many_tags" options="{'color_field': 'color'}"/>
form.many2many_tags (FormFieldMany2ManyTags)
Specialization of many2many_tags widget for form views. It has some extra
code to allow editing the color of a tag.
- Supported field types: *many2many*
kanban.many2many_tags (KanbanFieldMany2ManyTags)
Specialization of many2many_tags widget for kanban views.
- Supported field types: *many2many*
many2many_checkboxes (FieldMany2ManyCheckBoxes)
This field displays a list of checkboxes and allow the user to select a
subset of the choices.
- Supported field types: *many2many*
one2many (FieldOne2Many)
Default widget for one2many fields.
It usually displays data in a sub list view, or a sub kanban view.
- Supported field types: *one2many*
Options:
- create_text: a string that is used to customize the 'Add' label/text.
.. code-block:: xml
<field name="turtles" options="{\'create_text\': \'Add turtle\'}">
statusbar (FieldStatus)
This is a really specialized widget for the form views. It is the bar on top
of many forms which represent a flow, and allow selecting a specific state.
- Supported field types: *selection, many2one*
reference (FieldReference)
The FieldReference is a combination of a select (for the model) and a
FieldMany2One (for its value). It allows the selection of a record on an
arbitrary model.
- Supported field types: *char, reference*
one2many_list (FieldOne2Many)
This widget is exactly the same as a FieldOne2Many. It is registered at this
key only for backward compatibility reasons. Please avoid using this.
Client actions
==============
The idea of a client action is a customized widget that is integrated in the
web client interface, just like a *act_window_action*. This is useful when
you need a component that is not closely linked to an existing view or a
specific model. For example, the Discuss application is actually a client
action.
A client action is a term that has various meanings, depending on the context:
- from the perspective of the server, it is a record of the model *ir_action*,
with a field *tag* of type char
- from the perspective of the web client, it is a widget, which inherit from
the class AbstractAction, and is supposed to be registered in the
action registry under the corresponding key (from the field char)
Whenever a menu item is associated to a client action, opening it will simply
fetch the action definition from the server, then lookup into its action
registry to get the Widget definition at the appropriate key, and finally, it
will instantiate and append the widget to the proper place in the DOM.
Adding a client action
----------------------
A client action is a widget which will control the part of the screen below the
menu bar. It can have a control panel, if necessary. Defining a client action
can be done in two steps: implementing a new widget, and registering the widget
in the action registry.
Implementing a new client action.
This is done by creating a widget:
.. code-block:: javascript
var ControlPanelMixin = require('web.ControlPanelMixin');
var AbstractAction = require('web.AbstractAction');
var ClientAction = AbstractAction.extend(ControlPanelMixin, {
...
});
Do not add the controlpanel mixin if you do not need it. Note that some
code is needed to interact with the control panel (via the
``update_control_panel`` method given by the mixin).
Registering the client action.
As usual, we need to make the web client aware
of the mapping between client actions and the actual class:
.. code-block:: javascript
var core = require('web.core');
core.action_registry.add('my-custom-action', ClientAction);
Then, to use the client action in the web client, we need to create a client
action record (a record of the model ``ir.actions.client``) with the proper
``tag`` attribute:
.. code-block:: xml
<record id="my_client_action" model="ir.actions.client">
<field name="name">Some Name</field>
<field name="tag">my-custom-action</field>
</record>
Using the control panel mixin
-----------------------------
By default, the AbstractAction class does not include the control panel mixin.
This means that a client action does not display a control panel. In order to
do that, several steps should be done.
- add ControlPanelMixin in the widget:
.. code-block:: javascript
var ControlPanelMixin = require('web.ControlPanelMixin');
var MyClientAction = AbstractAction.extend(ControlPanelMixin, {
...
});
- call the method *update_control_panel* whenever we need to update the control
panel. For example:
.. code-block:: javascript
var SomeClientAction = Widget.extend(ControlPanelMixin, {
...
start: function () {
this._renderButtons();
this._updateControlPanel();
...
},
do_show: function () {
...
this._updateControlPanel();
},
_renderButtons: function () {
this.$buttons = $(QWeb.render('SomeTemplate.Buttons'));
this.$buttons.on('click', ...);
},
_updateControlPanel: function () {
this.update_control_panel({
cp_content: {
$buttons: this.$buttons,
},
});
For more information, look into the *control_panel.js* file.
.. _.appendTo():
https://api.jquery.com/appendTo/
.. _.prependTo():
https://api.jquery.com/prependTo/
.. _.insertAfter():
https://api.jquery.com/insertAfter/
.. _.insertBefore():
https://api.jquery.com/insertBefore/
.. _event delegation:
https://api.jquery.com/delegate/
.. _datepicker: https://github.com/Eonasdan/bootstrap-datetimepicker
.. _deferred: https://api.jquery.com/category/deferred-object/