[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#4016

X-original-commit: 7e4435deb8
Signed-off-by: Dardenne Florent (dafl) <dafl@odoo.com>
This commit is contained in:
fdardenne
2023-03-10 19:56:38 +00:00
parent 5df2d0d186
commit 79af1733d8
7 changed files with 434 additions and 548 deletions
@@ -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>
@@ -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"/>
@@ -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"/>
...