summaryrefslogtreecommitdiff
path: root/doc/reference/javascript_cheatsheet.rst
diff options
context:
space:
mode:
authorVictor Feyens <vfe@odoo.com>2021-04-30 13:24:57 +0000
committerAntoine Vandevenne (anv) <anv@odoo.com>2021-05-04 08:09:20 +0000
commit27bad8c3e32049ec69a588df0558e57a0b928309 (patch)
treeb60e430163d486c8c1fd052738c9854aeb8300f5 /doc/reference/javascript_cheatsheet.rst
parent75a122bbae3a32bfa79da3506179fc63a8bced5d (diff)
[MOV] doc: move documentation to a dedicated repository
In odoo/documentation-user#945, the developer documentation is merged with the user documentation in a single repository and build config. This commit then removes source files for the developer documentation from the odoo/odoo repository. DOC PR: https://github.com/odoo/documentation-user/pull/945 task-2351938 task-2352371 task-2205684 task-2352544 Co-authored-by: Victor Feyens <vfe@odoo.com> Co-authored-by: Antoine Vandevenne <anv@odoo.com>
Diffstat (limited to 'doc/reference/javascript_cheatsheet.rst')
-rw-r--r--doc/reference/javascript_cheatsheet.rst545
1 files changed, 0 insertions, 545 deletions
diff --git a/doc/reference/javascript_cheatsheet.rst b/doc/reference/javascript_cheatsheet.rst
deleted file mode 100644
index 1dfa1937d00..00000000000
--- a/doc/reference/javascript_cheatsheet.rst
+++ /dev/null
@@ -1,545 +0,0 @@
-:banner: banners/javascript.jpg
-
-.. _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 developers,
-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.