diff --git a/content/developer/reference/frontend/javascript_reference.rst b/content/developer/reference/frontend/javascript_reference.rst index 00e70696d5..107fb44afe 100644 --- a/content/developer/reference/frontend/javascript_reference.rst +++ b/content/developer/reference/frontend/javascript_reference.rst @@ -29,8 +29,8 @@ The Javascript framework is designed to work with three main use cases: 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. +(see below in the assets section). This document will focus mostly on the architecture +of the web client. Web client ========== @@ -38,41 +38,40 @@ 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. +The web client is a single-page application: instead of +requesting a full page from the server each time the user performs an action, +it only loads what is needed to update the user interface (UI) as a result of that +action. While doing this, it also takes care of updating information in the URL, +so that, in most cases, refreshing the page or closing the browser and opening it +again shows you the same thing. 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 +Here, we give a very quick overview of the web client code, in the :file:`web` addon. +The paths will be described relative to :file:`web/static/src`. +The following description is deliberately not exhaustive; the goal is only to +give the reader a bird's eye view of the architecture. + +- :file:`module_loader.js`: this is the file that defines the Odoo javascript module + system. It needs to be loaded before any other JS module. +- :file:`core/`: this folder contains code that forms the lowest level of the javascript + framework and that can be used in the web client as well as the website, portal, + and point of sale application. +- :file:`weblient/`: this folder contains files that are specific to the web client and + cannot be used in the website or point of sale, such as the action manager and the + action service. +- :file:`webclient/webclient.js`: this is the webclient component proper. It is mostly + a wrapper for the action container and the navbar, and does a few things that + are required upon starting the application, such as loading the state of the url. +- :file:`webclient/actions/`: this folder contains the code responsible for displaying + and switching between actions. +- :file:`views/`: this folder contains the code for the view infrastructure, as well + as most of the views (some types of views are added by other addons). +- :file:`views/fields/`: contains the definition of the various field components, as well + as some utilities used by multiple fields. +- :file:`search/` 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 What to do if a file is not loaded/updated @@ -81,925 +80,289 @@ 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 -- when in any debug mode, there is an option in the debug manager menu (bug icon) - 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... +- Make sure you saved your file; forgetting to do that happens to the best of us. +- Take a look at the console (in the dev tools, usually opened with F12) and check + for errors. +- Try adding a `console.log()` at the beginning of your file so you can see if + a file has been loaded or not. If it is not loaded, if may not be in the proper + assets bundle, or the asset bundle may not be up to date. +- Depending on your settings, the server may not regenerate the assets bundles + after a file has been modified; there are a few options to solve this: -.. 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). + - restarting the server will force it to check if the asset bundle is up to + date the next time it is requested + - in debug mode, there is an option in the debug menu (:icon:`fa-bug` button in the navbar) + to force the server to regenerate the assets bundle on the fly without restarting. + - starting the server with the `--dev=xml` option will force the server to check + if an asset bundle is up to date every time it is requested. We advise you to use + this option when actively developing, but not in production. + +- Make sure you refresh your page after changing the code. Odoo currently does not + have any hot module reloading mechanism. Loading Javascript Code ======================= Large applications are usually broken up into smaller files, that need to be -connected together. Some file may need to use some part of code defined in +connected together. Some file may need to use code defined in another file. There are two ways of sharing code between files: -- use the global scope (the *window* object) to write/read references to some +- using the global scope (the *window* object) to read/write references to some objects or functions, -- use a module system that will provide a way for each modules to export or import +- using a module system that will provide a way for each modules to export or import values, and will make sure that they are loaded in a proper order. While it's possible to work in the global scope, this has a number of issues: -It is difficult to ensure that implementation details are not exposed by work done in the global scope directly. - -- Dependencies are implicit, leading to fragile and unreliable load ordering. - -- The lack of insight into execution means it's impossible to use various optimisations (e.g. deferred and asynchronous - loading). - -- Module systems help resolve these issues: because modules specify their dependencies the module system can ensure the - necessary order of loading is respected, and because modules can precisely specify their exports it is less likely - that they will leak implementation details. - -For most Odoo code, we want to use a module system. Because of the way assets -work in Odoo (and in particular, the fact that each installed odoo addon can -modify the list of files contained in a bundle), Odoo has to resolve modules -browser side. To do that, Odoo provides a small module system described just -below (see :ref:`frontend/modules/odoo_module`). - -However, Odoo also provides support for native javascript modules (see -:ref:`frontend/modules/native_js`). These modules -will simply be translated by the server into odoo modules. It is encouraged to -write all javascript code as a native module, for a better IDE integration. In -the future, the Odoo module system should be considered an implementation detail, -not the primary way to write javascript code. - -.. note:: - Native javascript modules are the primary way to define javascript code. - -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*. - - -.. note :: - - Note that the custom class system should be avoided for creating new code. It - will be deprecated at some point, and then removed. New classes should use - the standard ES6 class system. - - -Creating a subclass -------------------- +- It is difficult to ensure that implementation details are not exposed: function + declarations in the global scope are accessible to all other code. -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). +- There is a single namespace, creating great potential for naming conflicts. -.. 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'); - }, - }); +- Dependencies are implicit: if a piece of code depends on another, the order in + which they are loaded is important, but difficult to guarantee. - var dog = new Dog(); - dog.move() +Using a module system helps resolve these issues: because modules specify their +dependencies, the module system can load them in the proper order or emit an error +if dependencies are missing or circular. Modules also form their own namespace, +and can choose what to export, preventing exposure of implementation detail and +naming collisions. -Mixins ------- +While we could use ECMAScript (ES) modules directly, there are a number of +disadvantages to that approach: each ES module requires a network round trip, which +becomes very slow when you have hundreds of files, and many files in Odoo need to +be present despite not being imported by anything because they simply add code +that the framework will use instead of the other way around. -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. +Because of this, Odoo has a system of asset bundles. In these bundles, JavaScript +files are ES modules with a special annotation at the top. These modules will be +bundled together and transpiled to be usable by our module loader. While you can +write code that doesn't use this module system, it is generally not recommended. -.. 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 ` -* 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 assets of -the module manifest, see -:ref:`assets `.) is given by: - -.. code-block:: xml +(see :ref:`frontend/modules/native_js`) -
- - -
-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 +Patching classes ---------------- -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 promise. The JS framework will wait for this promise - 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 promise to indicate when its work is done. - - :returns: promise - -.. 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 `. - 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.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 promise -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. - -* Make sure to wait for start to be finished before using $el e.g.: - - .. code-block:: javascript - - var Widget = require('web.Widget'); - - var AlmostCorrectWidget = Widget.extend({ - start: function () { - this.$el.hasClass(....) // in theory, $el is already set, but you don't know what the parent will do with it, better call super first - return this._super.apply(arguments); - }, - }); - - var IncorrectWidget = Widget.extend({ - start: function () { - this._super.apply(arguments); // the parent promise is lost, nobody will wait for the start of this widget - this.$el.hasClass(....) - }, - }); - - var CorrectWidget = Widget.extend({ - start: function () { - var self = this; - return this._super.apply(arguments).then(function() { - self.$el.hasClass(....) // this works, no promise is lost and the code executes in a controlled order: first super, then our code. - }); - }, - }); - -.. _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. - -The web client will wait for that list of template (included into the current asset) -to be loaded, before starting its first widget. - -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 Promise.all([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: +While we do our best to provide extension points that don't require it, it is +sometimes necessary to modify the behavior of an existing 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 `patch` utility function: .. code-block:: javascript - var Widget = require('web.Widget'); - var Counter = require('myModule.Counter'); + /** @odoo-module */ + import { Hamster } from "@web/core/hamster" + import { patch } from "@web/core/utils/patch"; - var MyWidget = Widget.extend({ - custom_events: { - valuechange: '_onValueChange' - }, - start: function () { - this.counter = new Counter(this); - var def = this.counter.appendTo(this.$el); - return Promise.all([def, this._super.apply(this, arguments)]); - }, - _onValueChange: function(event) { - // do something with event.data.val + patch(Hamster.prototype, { + sleep() { + super.sleep(...arguments); + console.log("zzzz"); }, }); - // in Counter widget, we need to call the trigger_up method: +When patching methods, you need to patch the class' prototype, but if you would +like to patch a static property of the class, you need to patch the class itself. - ... this.trigger_up('valuechange', {value: someValue}); +Patching is a dangerous operation and should be done with care as it will +modify all instances of the class, even if they have already been created. To +avoid weird issues, patches should be applied as soon as possible, at the +top-level of your module. Patching classes at runtime can result in extremely +difficult to debug issues if the class has already been instanciated. 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 +module). For example, one may need to add a new field widget 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: +There are a few registries available in the system. The registries that are used +by the framework are categories on the main registry, that can be imported from +:js:data:`@web/core/registry` -field registry (exported by :js:data:`web.field_registry`) +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); + import { registry } from "@web/core/registry"; + class PadField extends Component { ... } - Note that each value should be a subclass of *AbstractField* + registry.category("fields").add("pad", { + component: PadField, + supportedTypes: ["char"], + // ... + }); 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*. + This registry contains all JS views known to the web client. 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. + action. Client actions can be a function - the function will be called when the + action is invoked, and the returned value will be executed as a follow up action + if needed - or an Owl component that will be displayed when executing that action. 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 -------- +Within the webclient, there are some concerns that cannot be handled by a single +component, as the concern is transversal, involves many components, or needs to +maintain some state for as long as the application is alive. -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. +Services are a solution to these problems: they are created during application +startup, are available to components through the hook `useService`, and stay +alive for the entire lifetime of the application. -For example, we have the *ajax* service (job is to perform a rpc), the -*localStorage* (interact with the browser local storage) and many others. +For example, we have the *orm* service whose job is to allow interacting with +business objects on the server. -Here is a simplified example on how the ajax service is implemented: +Here is a simplified example on how the orm service is implemented: .. code-block:: javascript - var AbstractService = require('web.AbstractService'); - - var AjaxService = AbstractService.extend({ - name: 'ajax', - rpc: function (...) { - return ...; + import { registry } from "@web/core/registry"; + export const OrmService = { + start() { + return { + read(...) { ... }, + write(...) { ... }, + unlink(...) { ... }, + ... + } }, - }); - -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 ------- + }; + registry.category("services").add("orm", OrmService); -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. +Using services +-------------- -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. +Services are available in the environment, but should generally be used through +the `useService` hook, which prevents calling methods on the service after a +component has been destroyed, and prevents further code from executing after a +method call if the component was destroyed during the call. .. 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. + class SomeComponent extends Component { + setup() { + this.orm = useService("orm"); + } + // ... + getActivityModelViewID(model) { + return this.orm.call(model, "get_activity_view_id", this.params); + } + } -RPCs ----- +Talking to the server +--------------------- -The rpc functionality is supplied by the ajax service. But most people will -probably only interact with the *_rpc* helpers. +There are typically two use cases when working on Odoo: one may need to call a +method on a (python) model (this goes through the controller `/web/dataset/call_kw`), +or one may need to directly call a controller (available on some route). -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 is done through the orm service: -* Calling a method on a python model: + .. code-block:: javascript -.. code-block:: javascript + return this.orm.call("some.model", "some_method", [some, args]); - return this._rpc({ - model: 'some.model', - method: 'some_method', - args: [some, args], - }); +* Directly calling a controller is done through the rpc service: -* Directly calling a controller: + .. code-block:: javascript -.. code-block:: javascript + return this.rpc("/some/route/", { + some: param, + }); - return this._rpc({ - route: '/some/route/', - params: { some: kwargs}, - }); +.. note:: + The rpc service doesn't really perform what is generally understood as a + remote procedure call (RPC), but for historical reasons, within Odoo we + generally call any network request performed in JavaScript an RPC. As + highlighted in the previous paragraph, if you want to call a method on a + model, you should use the orm service. 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. +The types of notification follow the bootstrap toasts: -There are two types of notifications: - -- *notification*: useful to display some feedback. For example, whenever a user - unsubscribed to a channel. +- *info*: useful to display some informational feedback as a consequence of an + action that cannot fail. -- *warning*: useful to display some important/urgent information. Typically - most kind of (recoverable) errors in the system. +- *success*: the user performed an action that can sometimes fail but didn't. -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*. +- *warning*: the user performed an action that could only be partially completed. + Also useful if something is wrong but wasn't caused by the user directly, or + is not particularly actionable. -Notification system -------------------- +- *success*: the user tried to performed an action but it couldn't be completed. -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 +Notifications can also be used to ask a question to the user without disturbing +their workflow: e.g. a phone call received through VOIP: a sticky notification +could be displayed with two buttons to *Accept* or *Decline*. -- 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. +Displaying notifications +------------------------ -- a client action *display_notification*: this allows to trigger the display - of a notification from python (e.g. in the method called when the user - clicked on a button of type object). - -- an helper function in *ServiceMixin*: *displayNotification* - -Displaying a notification -------------------------- +There are two ways to display notifications in Odoo: -The most common way to display a notification is by using the method that come -from the *ServiceMixin*: +- The *notification* service allows component to display notifications from JS + code by calling the add method. -- *displayNotification(options)*: - Display a notification with the following *options*: +- The *display_notification* client action allows to trigger the display + of a notification from python (e.g. in the method called when the user + clicked on a button of type object). This client action uses the notification + service. - - *title*: string, optional. This will be displayed on the top as a title. +Notifications have a few *options*: - - *subtitle*: string, optional. This will be displayed on the top as a - subtitle. +- *title*: string, optional. This will be displayed on the top as a title. - - *message*: string, optional. The content of the notification. Do not forget - to escape your message via the markup function if needed. +- *message*: string, optional. The content of the notification. Can be a markup + object to display formatted text. - - *sticky*: boolean, optional (default false). If true, the notification - will stay until the user dismisses it. Otherwise, the notification will - be automatically closed after a short delay. +- *sticky*: boolean, optional (default false). If true, the notification + will stay until the user dismisses it. Otherwise, the notification will + be automatically closed after a short delay. - - *type*: string, optional (default 'warning'). Determines the style of the - notification. Possible values: 'info', 'success', 'warning', 'danger', ''. +- *type*: string, optional (default "warning"). Determines the style of the + notification. Possible values: "info", "success", "warning", "danger" - - *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. +- *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: +Here are some examples on how to display notifications in JS: .. code-block:: javascript // note that we call _t on the text to make sure it is properly translated. - this.displayNotification({ + this.notification.add({ title: _t("Success"), message: _t("Your signature request has been sent.") }); - this.displayNotification({ + this.notification.add({ title: _t("Error"), message: _t("Filter name is required."), - type: 'danger', + type: "danger", }); -Here an example in python: +And in Python: .. code-block:: python @@ -1018,116 +381,97 @@ Here an example in python: 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. +The Systray is the right part of the navbar in the interface, where the web +client displays a few widgets, such as the 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. +When the systray is created by the navbar, it will look for all registered +systray items and display them. -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. +There is currently no specific API for systray items. They are Owl components, +and can communicate with their environment just like other components, e.g. by +interacting with services. 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. +Items can be added to the systray by adding them to the "systray" registry: .. code-block:: javascript - var SystrayMenu = require('web.SystrayMenu'); - - var MySystrayWidget = Widget.extend({ + import { registry } from "@web/core/registry" + class MySystrayComponent extends Component { ... - }); - - SystrayMenu.Items.push(MySystrayWidget); - -Ordering --------- + } + registry.category("systray").add("MySystrayComponent", MySystrayComponent, { sequence: 1 }); -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; +The items are ordered in the systray according to their sequence in the systray +registry. 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: +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* +- each translatable string is tagged with the special function *_t* - 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 +- at 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:`/developer/howtos/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'); + import { _t } from "@web/core/l10n/translation"; - var _t = core._t; - var _lt = core._lt; - - var SomeWidget = Widget.extend({ - exampleString: _lt('this should be translated'), + class SomeComponent extends Component { + static exampleString = _t("this should be translated"); ... - someMethod: function () { - var str = _t('some text'); - ... - }, - }); + someMethod() { + const str = _t("some text"); + } + } -In this example, the *_lt* is necessary because the translations are not ready -when the module is loaded. +Note that using the translation function requires some care: the string given as +an argument cannot be dynamic, as it is extracted statically from the code to +generate the PO files and serves as the identifier for the term to translate. If +you need to inject some dynamic content in the string, `_t` supports placeholders: + +.. code-block:: javascript + + import { _t } from "@web/core/l10n/translation"; + const str = _t("Hello %s, you have %s unread messages.", user.name, unreadCount); + +Notice how the string itself is fixed. This allows the translation function to +retrieve the translated string *before* using it for interpolation. -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 +The webclient needs some information from the python to function properly. To +avoid an extra round-trip with the server by making a network request in JavaScript, +this information is serialized directly in the page, and can be accessed in JS +through the `@web/session` module. 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. +When the `/web` route is loaded, the server injects this information in a script +tag. The information is obtained by calling the method `session_info` of +the model `ir.http`. You can override this method to add information to the +returned dictionary. .. code-block:: python from odoo import models from odoo.http import request - class IrHttp(models.AbstractModel): _inherit = 'ir.http' @@ -1140,12 +484,12 @@ 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; + import { session } from "@web/session" + const 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 +needed by the web client to be ready. It is only 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. @@ -1153,65 +497,15 @@ process. Views ===== -The word 'view' has more than one meaning. This section is about the design of +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 - - - - - - - - - - - View - Controller - Renderer - Model - - -- 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). +While views are just owl components, the built-in views generally have the same +structure: a component called "SomethingController" which is the root of the view. +This component creates an instance of some "model" (an object responsible for +managing the data), and has a subcomponent called a "renderer" that handles the +display logic. .. _reference/js/widgets: @@ -2003,131 +1297,43 @@ Week Days (`week_days`) Client actions ============== -The idea of a client action is a customized widget that is integrated into the -web client interface, just like an *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 component that can be displayed as the main element in the +webclient, occupying all the space below the navbar, just like an `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 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 inherits from - the class AbstractAction, and is supposed to be registered in the - action registry under the corresponding key (from the field char) +- from the perspective of the web client, it is an Owl component registered in the + action registry under the same key its tag Whenever a menu item is associated with 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. +fetch the action definition from the server, then lookup its tag in the action +registry to get the component definition. This component will then be rendered by +the action container. Adding a client action ---------------------- -A client action is a widget that 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 AbstractAction = require('web.AbstractAction'); - - var ClientAction = AbstractAction.extend({ - hasControlPanel: true, - ... - }); +A client action is a component that will control the part of the screen below the +navbar. Defining a client action is as simple as creating an Owl component and +adding it to the action registry. -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 - - - Some Name - my-custom-action - - -Using the control panel ------------------------ - -By default, the client action does not display a control panel. To -do that, several steps should be done. - -- Set the *hasControlPanel* to *true*. - In the widget code: - - .. code-block:: javascript +.. code-block:: javascript - var MyClientAction = AbstractAction.extend({ - hasControlPanel: true, - ... - }); + import { registry } from "@web/core/registry"; + class MyClientAction extends Component { ... } + registry.category("actions").add("my-custom-action", ClientAction); -- Call the method *updateControlPanel* whenever we need to update the control panel. - For example: +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:: javascript +.. code-block:: xml - var SomeClientAction = Widget.extend({ - hasControlPanel: true, - ... - start: function () { - this._renderButtons(); - this._update_control_panel(); - ... - }, - do_show: function () { - ... - this._update_control_panel(); - }, - _renderButtons: function () { - this.$buttons = $(QWeb.render('SomeTemplate.Buttons')); - this.$buttons.on('click', ...); - }, - _update_control_panel: function () { - this.updateControlPanel({ - cp_content: { - $buttons: this.$buttons, - }, - }); - } - -The ``updateControlPanel`` is the main method to customize the content in the control panel. -For more information, look into the `control_panel_renderer.js <{GITHUB_PATH}/addons/web/static/src/js/views/control_panel/control_panel_renderer.js#L130>`_ file. - -.. _glob: - https://en.wikipedia.org/wiki/Glob_(programming) - -.. _.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 + + Some Name + my-custom-action +