[IMP] web: documents the new module system.

This add a basic overview of how the modules work.  This is a work in
progress, more documentation needs to be updated, such as the
tutorial.
This commit is contained in:
Géry Debongnie
2015-03-18 09:23:37 +01:00
parent ebc39b8788
commit b517e5a2b2
2 changed files with 192 additions and 76 deletions
+186 -70
View File
@@ -9,11 +9,11 @@ Javascript
Widgets
=======
.. class:: openerp.Widget
.. class:: Widget
The base class for all visual components. It corresponds to an MVC
view, and provides a number of service to simplify handling of a section
of a page:
Exported in ``web.Widget``, the base class for all visual components.
It corresponds to an MVC view, and provides a number of service to
simplify handling of a section of a page:
* Handles parent/child relationships between widgets
* Provides extensive lifecycle management with safety features (e.g.
@@ -25,45 +25,45 @@ Widgets
DOM Root
--------
A :class:`~openerp.Widget` is responsible for a section of the page
A :class:`~Widget` is responsible for a section of the page
materialized by the DOM root of the widget.
A widget's DOM root is available via two attributes:
.. attribute:: openerp.Widget.el
.. attribute:: Widget.el
raw DOM element set as root to the widget
.. attribute:: openerp.Widget.$el
.. attribute:: Widget.$el
jQuery wrapper around :attr:`~openerp.Widget.el`
jQuery wrapper around :attr:`~Widget.el`
There are two main ways to define and generate this DOM root:
.. attribute:: openerp.Widget.template
.. 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:: openerp.Widget.tagName
.. 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:: openerp.Widget.id
.. attribute:: Widget.id
Used to generate an ``id`` attribute on the generated DOM
root.
.. attribute:: openerp.Widget.className
.. attribute:: Widget.className
Used to generate a ``class`` attribute on the generated DOM root.
.. attribute:: openerp.Widget.attributes
.. 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
@@ -73,25 +73,25 @@ There are two main ways to define and generate this DOM root:
The DOM root can also be defined programmatically by overridding
.. function:: openerp.Widget.renderElement
.. function:: Widget.renderElement
Renders the widget's DOM root and sets it. The default
implementation will render a set template or generate an element
as described above, and will call
:func:`~openerp.Widget.setElement` on the result.
:func:`~Widget.setElement` on the result.
Any override to :func:`~openerp.Widget.renderElement` which
Any override to :func:`~Widget.renderElement` which
does not call its ``_super`` **must** call
:func:`~openerp.Widget.setElement` with whatever it
:func:`~Widget.setElement` with whatever it
generated or the widget's behavior is undefined.
.. note::
The default :func:`~openerp.Widget.renderElement` can
The default :func:`~Widget.renderElement` can
be called repeatedly, it will *replace* the previous DOM root
(using ``replaceWith``). However, this requires that the
widget correctly sets and unsets its events (and children
widgets). Generally, :func:`~openerp.Widget.renderElement` should
widgets). Generally, :func:`~Widget.renderElement` should
not be called repeatedly unless the widget advertizes this feature.
Using a widget
@@ -101,7 +101,7 @@ A widget's lifecycle has 3 main phases:
* creation and initialization of the widget instance
.. function:: openerp.Widget.init(parent)
.. function:: Widget.init(parent)
initialization method of widgets, synchronous, can be overridden to
take more parameters from the widget's creator/parent
@@ -109,26 +109,26 @@ A widget's lifecycle has 3 main phases:
: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:`~openerp.Widget`
:type parent: :class:`~Widget`
* DOM injection and startup, this is done by calling one of:
.. function:: openerp.Widget.appendTo(element)
.. function:: Widget.appendTo(element)
Renders the widget and inserts it as the last child of the target, uses
`.appendTo()`_
.. function:: openerp.Widget.prependTo(element)
.. function:: Widget.prependTo(element)
Renders the widget and inserts it as the first child of the target, uses
`.prependTo()`_
.. function:: openerp.Widget.insertAfter(element)
.. function:: Widget.insertAfter(element)
Renders the widget and inserts it as the preceding sibling of the target,
uses `.insertAfter()`_
.. function:: openerp.Widget.insertBefore(element)
.. function:: Widget.insertBefore(element)
Renders the widget and inserts it as the following sibling of the target,
uses `.insertBefore()`_
@@ -138,12 +138,12 @@ A widget's lifecycle has 3 main phases:
and are charged with three tasks:
* rendering the widget's root element via
:func:`~openerp.Widget.renderElement`
: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
.. function:: openerp.Widget.start()
.. function:: Widget.start()
asynchronous startup of the widget once it's been injected in the DOM,
generally used to perform asynchronous RPC calls to fetch whatever
@@ -152,7 +152,7 @@ A widget's lifecycle has 3 main phases:
Must return a deferred_ to indicate when its work is done.
A widget is *not guaranteed* to work correctly until its
:func:`~openerp.Widget.start` method has finished executing. The
:func:`~Widget.start` method has finished executing. The
widget's parent/creator must wait for a widget to be fully started
before interacting with it
@@ -160,7 +160,7 @@ A widget's lifecycle has 3 main phases:
* widget destruction and cleanup
.. function:: openerp.Widget.destroy()
.. function:: Widget.destroy()
destroys the widget's children, unbinds its events and removes its root
from the DOM. Automatically called when the widget's parent is destroyed,
@@ -171,7 +171,7 @@ A widget's lifecycle has 3 main phases:
Related to widget destruction is an important utility method:
.. function:: openerp.Widget.alive(deferred[, reject=false])
.. function:: Widget.alive(deferred[, reject=false])
A significant issue with RPC and destruction is that an RPC call may take
a long time to execute and return while a widget is being destroyed or
@@ -180,7 +180,7 @@ Related to widget destruction is an important utility method:
This is a frequent source of errors or strange behaviors.
:func:`~openerp.Widget.alive` can be used to wrap an RPC call,
:func:`~Widget.alive` can be used to wrap an RPC call,
ensuring that whatever operations should be executed when the call ends
are only executed if the widget is still alive::
@@ -199,7 +199,7 @@ Related to widget destruction is an important utility method:
``true``, the deferred_ will be rejected instead.
:returns: deferred_ object
.. function:: openerp.Widget.isDestroyed()
.. function:: Widget.isDestroyed()
:returns: ``true`` if the widget is being or has been destroyed, ``false``
otherwise
@@ -210,7 +210,7 @@ Accessing DOM content
Because a widget is only responsible for the content below its DOM root, there
is a shortcut for selecting sub-sections of a widget's DOM:
.. function:: openerp.Widget.$(selector)
.. function:: Widget.$(selector)
Applies the CSS selector specified as parameter to the widget's
DOM root::
@@ -229,7 +229,7 @@ Because a widget is only responsible for the content below its DOM root, there
Resetting the DOM root
''''''''''''''''''''''
.. function:: openerp.Widget.setElement(element)
.. 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
@@ -247,9 +247,9 @@ DOM events handling
A widget will generally need to respond to user action within its
section of the page. This entails binding events to DOM elements.
To this end, :class:`~openerp.Widget` provides a shortcut:
To this end, :class:`~Widget` provides a shortcut:
.. attribute:: openerp.Widget.events
.. 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
@@ -269,23 +269,23 @@ To this end, :class:`~openerp.Widget` provides a shortcut:
(only an event name is specified), the event will be set directly on the
widget's DOM root.
.. function:: openerp.Widget.delegateEvents
.. function:: Widget.delegateEvents
This method is in charge of binding :attr:`~openerp.Widget.events` to the
This method is in charge of binding :attr:`~Widget.events` to the
DOM. It is automatically called after setting the widget's DOM root.
It can be overridden to set up more complex events than the
:attr:`~openerp.Widget.events` map allows, but the parent should always be
called (or :attr:`~openerp.Widget.events` won't be handled correctly).
:attr:`~Widget.events` map allows, but the parent should always be
called (or :attr:`~Widget.events` won't be handled correctly).
.. function:: openerp.Widget.undelegateEvents
.. function:: Widget.undelegateEvents
This method is in charge of unbinding :attr:`~openerp.Widget.events` from
This method is in charge of unbinding :attr:`~Widget.events` from
the DOM root when the widget is destroyed or the DOM root is reset, in
order to avoid leaving "phantom" events.
It should be overridden to un-set any event set in an override of
:func:`~openerp.Widget.delegateEvents`.
:func:`~Widget.delegateEvents`.
.. note:: this behavior should be compatible with `Backbone's
delegateEvents`_, apart from not accepting any argument.
@@ -293,12 +293,12 @@ To this end, :class:`~openerp.Widget` provides a shortcut:
Subclassing Widget
------------------
:class:`~openerp.Widget` is subclassed in the standard manner (via the
:func:`~openerp.Class.extend` method), and provides a number of
:class:`~Widget` is subclassed in the standard manner (via the
:func:`~Class.extend` method), and provides a number of
abstract properties and concrete methods (which you may or may not want to
override). Creating a subclass looks like this::
var MyWidget = openerp.Widget.extend({
var MyWidget = Widget.extend({
// QWeb template to use when rendering the object
template: "MyQWebTemplate",
events: {
@@ -332,14 +332,14 @@ The new class can then be used in the following manner::
my_widget.appendTo(".some-div");
After these two lines have executed (and any promise returned by
:func:`~openerp.Widget.appendTo` has been resolved if needed), the widget is
:func:`~Widget.appendTo` has been resolved if needed), the widget is
ready to be used.
.. note:: the insertion methods will start the widget themselves, and will
return the result of :func:`~openerp.Widget.start()`.
return the result of :func:`~Widget.start()`.
If for some reason you do not want to call these methods, you will
have to first call :func:`~openerp.Widget.render()` on the
have to first call :func:`~Widget.render()` on the
widget, then insert it into your DOM and start it.
If the widget is not needed anymore (because it's transient), simply terminate
@@ -373,14 +373,14 @@ Development Guidelines
restricted to a given component's scope. Unfiltered selections such as
``$(selector)`` or ``document.querySelectorAll(selector)`` will generally
lead to unintended or incorrect behavior. Odoo Web's
:class:`~openerp.web.Widget` has an attribute providing its DOM root
(:attr:`~openerp.web.Widget.$el`), and a shortcut to select nodes directly
(:func:`~openerp.web.Widget.$`).
: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:`~openerp.web.Widget.$el`
its own personal :attr:`~Widget.$el`
* html templating/rendering should use QWeb unless absolutely trivial.
* All interactive components (components displaying information to the screen
or intercepting DOM events) must inherit from :class:`~openerp.web.Widget`
or intercepting DOM events) must inherit from :class:`~Widget`
and correctly implement and use its API and life cycle.
.. _.appendTo():
@@ -426,11 +426,11 @@ High-level API: calling into Odoo models
-------------------------------------------
Access to Odoo object methods (made available through XML-RPC from the server)
is done via :class:`openerp.Model`. It maps onto the Odoo server objects via
two primary methods, :func:`~openerp.Model.call` and
:func:`~openerp.Model.query`.
is done via :class:`Model` (exported in ``web.Model``). It maps onto the Odoo server objects via
two primary methods, :func:`~Model.call` and
:func:`~Model.query`.
:func:`~openerp.Model.call` is a direct mapping to the corresponding method of
:func:`~Model.call` is a direct mapping to the corresponding method of
the Odoo server object. Its usage is similar to that of the Odoo Model API,
with three differences:
@@ -446,14 +446,14 @@ with three differences:
* No notion of pooler, the model proxy is instantiated where needed,
not fetched from an other (somewhat global) object::
var Users = new openerp.Model('res.users');
var Users = new Model('res.users');
Users.call('change_password', ['oldpassword', 'newpassword'],
{context: some_context}).then(function (result) {
// do something with change_password result
});
:func:`~openerp.Model.query` is a shortcut for a builder-style
:func:`~Model.query` is a shortcut for a builder-style
interface to searches (``search`` + ``read`` in Odoo RPC terms). It
returns a :class:`~openerp.web.Query` object which is immutable but
allows building new :class:`~openerp.web.Query` instances from the
@@ -474,26 +474,26 @@ RPC call every time they are called.
For that reason, it's actually possible to keep "intermediate" queries
around and use them differently/add new specifications on them.
.. class:: openerp.Model(name)
.. class:: Model(name)
.. attribute:: openerp.Model.name
.. attribute:: Model.name
name of the OpenERP model this object is bound to
name of the model this object is bound to
.. function:: openerp.Model.call(method[, args][, kwargs])
.. function:: Model.call(method[, args][, kwargs])
Calls the ``method`` method of the current model, with the
provided positional and keyword arguments.
:param String method: method to call over rpc on the
:attr:`~openerp.Model.name`
:attr:`~Model.name`
:param Array<> args: positional arguments to pass to the
method, optional
:param Object<> kwargs: keyword arguments to pass to the
method, optional
:rtype: Deferred<>
.. function:: openerp.Model.query(fields)
.. function:: Model.query(fields)
:param Array<String> fields: list of fields to fetch during
the search
@@ -652,7 +652,7 @@ an array of :class:`~openerp.web.QueryGroup`:
.. function:: openerp.web.QueryGroup.query([fields...])
equivalent to :func:`openerp.web.Model.query` but pre-filtered to
equivalent to :func:`Model.query` but pre-filtered to
only include the records within this group. Returns a
:class:`~openerp.web.Query` which can be further manipulated as
usual.
@@ -670,8 +670,8 @@ While the previous section is great for calling core OpenERP code
Odoo Web.
For this, a lower-level API exists on on
:class:`~openerp.web.Session` objects (usually available through
``openerp.session``): the ``rpc`` method.
:class:`~Session` objects (the class is exported in ``web.Session``, but
an instance isusually available through ``web.session``): the ``rpc`` method.
This method simply takes an absolute path (the absolute URL of the JSON
:ref:`route <reference/http/routing>` to call) and a mapping of attributes to
@@ -681,7 +681,7 @@ fetches the return value of the Python methods, converted to JSON.
For instance, to call the ``resequence`` of the
:class:`~web.controllers.main.DataSet` controller::
openerp.session.rpc('/web/dataset/resequence', {
session.rpc('/web/dataset/resequence', {
model: some_model,
ids: array_of_ids,
offset: 42
@@ -696,6 +696,122 @@ For instance, to call the ``resequence`` of the
Web Client
==========
Javascript module system overview
---------------------------------
A new module system (inspired from requirejs) has now been deployed.
It has many advantages over the Odoo version 8 system.
* loading order: dependencies are guaranteed to be loaded first, even if
files are not loaded in the correct order in the bundle files.
* easier to split a file into smaller logical units.
* no global variables: easier to reason.
* it is possible to examine every dependencies and dependants. This
makes refactoring much simpler, and less risky.
It has also some disadvantages:
* files are required to use the module system if they want to interact
with odoo, since the various objects are only available in the module
system, and not in global variables
* circular dependencies are not supported. It makes sense, but it means
that one needs to be careful.
This is obviously a very large change and will require everyone to
adopt new habits. For example, the variable openerp does not exist
anymore. The new way of doing things is to import explicitely the module
you need, and declaring explicitely the objects you export. Here is a
simple example::
odoo.define('addon_name.service', function (require) {
var utils = require('web.utils');
var Model = require('web.Model');
// do things with utils and Model
var something_useful = 15;
return {
something_useful: something_useful,
};
});
This snippet shows a module named ``addon_name.service``. It is defined
with the ``odoo.define`` function. ``odoo.define`` takes a name and a
function for arguments:
* The name is the concatenation of the name of the addon it is defined in
and a name describing its purpose.
* The function is the place where the javascript module is actually
defined. It takes a function ``require`` as first argument, and
returns something (or not, depending if it needs to export something).
The ``require`` function is used to get a handle on the dependencies.
In this case, it gives a handle on two javascript modules from the
``web`` addon, namely ``web.utils`` and ``web.Model``.
The idea is that you define what you need to import (by using the
``require`` function) and declare what you export (by returning
something). The web client will then make sure that your code is loaded
properly.
Modules are contained in a file, but a file can define several modules
(however, it is better to keep them in separate files).
Web client structure
--------------------
The web client files have been refactored into smaller and simpler files.
Here is a description of the current file structure:
* the ``framework/`` folder contains all basic low level modules. The
modules here are supposed to be generic. Some of them are:
* ``web.ajax`` implements rpc calls
* ``web.core`` is a generic modules. It exports various useful
objects and functions, such as ``qweb``, ``_t`` or the main bus.
* ``web.Widget`` contains the widget class
* ``web.Model`` is an abstraction over ``web.ajax`` to make
calls to the server model methods
* ``web.session`` is the former ``openerp.session``
* ``web.utils`` for useful code snippets
* ``web.time`` for every time-related generic functions
* the ``views/`` folder contains all view definitions
* ``widgets/`` is for standalone widgets
The ``js/`` folder also contains some important files:
* ``action_manager.js`` is the ActionManager class
* ``boot.js`` is the file actually implementing the module system
* ``menu.js`` is the definition of the top menu
* ``web_client.js`` is for the root widget WebClient
* ``view_manager.js`` contains the ViewManager
The two other files are ``tour.js`` for the tours and ``compatibility.js``.
The latter file is a compatibility layer bridging the old system to the
new module system. This is where every module names are exported to the
global variable ``openerp``. In theory, our addons should work without
ever using the variable ``openerp``, and the compatibility module can be
disabled safely.
Javascript conventions
----------------------
Here are some basic conventions for the javascript code:
* 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 exports at the end.
* add the ``use strict`` statement at the beginning of every module
* always name your module properly: ``addon_name.description``.
* use capital letters for classes (for example, ``ActionManager`` is
defined in the module ``web.ActionManager``), and lowercase for
everything else (for example, ``ajax`` is defined in ``web.ajax``).
* declare one module per file
Testing in Odoo Web Client
==========================
+6 -6
View File
@@ -516,13 +516,13 @@ The javascript QWeb implementation provides a few debugging hooks:
Helpers
-------
.. js:attribute:: openerp.qweb
.. js:attribute:: core.qweb
An instance of :js:class:`QWeb2.Engine` with all module-defined template
(core is the ``web.core`` module) An instance of :js:class:`QWeb2.Engine` with all module-defined template
files loaded, and references to standard helper objects ``_``
(underscore), ``_t`` (translation function) and JSON_.
:js:func:`openerp.qweb.render <QWeb2.Engine.render>` can be used to
:js:func:`core.qweb.render <QWeb2.Engine.render>` can be used to
easily render basic module templates
API
@@ -533,9 +533,9 @@ API
The QWeb "renderer", handles most of QWeb's logic (loading,
parsing, compiling and rendering templates).
OpenERP Web instantiates one for the user, and sets it to
``instance.web.qweb``. It also loads all the template files of the
various modules into that QWeb instance.
OpenERP Web instantiates one for the user in the core module, and
exports it to ``core.qweb``. It also loads all the template files
of the various modules into that QWeb instance.
A :js:class:`QWeb2.Engine` also serves as a "template namespace".