[IMP] create JavaScript howtos
The JavaScript cheatsheet is outdated, we therefore remove it and
replace it by multiple howtos:
- Create a view from scratch
- Extending an existing view
- Create a field from scratch
- Extend an existing field
- Create a client action
There is other subjects to introduce as the web framework is big. Other
future contributions will cover them.
closes odoo/documentation#4020
X-original-commit: 7e4435deb8
Signed-off-by: Dardenne Florent (dafl) <dafl@odoo.com>
This commit is contained in:
parent
931920bc31
commit
56a5b0480e
@ -9,6 +9,9 @@ How-to guides
|
||||
:titlesonly:
|
||||
|
||||
howtos/scss_tips
|
||||
howtos/javascript_field
|
||||
howtos/javascript_view
|
||||
howtos/javascript_client_action
|
||||
howtos/web_services
|
||||
howtos/company
|
||||
howtos/accounting_localization
|
||||
@ -23,6 +26,21 @@ How-to guides
|
||||
|
||||
Follow this guide to keep the technical debt of your CSS code under control.
|
||||
|
||||
.. card:: Customize a field
|
||||
:target: howtos/javascript_field
|
||||
|
||||
Learn how to customize field components in the Odoo JavaScript web framework.
|
||||
|
||||
.. card:: Customize a view type
|
||||
:target: howtos/javascript_view
|
||||
|
||||
Learn how to customize view types in the Odoo JavaScript web framework.
|
||||
|
||||
.. card:: Create a client action
|
||||
:target: howtos/javascript_client_action
|
||||
|
||||
Learn how to create client actions in the Odoo JavaScript web framework.
|
||||
|
||||
.. card:: Web services
|
||||
:target: howtos/web_services
|
||||
|
||||
|
46
content/developer/howtos/javascript_client_action.rst
Normal file
46
content/developer/howtos/javascript_client_action.rst
Normal file
@ -0,0 +1,46 @@
|
||||
|
||||
======================
|
||||
Create a client action
|
||||
======================
|
||||
|
||||
A client action triggers an action that is entirely implemented in the client side.
|
||||
One of the benefits of using a client action is the ability to create highly customized interfaces
|
||||
with ease. A client action is typically defined by an OWL component; we can also use the web
|
||||
framework and use services, core components, hooks,...
|
||||
|
||||
#. Create the :ref:`client action <reference/actions/client>`, don't forget to
|
||||
make it accessible.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<record model="ir.actions.client" id="my_client_action">
|
||||
<field name="name">My Client Action</field>
|
||||
<field name="tag">my_module.MyClientAction</field>
|
||||
</record>
|
||||
|
||||
#. Create a component that represents the client action.
|
||||
|
||||
.. code-block:: js
|
||||
:caption: :file:`my_client_action.js`
|
||||
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
class MyClientAction extends Component {}
|
||||
MyClientAction.template = "my_module.clientaction";
|
||||
|
||||
// remember the tag name we put in the first step
|
||||
registry.category("actions").add("my_module.MyClientAction", MyClientAction);
|
||||
|
||||
.. code-block:: xml
|
||||
:caption: :file:`my_client_action.xml`
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="awesome_tshirt.clientaction" owl="1">
|
||||
Hello world
|
||||
</t>
|
||||
</templates>
|
107
content/developer/howtos/javascript_field.rst
Normal file
107
content/developer/howtos/javascript_field.rst
Normal file
@ -0,0 +1,107 @@
|
||||
|
||||
=================
|
||||
Customize a field
|
||||
=================
|
||||
|
||||
Subclass an existing field component
|
||||
====================================
|
||||
|
||||
Let's take an example where we want to extends the `BooleanField` to create a boolean field
|
||||
displaying "Late!" in red whenever the checkbox is checked.
|
||||
|
||||
#. Create a new widget component extending the desired field component.
|
||||
|
||||
.. code-block:: javascript
|
||||
:caption: :file:`late_order_boolean_field.js`
|
||||
|
||||
/** @odoo-module */
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { BooleanField } from "@web/views/fields/boolean/boolean_field";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
|
||||
class LateOrderBooleanField extends BooleanField {}
|
||||
LateOrderBooleanField.template = "my_module.LateOrderBooleanField";
|
||||
|
||||
#. Create the field template.
|
||||
|
||||
The component uses a new template with the name `my_module.LateOrderBooleanField`. Create it by
|
||||
inheriting the current template of the `BooleanField`.
|
||||
|
||||
.. code-block:: xml
|
||||
:caption: :file:`late_order_boolean_field.xml`
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="my_module.LateOrderBooleanField" t-inherit="web.BooleanField" owl="1">
|
||||
<xpath expr="//CheckBox" position="after">
|
||||
<span t-if="props.value" class="text-danger"> Late! </span>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
||||
#. Register the component to the fields registry.
|
||||
|
||||
.. code-block::
|
||||
:caption: :file:`late_order_boolean_field.js`
|
||||
|
||||
registry.category("fields").add("late_boolean", LateOrderBooleanField);
|
||||
|
||||
#. Add the widget in the view arch as an attribute of the field.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<field name="somefield" widget="late_boolean"/>
|
||||
|
||||
Create a new field component
|
||||
============================
|
||||
|
||||
Assume that we want to create a field that displays a simple text in red.
|
||||
|
||||
#. Create a new Owl component representing our new field
|
||||
|
||||
.. code-block:: js
|
||||
:caption: :file:`my_text_field.js`
|
||||
|
||||
/** @odoo-module */
|
||||
|
||||
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
export class MyTextField extends Component {
|
||||
|
||||
/**
|
||||
* @param {boolean} newValue
|
||||
*/
|
||||
onChange(newValue) {
|
||||
this.props.update(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
MyTextField.template = xml`
|
||||
<input t-att-id="props.id" class="text-danger" t-att-value="props.value" onChange.bind="onChange" />
|
||||
`;
|
||||
MyTextField.props = {
|
||||
...standardFieldProps,
|
||||
};
|
||||
MyTextField.supportedTypes = ["char"];
|
||||
|
||||
The imported `standardFieldProps` contains the standard props passed by the `View` such as
|
||||
the `update` function to update the value, the `type` of the field in the model, the
|
||||
`readonly` boolean, and others.
|
||||
|
||||
#. In the same file, register the component to the fields registry.
|
||||
|
||||
.. code-block:: js
|
||||
:caption: :file:`my_text_field.js`
|
||||
|
||||
registry.category("fields").add("my_text_field", MyTextField);
|
||||
|
||||
This maps the widget name in the arch to its actual component.
|
||||
|
||||
#. Add the widget in the view arch as an attribute of the field.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<field name="somefield" widget="my_text_field"/>
|
262
content/developer/howtos/javascript_view.rst
Normal file
262
content/developer/howtos/javascript_view.rst
Normal file
@ -0,0 +1,262 @@
|
||||
=====================
|
||||
Customize a view type
|
||||
=====================
|
||||
|
||||
Subclass an existing view
|
||||
=========================
|
||||
|
||||
Assume we need to create a custom version of a generic view. For example, a kanban view with some
|
||||
extra ribbon-like widget on top (to display some specific custom information). In that case, this
|
||||
can be done in a few steps:
|
||||
|
||||
#. Extend the kanban controller/renderer/model and register it in the view registry.
|
||||
|
||||
.. code-block:: js
|
||||
:caption: :file:`custom_kanban_controller.js`
|
||||
|
||||
/** @odoo-module */
|
||||
|
||||
import { KanbanController } from "@web/views/kanban/kanban_controller";
|
||||
import { kanbanView } from "@web/views/kanban/kanban_view";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
// the controller usually contains the Layout and the renderer.
|
||||
class CustomKanbanController extends KanbanController {
|
||||
// Your logic here, override or insert new methods...
|
||||
// if you override setup(), don't forget to call super.setup()
|
||||
}
|
||||
|
||||
CustomKanbanController.template = "my_module.CustomKanbanView";
|
||||
|
||||
export const customKanbanView = {
|
||||
...kanbanView, // contains the default Renderer/Controller/Model
|
||||
Controller: CustomKanbanController,
|
||||
};
|
||||
|
||||
// Register it to the views registry
|
||||
registry.category("views").add("custom_kanban", customeKanbanView);
|
||||
|
||||
In our custom kanban, we defined a new template. We can either inherit the kanban controller
|
||||
template and add our template pieces or we can define a completely new template.
|
||||
|
||||
.. code-block:: xml
|
||||
:caption: :file:`custom_kanban_controller.xml`
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates>
|
||||
<t t-name="my_module.CustomKanbanView" t-inherit="web.KanbanView" owl="1">
|
||||
<xpath expr="//Layout" position="before">
|
||||
<div>
|
||||
Hello world !
|
||||
</div>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
||||
#. Use the view with the `js_class` attribute in arch.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<kanban js_class="custom_kanban">
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<!--Your comment-->
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
|
||||
The possibilities for extending views are endless. While we have only extended the controller
|
||||
here, you can also extend the renderer to add new buttons, modify how records are presented, or
|
||||
customize the dropdown, as well as extend other components such as the model and `buttonTemplate`.
|
||||
|
||||
Create a new view from scratch
|
||||
==============================
|
||||
|
||||
Creating a new view is an advanced topic. This guide highlight only the essential steps.
|
||||
|
||||
#. Create the controller.
|
||||
|
||||
The primary role of a controller is to facilitate the coordination between various components
|
||||
of a view, such as the Renderer, Model, and Layout.
|
||||
|
||||
.. code-block:: js
|
||||
:caption: :file:`beautiful_controller.js`
|
||||
|
||||
/** @odoo-module */
|
||||
|
||||
import { Layout } from "@web/search/layout";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { Component, onWillStart, useState} from "@odoo/owl";
|
||||
|
||||
export class BeautifulController extends Component {
|
||||
setup() {
|
||||
this.orm = useService("orm");
|
||||
|
||||
// The controller create the model and make it reactive so whenever this.model is
|
||||
// accessed and edited then it'll cause a rerendering
|
||||
this.model = useState(
|
||||
new this.props.Model(
|
||||
this.orm,
|
||||
this.props.resModel,
|
||||
this.props.fields,
|
||||
this.props.archInfo,
|
||||
this.props.domain
|
||||
)
|
||||
);
|
||||
|
||||
onWillStart(async () => {
|
||||
await this.model.load();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
BeautifulController.template = "my_module.View";
|
||||
BeautifulController.components = { Layout };
|
||||
|
||||
The template of the Controller displays the control panel with Layout and also the
|
||||
renderer.
|
||||
|
||||
.. code-block:: xml
|
||||
:caption: :file:`beautiful_controller.xml`
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="my_module.View" owl="1">
|
||||
<Layout display="props.display" className="'h-100 overflow-auto'">
|
||||
<t t-component="props.Renderer" records="model.records" propsYouWant="'Hello world'"/>
|
||||
</Layout>
|
||||
</t>
|
||||
</templates>
|
||||
|
||||
#. Create the renderer.
|
||||
|
||||
The primary function of a renderer is to generate a visual representation of data by rendering
|
||||
the view that includes records.
|
||||
|
||||
.. code-block:: js
|
||||
:caption: :file:`beautiful_renderer.js`
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
export class BeautifulRenderer extends Component {}
|
||||
|
||||
BeautifulRenderer.template = "my_module.Renderer";
|
||||
|
||||
.. code-block:: xml
|
||||
:caption: :file:`beautiful_renderer.xml`
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="my_module.Renderer" owl="1">
|
||||
<t t-esc="props.propsYouWant"/>
|
||||
<t t-foreach="props.records" t-as="record" t-key="record.id">
|
||||
// Show records
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
|
||||
#. Create the model.
|
||||
|
||||
The role of the model is to retrieve and manage all the necessary data in the view.
|
||||
|
||||
.. code-block:: js
|
||||
:caption: :file:`beautiful_model.js`
|
||||
|
||||
/** @odoo-module */
|
||||
|
||||
import { KeepLast } from "@web/core/utils/concurrency";
|
||||
|
||||
export class BeautifulModel {
|
||||
constructor(orm, resModel, fields, archInfo, domain) {
|
||||
this.orm = orm;
|
||||
this.resModel = resModel;
|
||||
// We can access arch information parsed by the beautiful arch parser
|
||||
const { fieldFromTheArch } = archInfo;
|
||||
this.fieldFromTheArch = fieldFromTheArch;
|
||||
this.fields = fields;
|
||||
this.domain = domain;
|
||||
this.keepLast = new KeepLast();
|
||||
}
|
||||
|
||||
async load() {
|
||||
// The keeplast protect against concurrency call
|
||||
const { length, records } = await this.keepLast.add(
|
||||
this.orm.webSearchRead(this.resModel, this.domain, [this.fieldsFromTheArch], {})
|
||||
);
|
||||
this.records = records;
|
||||
this.recordsLength = length;
|
||||
}
|
||||
}
|
||||
|
||||
.. note::
|
||||
|
||||
For advanced cases, instead of creating a model from scratch, it is also possible to use
|
||||
`RelationalModel`, which is used by other views.
|
||||
|
||||
#. Create the arch parser.
|
||||
|
||||
The role of the arch parser is to parse the arch view so the view has access to the information.
|
||||
|
||||
.. code-block:: js
|
||||
:caption: :file:`beautiful_arch_parser.js`
|
||||
|
||||
/** @odoo-module */
|
||||
|
||||
import { XMLParser } from "@web/core/utils/xml";
|
||||
|
||||
export class BeautifulArchParser extends XMLParser {
|
||||
parse(arch) {
|
||||
const xmlDoc = this.parseXML(arch);
|
||||
const fieldFromTheArch = xmlDoc.getAttribute("fieldFromTheArch");
|
||||
return {
|
||||
fieldFromTheArch,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#. Create the view and combine all the pieces together, then register the view in the views
|
||||
registry.
|
||||
|
||||
.. code-block:: js
|
||||
:caption: :file:`beautiful_view.js`
|
||||
|
||||
/** @odoo-module */
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { BeautifulController } from "./beautiful_controller";
|
||||
import { BeautifulArchParser } from "./beautiful_arch_parser";
|
||||
import { BeautifylModel } from "./beautiful_model";
|
||||
import { BeautifulRenderer } from "./beautiful_renderer";
|
||||
|
||||
export const beautifulView = {
|
||||
type: "beautiful",
|
||||
display_name: "Beautiful",
|
||||
icon: "fa fa-picture-o", // the icon that will be displayed in the Layout panel
|
||||
multiRecord: true,
|
||||
Controller: BeautifulController,
|
||||
ArchParser: BeautifulArchParser,
|
||||
Model: BeautifulModel,
|
||||
Renderer: BeautifulRenderer,
|
||||
|
||||
props(genericProps, view) {
|
||||
const { ArchParser } = view;
|
||||
const { arch } = genericProps;
|
||||
const archInfo = new ArchParser().parse(arch);
|
||||
|
||||
return {
|
||||
...genericProps,
|
||||
Model: view.Model,
|
||||
Renderer: view.Renderer,
|
||||
archInfo,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("views").add("beautifulView", beautifulView);
|
||||
|
||||
#. Use the view in an arch.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
...
|
||||
<beautiful fieldFromTheArch="res.partner"/>
|
||||
...
|
@ -15,7 +15,6 @@ JavaScript framework
|
||||
frontend/services
|
||||
frontend/hooks
|
||||
frontend/patching_code
|
||||
frontend/javascript_cheatsheet
|
||||
frontend/javascript_reference
|
||||
frontend/mobile
|
||||
frontend/qweb
|
||||
|
@ -1,546 +0,0 @@
|
||||
|
||||
.. _reference/jscs:
|
||||
|
||||
=====================
|
||||
Javascript Cheatsheet
|
||||
=====================
|
||||
|
||||
There are many ways to solve a problem in JavaScript, and in Odoo. However, the
|
||||
Odoo framework was designed to be extensible (this is a pretty big constraint),
|
||||
and some common problems have a nice standard solution. The standard solution
|
||||
has probably the advantage of being easy to understand for an odoo developer,
|
||||
and will probably keep working when Odoo is modified.
|
||||
|
||||
This document tries to explain the way one could solve some of these issues.
|
||||
Note that this is not a reference. This is just a random collection of recipes,
|
||||
or explanations on how to proceed in some cases.
|
||||
|
||||
|
||||
First of all, remember that the first rule of customizing odoo with JS is:
|
||||
*try to do it in python*. This may seem strange, but the python framework is
|
||||
quite extensible, and many behaviours can be done simply with a touch of xml or
|
||||
python. This has usually a lower cost of maintenance than working with JS:
|
||||
|
||||
- the JS framework tends to change more, so JS code needs to be more frequently
|
||||
updated
|
||||
- it is often more difficult to implement a customized behaviour if it needs to
|
||||
communicate with the server and properly integrate with the javascript framework.
|
||||
There are many small details taken care by the framework that customized code
|
||||
needs to replicate. For example, responsiveness, or updating the url, or
|
||||
displaying data without flickering.
|
||||
|
||||
|
||||
.. note:: This document does not really explain any concepts. This is more a
|
||||
cookbook. For more details, please consult the javascript reference
|
||||
page (see :doc:`javascript_reference`)
|
||||
|
||||
Creating a new field widget
|
||||
===========================
|
||||
|
||||
This is probably a really common usecase: we want to display some information in
|
||||
a form view in a really specific (maybe business dependent) way. For example,
|
||||
assume that we want to change the text color depending on some business condition.
|
||||
|
||||
This can be done in three steps: creating a new widget, registering it in the
|
||||
field registry, then adding the widget to the field in the form view
|
||||
|
||||
- creating a new widget:
|
||||
This can be done by extending a widget:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var FieldChar = require('web.basic_fields').FieldChar;
|
||||
|
||||
var CustomFieldChar = FieldChar.extend({
|
||||
_renderReadonly: function () {
|
||||
// implement some custom logic here
|
||||
},
|
||||
});
|
||||
|
||||
- registering it in the field registry:
|
||||
The web client needs to know the mapping between a widget name and its
|
||||
actual class. This is done by a registry:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var fieldRegistry = require('web.field_registry');
|
||||
|
||||
fieldRegistry.add('my-custom-field', CustomFieldChar);
|
||||
|
||||
- adding the widget in the form view
|
||||
.. code-block:: xml
|
||||
|
||||
<field name="somefield" widget="my-custom-field"/>
|
||||
|
||||
Note that only the form, list and kanban views use this field widgets registry.
|
||||
These views are tightly integrated, because the list and kanban views can
|
||||
appear inside a form view).
|
||||
|
||||
Modifying an existing field widget
|
||||
==================================
|
||||
|
||||
Another use case is that we want to modify an existing field widget. For
|
||||
example, the voip addon in odoo need to modify the FieldPhone widget to add the
|
||||
possibility to easily call the given number on voip. This is done by *including*
|
||||
the FieldPhone widget, so there is no need to change any existing form view.
|
||||
|
||||
Field Widgets (instances of (subclass of) AbstractField) are like every other
|
||||
widgets, so they can be monkey patched. This looks like this:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var basic_fields = require('web.basic_fields');
|
||||
var Phone = basic_fields.FieldPhone;
|
||||
|
||||
Phone.include({
|
||||
events: _.extend({}, Phone.prototype.events, {
|
||||
'click': '_onClick',
|
||||
}),
|
||||
|
||||
_onClick: function (e) {
|
||||
if (this.mode === 'readonly') {
|
||||
e.preventDefault();
|
||||
var phoneNumber = this.value;
|
||||
// call the number on voip...
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Note that there is no need to add the widget to the registry, since it is already
|
||||
registered.
|
||||
|
||||
Modifying a main widget from the interface
|
||||
==========================================
|
||||
|
||||
Another common usecase is the need to customize some elements from the user
|
||||
interface. For example, adding a message in the home menu. The usual process
|
||||
in this case is again to *include* the widget. This is the only way to do it,
|
||||
since there are no registries for those widgets.
|
||||
|
||||
This is usually done with code looking like this:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var HomeMenu = require('web_enterprise.HomeMenu');
|
||||
|
||||
HomeMenu.include({
|
||||
render: function () {
|
||||
this._super();
|
||||
// do something else here...
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
Creating a new view (from scratch)
|
||||
==================================
|
||||
|
||||
Creating a new view is a more advanced topic. This cheatsheet will only
|
||||
highlight the steps that will probably need to be done (in no particular order):
|
||||
|
||||
- adding a new view type to the field ``type`` of ``ir.ui.view``::
|
||||
|
||||
class View(models.Model):
|
||||
_inherit = 'ir.ui.view'
|
||||
|
||||
type = fields.Selection(selection_add=[('map', "Map")])
|
||||
|
||||
- adding the new view type to the field ``view_mode`` of ``ir.actions.act_window.view``::
|
||||
|
||||
class ActWindowView(models.Model):
|
||||
_inherit = 'ir.actions.act_window.view'
|
||||
|
||||
view_mode = fields.Selection(selection_add=[('map', "Map")])
|
||||
|
||||
|
||||
- creating the four main pieces which makes a view (in JavaScript):
|
||||
we need a view (a subclass of ``AbstractView``, this is the factory), a
|
||||
renderer (from ``AbstractRenderer``), a controller (from ``AbstractController``)
|
||||
and a model (from ``AbstractModel``). I suggest starting by simply
|
||||
extending the superclasses:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var AbstractController = require('web.AbstractController');
|
||||
var AbstractModel = require('web.AbstractModel');
|
||||
var AbstractRenderer = require('web.AbstractRenderer');
|
||||
var AbstractView = require('web.AbstractView');
|
||||
|
||||
var MapController = AbstractController.extend({});
|
||||
var MapRenderer = AbstractRenderer.extend({});
|
||||
var MapModel = AbstractModel.extend({});
|
||||
|
||||
var MapView = AbstractView.extend({
|
||||
config: {
|
||||
Model: MapModel,
|
||||
Controller: MapController,
|
||||
Renderer: MapRenderer,
|
||||
},
|
||||
});
|
||||
|
||||
- adding the view to the registry:
|
||||
As usual, the mapping between a view type and the actual class needs to be
|
||||
updated:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var viewRegistry = require('web.view_registry');
|
||||
|
||||
viewRegistry.add('map', MapView);
|
||||
|
||||
- implementing the four main classes:
|
||||
The ``View`` class needs to parse the ``arch`` field and setup the other
|
||||
three classes. The ``Renderer`` is in charge of representing the data in
|
||||
the user interface, the ``Model`` is supposed to talk to the server, to
|
||||
load data and process it. And the ``Controller`` is there to coordinate,
|
||||
to talk to the web client, ...
|
||||
|
||||
- creating some views in the database:
|
||||
.. code-block:: xml
|
||||
|
||||
<record id="customer_map_view" model="ir.ui.view">
|
||||
<field name="name">customer.map.view</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="arch" type="xml">
|
||||
<map latitude="partner_latitude" longitude="partner_longitude">
|
||||
<field name="name"/>
|
||||
</map>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
Customizing an existing view
|
||||
============================
|
||||
|
||||
Assume we need to create a custom version of a generic view. For example, a
|
||||
kanban view with some extra *ribbon-like* widget on top (to display some
|
||||
specific custom information). In that case, this can be done with 3 steps:
|
||||
extend the kanban view (which also probably mean extending controllers/renderers
|
||||
and/or models), then registering the view in the view registry, and finally,
|
||||
using the view in the kanban arch (a specific example is the helpdesk dashboard).
|
||||
|
||||
- extending a view:
|
||||
Here is what it could look like:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var HelpdeskDashboardRenderer = KanbanRenderer.extend({
|
||||
...
|
||||
});
|
||||
|
||||
var HelpdeskDashboardModel = KanbanModel.extend({
|
||||
...
|
||||
});
|
||||
|
||||
var HelpdeskDashboardController = KanbanController.extend({
|
||||
...
|
||||
});
|
||||
|
||||
var HelpdeskDashboardView = KanbanView.extend({
|
||||
config: _.extend({}, KanbanView.prototype.config, {
|
||||
Model: HelpdeskDashboardModel,
|
||||
Renderer: HelpdeskDashboardRenderer,
|
||||
Controller: HelpdeskDashboardController,
|
||||
}),
|
||||
});
|
||||
|
||||
- adding it to the view registry:
|
||||
as usual, we need to inform the web client of the mapping between the name
|
||||
of the views and the actual class.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var viewRegistry = require('web.view_registry');
|
||||
viewRegistry.add('helpdesk_dashboard', HelpdeskDashboardView);
|
||||
|
||||
- using it in an actual view:
|
||||
we now need to inform the web client that a specific ``ir.ui.view`` needs to
|
||||
use our new class. Note that this is a web client specific concern. From
|
||||
the point of view of the server, we still have a kanban view. The proper
|
||||
way to do this is by using a special attribute ``js_class`` (which will be
|
||||
renamed someday into ``widget``, because this is really not a good name) on
|
||||
the root node of the arch:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<record id="helpdesk_team_view_kanban" model="ir.ui.view" >
|
||||
...
|
||||
<field name="arch" type="xml">
|
||||
<kanban js_class="helpdesk_dashboard">
|
||||
...
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
.. note::
|
||||
|
||||
Note: you can change the way the view interprets the arch structure. However,
|
||||
from the server point of view, this is still a view of the same base type,
|
||||
subjected to the same rules (rng validation, for example). So, your views still
|
||||
need to have a valid arch field.
|
||||
|
||||
Promises and asynchronous code
|
||||
==============================
|
||||
|
||||
For a very good and complete introduction to promises, please read this excellent article https://github.com/getify/You-Dont-Know-JS/blob/1st-ed/async%20%26%20performance/ch3.md
|
||||
|
||||
Creating new Promises
|
||||
---------------------
|
||||
|
||||
- turn a constant into a promise
|
||||
There are 2 static functions on Promise that create a resolved or rejected promise based on a constant:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var p = Promise.resolve({blabla: '1'}); // creates a resolved promise
|
||||
p.then(function (result) {
|
||||
console.log(result); // --> {blabla: '1'};
|
||||
});
|
||||
|
||||
|
||||
var p2 = Promise.reject({error: 'error message'}); // creates a rejected promise
|
||||
p2.catch(function (reason) {
|
||||
console.log(reason); // --> {error: 'error message');
|
||||
});
|
||||
|
||||
|
||||
.. note:: Note that even if the promises are created already resolved or rejected, the `then` or `catch` handlers will still be called asynchronously.
|
||||
|
||||
|
||||
- based on an already asynchronous code
|
||||
Suppose that in a function you must do a rpc, and when it is completed set the result on this.
|
||||
The `this._rpc` is a function that returns a `Promise`.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
function callRpc() {
|
||||
var self = this;
|
||||
return this._rpc(...).then(function (result) {
|
||||
self.myValueFromRpc = result;
|
||||
});
|
||||
}
|
||||
|
||||
- for callback based function
|
||||
Suppose that you were using a function `this.close` that takes as parameter a callback that is called when the closing is finished.
|
||||
Now suppose that you are doing that in a method that must send a promise that is resolved when the closing is finished.
|
||||
|
||||
.. code-block:: javascript
|
||||
:linenos:
|
||||
|
||||
function waitForClose() {
|
||||
var self = this;
|
||||
return new Promise (function(resolve, reject) {
|
||||
self.close(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
* line 2: we save the `this` into a variable so that in an inner function, we can access the scope of our component
|
||||
* line 3: we create and return a new promise. The constructor of a promise takes a function as parameter. This function itself has 2 parameters that we called here `resolve` and `reject`
|
||||
- `resolve` is a function that, when called, puts the promise in the resolved state.
|
||||
- `reject` is a function that, when called, puts the promise in the rejected state. We do not use reject here and it can be omitted.
|
||||
* line 4: we are calling the function close on our object. It takes a function as parameter (the callback) and it happens that resolve is already a function, so we can pass it directly. To be clearer, we could have written:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
return new Promise (function (resolve) {
|
||||
self.close(function () {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
- creating a promise generator (calling one promise after the other *in sequence* and waiting for the last one)
|
||||
Suppose that you need to loop over an array, do an operation *in sequence* and resolve a promise when the last operation is done.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
function doStuffOnArray(arr) {
|
||||
var done = Promise.resolve();
|
||||
arr.forEach(function (item) {
|
||||
done = done.then(function () {
|
||||
return item.doSomethingAsynchronous();
|
||||
});
|
||||
});
|
||||
return done;
|
||||
}
|
||||
|
||||
This way, the promise you return is effectively the last promise.
|
||||
- creating a promise, then resolving it outside the scope of its definition (anti-pattern)
|
||||
.. note:: we do not recommend using this, but sometimes it is useful. Think carefully for alternatives first...
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
...
|
||||
var resolver, rejecter;
|
||||
var prom = new Promise(function (resolve, reject){
|
||||
resolver = resolve;
|
||||
rejecter = reject;
|
||||
});
|
||||
...
|
||||
|
||||
resolver("done"); // will resolve the promise prom with the result "done"
|
||||
rejecter("error"); // will reject the promise prom with the reason "error"
|
||||
|
||||
Waiting for Promises
|
||||
--------------------
|
||||
|
||||
- waiting for a number of Promises
|
||||
if you have multiple promises that all need to be waited, you can convert them into a single promise that will be resolved when all the promises are resolved using Promise.all(arrayOfPromises).
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var prom1 = doSomethingThatReturnsAPromise();
|
||||
var prom2 = Promise.resolve(true);
|
||||
var constant = true;
|
||||
|
||||
var all = Promise.all([prom1, prom2, constant]); // all is a promise
|
||||
// results is an array, the individual results correspond to the index of their
|
||||
// promise as called in Promise.all()
|
||||
all.then(function (results) {
|
||||
var prom1Result = results[0];
|
||||
var prom2Result = results[1];
|
||||
var constantResult = results[2];
|
||||
});
|
||||
return all;
|
||||
|
||||
|
||||
- waiting for a part of a promise chain, but not another part
|
||||
If you have an asynchronous process that you want to wait to do something, but you also want to return to the caller before that something is done.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
function returnAsSoonAsAsyncProcessIsDone() {
|
||||
var prom = AsyncProcess();
|
||||
prom.then(function (resultOfAsyncProcess) {
|
||||
return doSomething();
|
||||
});
|
||||
/* returns prom which will only wait for AsyncProcess(),
|
||||
and when it will be resolved, the result will be the one of AsyncProcess */
|
||||
return prom;
|
||||
}
|
||||
|
||||
Error handling
|
||||
--------------
|
||||
|
||||
- in general in promises
|
||||
The general idea is that a promise should not be rejected for control flow, but should only be rejected for errors.
|
||||
When that is the case, you would have multiple resolutions of your promise with, for instance status codes that you would have to check in the `then` handlers and a single `catch` handler at the end of the promise chain.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
function a() {
|
||||
x.y(); // <-- this is an error: x is undefined
|
||||
return Promise.resolve(1);
|
||||
}
|
||||
function b() {
|
||||
return Promise.reject(2);
|
||||
}
|
||||
|
||||
a().catch(console.log); // will log the error in a
|
||||
a().then(b).catch(console.log); // will log the error in a, the then is not executed
|
||||
b().catch(console.log); // will log the rejected reason of b (2)
|
||||
Promise.resolve(1)
|
||||
.then(b) // the then is executed, it executes b
|
||||
.then(...) // this then is not executed
|
||||
.catch(console.log); // will log the rejected reason of b (2)
|
||||
|
||||
|
||||
|
||||
- in Odoo specifically
|
||||
In Odoo, it happens that we use promise rejection for control flow, like in mutexes and other concurrency primitives defined in module `web.concurrency`
|
||||
We also want to execute the catch for *business* reasons, but not when there is a coding error in the definition of the promise or of the handlers.
|
||||
For this, we have introduced the concept of `guardedCatch`. It is called like `catch` but not when the rejected reason is an error
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
function blabla() {
|
||||
if (someCondition) {
|
||||
return Promise.reject("someCondition is truthy");
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
var promise = blabla();
|
||||
promise.then(function (result) { console.log("everything went fine"); })
|
||||
// this will be called if blabla returns a rejected promise, but not if it has an error
|
||||
promise.guardedCatch(function (reason) { console.log(reason); });
|
||||
|
||||
// ...
|
||||
|
||||
var anotherPromise =
|
||||
blabla().then(function () { console.log("everything went fine"); })
|
||||
// this will be called if blabla returns a rejected promise,
|
||||
// but not if it has an error
|
||||
.guardedCatch(console.log);
|
||||
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var promiseWithError = Promise.resolve().then(function () {
|
||||
x.y(); // <-- this is an error: x is undefined
|
||||
});
|
||||
promiseWithError.guardedCatch(function (reason) {console.log(reason);}); // will not be called
|
||||
promiseWithError.catch(function (reason) {console.log(reason);}); // will be called
|
||||
|
||||
|
||||
|
||||
Testing asynchronous code
|
||||
-------------------------
|
||||
|
||||
- using promises in tests
|
||||
In the tests code, we support the latest version of Javascript, including primitives like `async` and `await`. This makes using and waiting for promises very easy.
|
||||
Most helper methods also return a promise (either by being marked `async` or by returning a promise directly.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var testUtils = require('web.test_utils');
|
||||
QUnit.test("My test", async function (assert) {
|
||||
// making the function async has 2 advantages:
|
||||
// 1) it always returns a promise so you don't need to define `var done = assert.async()`
|
||||
// 2) it allows you to use the `await`
|
||||
assert.expect(1);
|
||||
|
||||
var form = await testUtils.createView({ ... });
|
||||
await testUtils.form.clickEdit(form);
|
||||
await testUtils.form.click('jquery selector');
|
||||
assert.containsOnce('jquery selector');
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("My test - no async - no done", function (assert) {
|
||||
// this function is not async, but it returns a promise.
|
||||
// QUnit will wait for for this promise to be resolved.
|
||||
assert.expect(1);
|
||||
|
||||
return testUtils.createView({ ... }).then(function (form) {
|
||||
return testUtils.form.clickEdit(form).then(function () {
|
||||
return testUtils.form.click('jquery selector').then(function () {
|
||||
assert.containsOnce('jquery selector');
|
||||
form.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
QUnit.test("My test - no async", function (assert) {
|
||||
// this function is not async and does not return a promise.
|
||||
// we have to use the done function to signal QUnit that the test is async and will be finished inside an async callback
|
||||
assert.expect(1);
|
||||
var done = assert.async();
|
||||
|
||||
testUtils.createView({ ... }).then(function (form) {
|
||||
testUtils.form.clickEdit(form).then(function () {
|
||||
testUtils.form.click('jquery selector').then(function () {
|
||||
assert.containsOnce('jquery selector');
|
||||
form.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
as you can see, the nicer form is to use `async/await` as it is clearer and shorter to write.
|
@ -46,4 +46,4 @@ developer/howtos/discover_js_framework/07_testing.rst developer/tutorials/discov
|
||||
# developer/reference/frontend
|
||||
|
||||
developer/reference/frontend/icons_library.rst contributing/development/ui/icons.rst # Odoo UI icons -> UI Icons
|
||||
|
||||
developer/reference/frontend/javascript_cheatsheet.rst developer/howtos/javascript_create_field.rst # refactor JavaScript cheatsheet into howtos
|
||||
|
Loading…
Reference in New Issue
Block a user