From 7e4435deb8ca089849e2622740a4a2ce7e9780b8 Mon Sep 17 00:00:00 2001 From: fdardenne Date: Fri, 10 Mar 2023 19:56:38 +0000 Subject: [PATCH] [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#3969 Signed-off-by: Dardenne Florent (dafl) --- content/developer/howtos.rst | 18 + .../howtos/javascript_client_action.rst | 46 ++ content/developer/howtos/javascript_field.rst | 107 ++++ content/developer/howtos/javascript_view.rst | 262 +++++++++ content/developer/reference/frontend.rst | 1 - .../frontend/javascript_cheatsheet.rst | 546 ------------------ redirects/16.0.txt | 2 +- 7 files changed, 434 insertions(+), 548 deletions(-) create mode 100644 content/developer/howtos/javascript_client_action.rst create mode 100644 content/developer/howtos/javascript_field.rst create mode 100644 content/developer/howtos/javascript_view.rst delete mode 100644 content/developer/reference/frontend/javascript_cheatsheet.rst diff --git a/content/developer/howtos.rst b/content/developer/howtos.rst index 8a2d04a08..9db5f6d46 100644 --- a/content/developer/howtos.rst +++ b/content/developer/howtos.rst @@ -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 diff --git a/content/developer/howtos/javascript_client_action.rst b/content/developer/howtos/javascript_client_action.rst new file mode 100644 index 000000000..43070991d --- /dev/null +++ b/content/developer/howtos/javascript_client_action.rst @@ -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 `, don't forget to + make it accessible. + + .. code-block:: xml + + + My Client Action + my_module.MyClientAction + + +#. 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` + + + + + Hello world + + diff --git a/content/developer/howtos/javascript_field.rst b/content/developer/howtos/javascript_field.rst new file mode 100644 index 000000000..75166dcdb --- /dev/null +++ b/content/developer/howtos/javascript_field.rst @@ -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` + + + + + + Late! + + + + +#. 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 + + + +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` + + `; + 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 + + diff --git a/content/developer/howtos/javascript_view.rst b/content/developer/howtos/javascript_view.rst new file mode 100644 index 000000000..eba0ed1a1 --- /dev/null +++ b/content/developer/howtos/javascript_view.rst @@ -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` + + + + + +
+ Hello world ! +
+
+
+
+ +#. Use the view with the `js_class` attribute in arch. + + .. code-block:: xml + + + + + + + + + +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` + + + + + + + + + + +#. 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` + + + + + + + // Show records + + + + +#. 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 + + ... + + ... diff --git a/content/developer/reference/frontend.rst b/content/developer/reference/frontend.rst index e58c62474..cf95a3841 100644 --- a/content/developer/reference/frontend.rst +++ b/content/developer/reference/frontend.rst @@ -15,7 +15,6 @@ JavaScript framework frontend/services frontend/hooks frontend/patching_code - frontend/javascript_cheatsheet frontend/javascript_reference frontend/mobile frontend/qweb diff --git a/content/developer/reference/frontend/javascript_cheatsheet.rst b/content/developer/reference/frontend/javascript_cheatsheet.rst deleted file mode 100644 index cc5d3a680..000000000 --- a/content/developer/reference/frontend/javascript_cheatsheet.rst +++ /dev/null @@ -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 - - - - 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 - - - customer.map.view - res.partner - - - - - - - - -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 - - - ... - - - ... - - - - -.. 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. diff --git a/redirects/16.0.txt b/redirects/16.0.txt index a1343cbdc..592e8c8f0 100644 --- a/redirects/16.0.txt +++ b/redirects/16.0.txt @@ -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