[IMP] tutorials/js_framework: reword, clarify and improve instructions

X-original-commit: b58dbb1fbb
Part-of: odoo/documentation#4281
This commit is contained in:
Antoine Vandevenne (anv) 2023-04-03 15:13:27 +00:00
parent 1ab6782608
commit 4815658419
4 changed files with 208 additions and 202 deletions

View File

@ -43,15 +43,15 @@ button.
import { Component, useState } from "@odoo/owl";
class Counter extends Component {
static template = "my_module.Counter";
static template = "my_module.Counter";
setup() {
state = useState({ value: 0 });
}
setup() {
state = useState({ value: 0 });
}
increment() {
this.state.value++;
}
increment() {
this.state.value++;
}
}
The `Counter` component specifies the name of the template to render. The template is written in XML
@ -80,9 +80,9 @@ route with your browser.
.. exercise::
#. Modify :file:`playground.js` so that it acts as a counter like in the example above. You will
need to use the `useState
<{OWL_PATH}/doc/reference/hooks.md#usestate>`_ function so that the component is re-rendered
whenever any part of the state object has been read by this component is modified.
need to use the `useState hook
<{OWL_PATH}/doc/reference/hooks.md#usestate>`_ so that the component is re-rendered
whenever any part of the state object that has been read by this component is modified.
#. In the same component, create an `increment` method.
#. Modify the template in :file:`playground.xml` so that it displays your counter variable. Use
`t-esc <{OWL_PATH}/doc/reference/templates.md#outputting-data>`_ to output the data.
@ -109,8 +109,8 @@ For now we have the logic of a counter in the `Playground` component, let us see
#. Extract the counter code from the `Playground` component into a new `Counter` component.
#. You can do it in the same file first, but once it's done, update your code to move the
`Counter` in its own file.
#. Make sure the template is in its own file, with the same name.
`Counter` in its own folder and file. Import it relatively from `./counter/counter`. Make sure
the template is in its own file, with the same name.
.. important::
Don't forget :code:`/** @odoo-module **/` in your JavaScript files. More information on this can
@ -129,7 +129,7 @@ todos. This will be done incrementally in multiple exercises that will introduce
**3. buy milk**.
#. Add the Bootstrap classes `text-muted` and `text-decoration-line-through` on the task if it is
done. To do that, you can use `dynamic attributes
<{OWL_PATH}/doc/reference/templates.md#dynamic-attributes>`_
<{OWL_PATH}/doc/reference/templates.md#dynamic-attributes>`_.
#. Modify :file:`owl_playground/static/src/playground.js` and
:file:`owl_playground/static/src/playground.xml` to display your new `Todo` component with
some hard-coded props to test it first.
@ -157,7 +157,7 @@ The `Todo` component has an implicit API. It expects to receive in its props the
todo object in a specified format: `id`, `description` and `done`. Let us make that API more
explicit. We can add a props definition that will let Owl perform a validation step in `dev mode
<{OWL_PATH}/doc/reference/app.md#dev-mode>`_. You can activate the dev mode in the `App
configuration <{OWL_PATH}/doc/reference/app.md#configuration>`_
configuration <{OWL_PATH}/doc/reference/app.md#configuration>`_.
It is a good practice to do props validation for every component.
@ -165,8 +165,9 @@ configuration <{OWL_PATH}/doc/reference/app.md#configuration>`_
#. Add `props validation <{OWL_PATH}/doc/reference/props.md#props-validation>`_ to the `Todo`
component.
#. Make sure it passes in dev mode which is activated by default in `owl_playground`. The dev
mode can be activated and deactivated by modifying the `dev` attribute in the in the `config`
#. Open the :guilabel:`Console` tab of your browser's dev tools and make sure the props
validation passes in dev mode, which is activated by default in `owl_playground`. The dev mode
can be activated and deactivated by modifying the `dev` attribute in the in the `config`
parameter of the `mount <{OWL_PATH}/doc/reference/app.md#mount-helper>`_ function in
:file:`owl_playground/static/src/main.js`.
#. Remove `done` from the props and reload the page. The validation should fail.
@ -179,8 +180,9 @@ list.
.. exercise::
#. Change the code to display a list of todos instead of just one, and use `t-foreach
<{OWL_PATH}/doc/reference/templates.md#loops>`_ in the template.
#. Change the code to display a list of todos instead of just one. Create a new `TodoList`
component to hold the `Todo` components and use `t-foreach
<{OWL_PATH}/doc/reference/templates.md#loops>`_ in its template.
#. Think about how it should be keyed with the `t-key` directive.
.. image:: 01_owl_components/todo_list.png
@ -197,17 +199,15 @@ a todo to the list.
#. Add an input above the task list with placeholder *Enter a new task*.
#. Add an `event handler <{OWL_PATH}/doc/reference/event_handling.md>`_ on the `keyup` event
named ``addTodo``.
named `addTodo`.
#. Implement `addTodo` to check if enter was pressed (:code:`ev.keyCode === 13`), and in that
case, create a new todo with the current content of the input as the description.
#. Make sure it has a unique id. It can be just a counter that increments at each todo.
#. Then, clear the input of all content.
case, create a new todo with the current content of the input as the description and clear the
input of all content.
#. Make sure the todo has a unique id. It can be just a counter that increments at each todo.
#. Wrap the todo list in a `useState` hook to let Owl know that it should update the UI when the
list is modified.
#. Bonus point: don't do anything if the input is empty.
.. note::
Notice that nothing updates in the UI: this is because Owl does not know that it should update
the UI. This can be fixed by wrapping the todo list in a `useState` hook.
.. code-block:: javascript
this.todos = useState([]);
@ -228,9 +228,10 @@ Let's see how we can access the DOM with `t-ref <{OWL_PATH}/doc/reference/refs.m
.. exercise::
#. Focus the `input` from the previous exercise when the dashboard is `mounted
<{OWL_PATH}/doc/reference/component.md#mounted>`_.
<{OWL_PATH}/doc/reference/component.md#mounted>`_. This this should be done from the
`TodoList` component.
#. Bonus point: extract the code into a specialized `hook <{OWL_PATH}/doc/reference/hooks.md>`_
`useAutofocus`.
`useAutofocus` in a new :file:`owl_playground/utils.js` file.
.. seealso::
`Owl: Component lifecycle <{OWL_PATH}/doc/reference/component.md#lifecycle>`_
@ -248,6 +249,11 @@ way to do this is by using a `callback prop
#. Add an input with the attribute :code:`type="checkbox"` before the id of the task, which must
be checked if the state `done` is true.
.. tip::
QWeb does not create attributes computed with the `t-att` directive it it evaluates to a
falsy value.
#. Add a callback props `toggleState`.
#. Add a `click` event handler on the input in the `Todo` component and make sure it calls the
`toggleState` function with the todo id.
@ -265,12 +271,12 @@ The final touch is to let the user delete a todo.
.. exercise::
#. Add a new callback prop `removeTodo`.
#. Insert :code:`<span class="fa fa-remove">` in the template of the `Todo` component.
#. Insert :code:`<span class="fa fa-remove"/>` in the template of the `Todo` component.
#. Whenever the user clicks on it, it should call the `removeTodo` method.
.. tip::
If you're using an array to store your todo list, you can use the JavaScript `splice` function
to remove a todo from it.
.. tip::
If you're using an array to store your todo list, you can use the JavaScript `splice`
function to remove a todo from it.
.. code-block::
@ -285,15 +291,18 @@ The final touch is to let the user delete a todo.
:scale: 70%
:align: center
10. Generic components with slots
=================================
.. _tutorials/discover_js_framework/generic_card:
10. Generic card with slots
===========================
Owl has a powerful `slot <{OWL_PATH}/doc/reference/slots.md>`_ system to allow you to write generic
components. This is useful to factorize the common layout between different parts of the interface.
.. exercise::
#. Write a `Card` component using the following Bootstrap HTML structure:
#. Insert a new `Card` component between the `Counter` and `Todolist` components. Use the
following Bootstrap HTML structure for the card:
.. code-block:: html
@ -310,18 +319,15 @@ components. This is useful to factorize the common layout between different part
</div>
#. This component should have two slots: one slot for the title, and one for the content (the
default slot).
default slot). It should be possible to use the `Card` component as follows:
.. example::
Here is how one could use it:
.. code-block:: html
.. code-block:: html
<Card>
<t t-set-slot="title">Card title</t>
<p class="card-text">Some quick example text...</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</Card>
<Card>
<t t-set-slot="title">Card title</t>
<p class="card-text">Some quick example text...</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</Card>
#. Bonus point: if the `title` slot is not given, the `h5` should not be rendered at all.
@ -332,8 +338,8 @@ components. This is useful to factorize the common layout between different part
.. seealso::
`Bootstrap: documentation on cards <https://getbootstrap.com/docs/5.2/components/card/>`_
11. Go further
==============
11. Extensive props validation
==============================
.. exercise::

View File

@ -21,7 +21,7 @@ about the Odoo JavaScript framework in its entirety, as used by the web client.
:width: 50%
For this chapter, we will start from the empty dashboard provided by the `awesome_tshirt`
addon. We will progressively add features to it, using the odoo framework.
addon. We will progressively add features to it, using the Odoo JavaScript framework.
.. admonition:: Goal
@ -38,14 +38,18 @@ addon. We will progressively add features to it, using the odoo framework.
===============
Most screens in the Odoo web client uses a common layout: a control panel on top, with some buttons,
and a main content zone just below. This is done using a `Layout component
and a main content zone just below. This is done using the `Layout component
<{GITHUB_PATH}/addons/web/static/src/search/layout.js>`_, available in `@web/search/layout`.
.. exercise::
Update the `AwesomeDashboard` component located in :file:`awesome_tshirt/static/src/` to use the
`Layout` component. You can use :code:`{ "top-right": false, "bottom-right": false }` for the
`display` props of the `Layout` component.
`Layout` component. You can use
:code:`{controlPanel: { "top-right": false, "bottom-right": false } }` for the `display` props of
the `Layout` component.
Open http://localhost:8069/web, then open the :guilabel:`Awesome T-Shirts` app, and see the
result.
.. image:: 02_web_framework/new_layout.png
:align: center
@ -61,12 +65,11 @@ and a main content zone just below. This is done using a `Layout component
2. Add some buttons for quick navigation
========================================
Bafien Carpink want buttons for easy access to common views in Odoo. In order to do that, you will
need to use the action service.
Let us now use the action service for an easy access to the common views in Odoo.
:ref:`Services <frontend/services>` is a notion defined by the Odoo JavaScript framework, it is a
persistent piece of code that exports state and/or functions. Each service can depend on other
services, and components can import a service with the `useService()` hooks.
:ref:`Services <frontend/services>` is a notion defined by the Odoo JavaScript framework; it is a
persistent piece of code that exports a state and/or functions. Each service can depend on other
services, and components can import a service with the `useService()` hook.
.. example::
@ -92,9 +95,17 @@ services, and components can import a service with the `useService()` hooks.
exists, so you should use `its xml id
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
odoo/addons/base/views/res_partner_views.xml#L525>`_).
#. A button `New Orders`, which opens a list view with all orders created in the last 7 days.
#. A button `New Orders`, which opens a list view with all orders created in the last 7 days. Use
the `Domain <https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
odoo/addons/web/static/src/core/domain.js#L19>`_ helper class to represent the domain.
.. tip::
One way to represent the desired domain could be
:code:`[('create_date','>=', (context_today() - datetime.timedelta(days=7)).strftime('%Y-%m-%d'))]`
#. A button `Cancelled Order`, which opens a list of all orders created in the last 7 days, but
already cancelled.
already cancelled. Rather than defining the action twice, factorize it in a new `openOrders`
method.
.. image:: 02_web_framework/navigation_buttons.png
:align: center
@ -109,9 +120,10 @@ services, and components can import a service with the `useService()` hooks.
3. Call the server, add some statistics
=======================================
Let's improve the dashboard by adding a few cards (see the `Card` component made in the Owl
training) containing a few statistics. There is a route `/awesome_tshirt/statistics` that will
perform some computations and return an object containing some useful information.
Let's improve the dashboard by adding a few cards (see the `Card` component :ref:`made in the
previous chapter <tutorials/discover_js_framework/generic_card>`) containing a few statistics. There
is a route `/awesome_tshirt/statistics` that performs some computations and returns an object
containing some useful information.
Whenever we need to call a specific controller, we need to use the :ref:`rpc service
<frontend/services/rpc>`. It only exports a single function that perform the request:
@ -162,11 +174,11 @@ Here is a short explanation on the various arguments:
4. Cache network calls, create a service
========================================
If you open your browser dev tools, in the network tabs, you will probably see that the call to
If you open the :guilabel:`Network` tab of your browser's dev tools, you will see that the call to
`/awesome_tshirt/statistics` is done every time the client action is displayed. This is because the
`onWillStart` hook is called each time the `Dashboard` component is mounted. But in this case, we
would probably prefer to do it only the first time, so we actually need to maintain some state
outside of the `Dashboard` component. This is a nice use case for a service!
would prefer to do it only the first time, so we actually need to maintain some state outside of the
`Dashboard` component. This is a nice use case for a service!
.. example::
@ -188,13 +200,12 @@ outside of the `Dashboard` component. This is a nice use case for a service!
.. exercise::
#. Implements a new `awesome_tshirt.statistics` service.
#. Register and import a new `awesome_tshirt.statistics` service.
#. It should provide a function `loadStatistics` that, once called, performs the actual rpc, and
always return the same information.
#. Maybe use the `memoize
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
#. Use the `memoize <https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
addons/web/static/src/core/utils/functions.js#L11>`_ utility function from
`@web/core/utils/functions`
`@web/core/utils/functions` that will allow caching the statistics.
#. Use this service in the `Dashboard` component.
#. Check that it works as expected
@ -206,13 +217,13 @@ outside of the `Dashboard` component. This is a nice use case for a service!
5. Display a pie chart
======================
Everyone likes charts (!), so let us add a pie chart in our dashboard, which displays the
Everyone likes charts (!), so let us add a pie chart in our dashboard. It will display the
proportions of t-shirts sold for each size: S/M/L/XL/XXL.
For this exercise, we will use `Chart.js <https://www.chartjs.org/>`_. It is the chart library used
by the graph view. However, it is not loaded by default, so we will need to either add it to our
assets bundle, or lazy load it (it's usually better since our users will not have to load the
chartjs code every time if they don't need it).
assets bundle, or lazy load it. Lazy loading is usually better since our users will not have to load
the chartjs code every time if they don't need it.
.. exercise::
#. Load chartjs, you can use the `loadJs

View File

@ -99,31 +99,28 @@ views (namely: `form`, `list`, `kanban`) by using the `widget` attribute.
<field name="preview_moves" widget="account_resequence_widget"/>
.. _tutorials/master_odoo_web_framework/image_preview_field:
1. An `image_preview` field
===========================
Each new order on the website will be created as an `awesome_tshirt.order`. This model has a
`image_url` field (of type `char`), which is currently only visible as a string. We want to be able
to see it in the form view.
For this task, we need to create a new field component `image_preview`. This component is
specified as follows: In readonly mode, it is only an image tag with the correct `src` if the field
is set; In edit mode, it also behaves like classical `char` fields (you can use the `CharField` in
your template by passing it in the props). An `input` should be displayed with the text value of the
field, so it can be edited.
to see the image itself in the form view.
.. exercise::
#. Create a new `ImagePreview` component and use the `CharField` component in your template. You
can use `t-props
<{OWL_PATH}/doc/reference/props.md#dynamic-props>`_ to pass props
received by `ImagePreview` to `CharField`.
#. Register your field in the proper :ref:`registry <frontend/registries>`.
#. Update the arch of the form view to use your new field by setting the `widget` attribute.
#. Create a new `ImagePreview` component and register it in the proper :ref:`registry
<frontend/registries>`. Use the `CharField` component in your template. You can use `t-props
<{OWL_PATH}/doc/reference/props.md#dynamic-props>`_ to pass props received by `ImagePreview`
to `CharField`. Update the arch of the form view to use your new field by setting the `widget`
attribute.
#. Change the code of the `ImagePreview` component so that the image is displayed below the URL.
#. When the field is readonly, only the image should be displayed and the URL should be hidden.
.. note::
It is possible to solve this exercise by inheriting `CharField` , but the goal of this exercise
is to create a field from scratch.
It is possible to solve this exercise by inheriting `CharField`, but the goal of this exercise is
to create a field from scratch.
.. image:: 01_fields_and_views/image_field.png
:align: center
@ -136,11 +133,12 @@ field, so it can be edited.
2. Improving the `image_preview` field
======================================
We want to improve the field of the previous task to help the staff recognize orders for which some
action should be done.
.. exercise::
We want to improve the field of the previous task to help the staff recognize orders for which
some action should be done. In particular, we want to display a warning "MISSING TSHIRT DESIGN"
in red if there is no image URL specified on the order.
Display a warning "MISSING TSHIRT DESIGN" in red if there is no image URL specified on the order.
.. image:: 01_fields_and_views/missing_image.png
:align: center
@ -150,9 +148,9 @@ field, so it can be edited.
Let's see how to use inheritance to extend an existing component.
There is a `is_late`, readonly, boolean field on the task model. That would be useful information to
see on the list/kanban/view. Then, let us say that we want to add a red word "Late!" next to it
whenever it is set to true.
There is a `is_late`, readonly, boolean field on the order model. That would be useful information
to see on the list/kanban/view. Then, let us say that we want to add a red word "Late!" next to it
whenever it is set to `true`.
.. exercise::
@ -166,11 +164,8 @@ whenever it is set to true.
:align: center
.. seealso::
- `Example: A field inheriting another (JS)
<{GITHUB_PATH}/addons/account/static/src/components/account_type_selection/account_type_selection.js>`_
- `Example: A field inheriting another (XML)
<{GITHUB_PATH}/addons/account/static/src/components/account_type_selection/account_type_selection.xml>`_
- `Example: A field inheriting another
<{GITHUB_PATH}/addons/account/static/src/components/account_type_selection>`_
- :ref:`Documentation on xpath <reference/views/inheritance>`
4. Message for some customers
@ -181,14 +176,17 @@ insert arbitrary components in the form view. Let us see how we can use it.
.. exercise::
For a super efficient workflow, we would like to display a message/warning box with some
information in the form view, with specific messages depending on some conditions:
For a super efficient workflow, we would like to display an alert block with specific messages
depending on some conditions:
- If the `image_url` field is not set, it should display "No image".
- If the amount of the order is higher than 100 euros, it should display "Add promotional
- If the `amount` of the order is higher than 100 euros, it should display "Add promotional
material".
- Make sure that your widget is updated in real time.
.. tip::
Try to evaluate `props.record` in the :guilabel:`Console` tab of your browser's dev tools.
.. image:: 01_fields_and_views/warning_widget.png
:align: center
@ -197,26 +195,26 @@ insert arbitrary components in the form view. Let us see how we can use it.
- `Example: Using the tag <widget> in a form view
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
addons/calendar/views/calendar_views.xml#L197>`_
- `Example: Implementation of a widget (JS)
<{GITHUB_PATH}/addons/web/static/src/views/widgets/week_days/week_days.js>`_
- `Example: Implementation of a widget (XML)
<{GITHUB_PATH}/addons/web/static/src/views/widgets/week_days/week_days.xml>`_
- `Example: Implementation of a widget
<{GITHUB_PATH}/addons/web/static/src/views/widgets/week_days>`_
5. Use `markup`
===============
Let's see how we can display raw HTML in a template. Before, there was a `t-raw` directive that
would just output anything as HTML. This was unsafe, and has been replaced by a `t-out
<{OWL_PATH}/doc/reference/templates.md#outputting-data>`_ directive that acts like a `t-esc` unless
the data has been marked explicitly with a `markup` function.
Lets see how we can display raw HTML in a template. The `t-out` directive can be used for that
propose. Indeed, `it generally acts like t-esc, unless the data has been marked explicitly with a
markup function <{OWL_PATH}/doc/reference/templates.md#outputting-data>`_. In that case, its value
is injected as HTML.
.. exercise::
#. Modify the previous exercise to put the `image` and `material` words in bold.
#. The warnings should be markuped, and the template should be modified to use `t-out`.
#. Import the `markup` function from Owl and, for each message, replace it with a call of the
function with the message passed as argument.
.. note::
This is an example of a safe use of `t-out` , since the string is static.
This is an example of a safe use of `t-out`, since the string is static.
.. image:: 01_fields_and_views/warning_widget2.png
:align: center
@ -311,18 +309,19 @@ The view description can define a `props` function, which receives the standard
the base props of the concrete view. The `props` function is executed only once, and can be thought
of as being some kind of factory. It is useful to parse the `arch` XML document, and to allow the
view to be parameterized (for example, it can return a Renderer component that will be used as
Renderer), but then it makes it easy to customize the specific renderer used by a sub view.
Renderer). Then, it is easy to customize the specific renderer used by a sub view.
These props will be extended before being given to the Controller. In particular, the search props
The props will be extended before being given to the Controller. In particular, the search props
(domain/context/groupby) will be added.
Then, the root component, commonly called the `Controller`, coordinates everything. It uses the
Finally, the root component, commonly called the `Controller`, coordinates everything. It uses the
generic `Layout` component (to add a control panel), instantiates a `Model`, and uses a `Renderer`
component in the `Layout` default slot. The `Model` is tasked with loading and updating data, and
the `Renderer` is supposed to handle all rendering work, along with all user interactions.
In practice, once the t-shirt order is printed, we need to print a label to put on the package. To
do that, let us add a button in the order form view control panel which will call a model method.
do that, let us add a button in the order's form view's control panel, which will call a model
method.
There is a service dedicated to calling models methods: `orm_service`, located in
`core/orm_service.js`. It provides a way to call common model methods, as well as a generic
@ -343,18 +342,27 @@ There is a service dedicated to calling models methods: `orm_service`, located i
.. exercise::
#. Create a customized form view extending the web form view and register it as
#. Create a customized form view extending the `web` form view and register it as
`awesome_tshirt.order_form_view`.
#. Add a `js_class` attribute to the arch of the form view so Odoo will load it.
#. Create a new template inheriting from the form controller template to add a button after the
create button.
#. Add a button. Clicking on this button should call the method `print_label` from the model
`awesome_tshirt.order` with the proper id. Note: `print_label` is a mock method, it only
displays a message in the logs.
#. The button should be disabled if the current order is in `create` mode (i.e., it does not
#. Add a `js_class="awesome_tshirt.order_form_view"` attribute to the arch of the form view so
that Odoo will load it.
#. Create a new template inheriting from the form controller template and add a "Print Label"
button after the "New" button.
#. Clicking on this button should call the method `print_label` from the model
`awesome_tshirt.order` with the proper id.
.. note::
`print_label` is a mock method; it only displays a message in the logs.
#. The button should not be disabled if the current order is in `create` mode (i.e., it does not
exist yet).
.. tip::
Log `this.props.resId` and `this.model.root.resId` and compare the two values before and
after entering `create` mode.
#. The button should be displayed as a primary button if the customer is properly set and if the
task stage is `printed`. Otherwise, it is displayed as a secondary button.
task stage is `printed`. Otherwise, it should be displayed as a secondary button.
#. Bonus point: clicking twice on the button should not trigger 2 RPCs.
.. image:: 01_fields_and_views/form_button.png
@ -371,6 +379,9 @@ There is a service dedicated to calling models methods: `orm_service`, located i
- `Code: orm service <{GITHUB_PATH}/addons/web/static/src/core/orm_service.js>`_
- `Example: Using the orm service
<{GITHUB_PATH}/addons/account/static/src/components/open_move_widget/open_move_widget.js>`_
- `Code: useDebounced hook
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
addons/web/static/src/core/utils/timing.js#L117>`_
7. Auto-reload the kanban view
==============================

View File

@ -67,7 +67,7 @@ printer is not connected or ran out of paper).
:scale: 60%
.. seealso::
`Example: Code using the notification service
`Example: Using the notification service
<{GITHUB_PATH}/addons/web/static/src/views/fields/image_url/image_url_field.js>`_
2. Add a systray item
@ -107,7 +107,6 @@ do that by calling periodically (for example, every minute) the server to reload
.. exercise::
#. Modify the systray item code to get its data from the `tshirt` service.
#. The `tshirt` service should periodically reload its data.
Now, the question arises: how is the systray item notified that it should re-render itself? It can
@ -115,11 +114,12 @@ be done in various ways but, for this training, we choose to use the most *decla
.. exercise::
#. Modify the `tshirt` service to return a `reactive
2. Modify the `tshirt` service to return a `reactive
<{OWL_PATH}/doc/reference/reactivity.md#reactive>`_ object. Reloading data should update the
reactive object in place.
#. The systray item can then perform a `useState` on the service return value.
#. This is not really necessary, but you can also *package* the calls to `useService` and
3. The systray item can then perform a `useState
<{OWL_PATH}/doc/reference/reactivity.md#usestate>`_ on the service return value.
4. This is not really necessary, but you can also *package* the calls to `useService` and
`useState` in a custom hook `useStatistics`.
.. seealso::
@ -137,48 +137,45 @@ by pressing `CTRL+K` in the Odoo interface.
.. exercise::
Let us modify the image preview field (from a previous exercise) to add a command to the command
palette to open the image in a new browser tab (or window).
Modify the :ref:`image preview field <tutorials/master_odoo_web_framework/image_preview_field>`
to add a command to the command palette to open the image in a new browser tab (or window).
Make sure that the command is only active whenever a field preview is visible in the screen.
Ensure the command is only active whenever a field preview is visible on the screen.
.. image:: 02_miscellaneous/new_command.png
:align: center
.. seealso::
- `Example: Using the useCommand hook
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
addons/web/static/src/core/debug/debug_menu.js#L15>`_
- `Code: The command service
<{GITHUB_PATH}/addons/web/static/src/core/commands/command_service.js>`_
`Example: Using the useCommand hook
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
addons/web/static/src/core/debug/debug_menu.js#L15>`_
5. Monkey patching a component
==============================
Often, it is possible to do what we want by using existing extension points that allow
customization, such as registering something in a registry. But it happens that we want to modify
something that has no such mechanism. In that case, we have to fall back on a less safe form of
Often, we can achieve what we want by using existing extension points that allow for customization,
such as registering something in a registry. Sometimes, however, it happens that we want to modify
something that has no such mechanism. In that case, we must fall back on a less safe form of
customization: monkey patching. Almost everything in Odoo can be monkey patched.
Bafien, our beloved leader, heard that employees perform better if they are constantly being
watched. Since he is not able to be there in person for each and every one of his employees, he
tasked you with the following: update the user interface to add a blinking red eye in the control
panel. Clicking on that eye should open a dialog with the following message: "Bafien is watching
you. This interaction is recorded and may be used in legal proceedings if necessary. Do you agree to
these terms?".
Bafien, our beloved leader, heard about employees performing better if they are constantly being
watched. Since he cannot be there in person for each of his employees, he tasked you with updating
the user interface to add a blinking red eye in the control panel. Clicking on that eye should open
a dialog with the following message: "Bafien is watching you. This interaction is recorded and may
be used in legal proceedings if necessary. Do you agree to these terms?"
.. exercise::
#. Create the :file:`control_panel_patch.js` file, as well as corresponding CSS and XML files.
#. :doc:`Patch </developer/reference/frontend/patching_code>` the `ControlPanel` template to add
an icon next to the breadcrumbs. You might want to use the `fa-eye` or `fa-eyes` icons. Make
sure it is visible in all views!
.. tip::
There are two ways to inherit a template using XPath: by specifying
`t-inherit-mode="primary"`, which creates a new, independent template with the desired
modifications, or by using `t-inherit-mode="extension"`, which modifies the original
template in place.
#. :ref:`Inherit <reference/qweb/template_inheritance>` the `web.Breadcrumbs` template of the
`ControlPanel component <{GITHUB_PATH}/addons/web/static/src/search/control_panel>`_ to add an
icon next to the breadcrumbs. You might want to use the `fa-eye` or `fa-eyes` icons.
#. :doc:`Patch </developer/reference/frontend/patching_code>` the component to display the
message on click by using `the dialog service
<{GITHUB_PATH}/addons/web/static/src/core/dialog/dialog_service.js>`_. You can use
`ConfirmationDialog
<{GITHUB_PATH}/addons/web/static/src/core/confirmation_dialog/confirmation_dialog.js>`_.
#. Add the CSS class `blink` to the element representing the eye and paste the following code in
a new CSS file located in your patch's directory.
.. code-block:: css
@ -197,10 +194,6 @@ these terms?".
}
}
#. Import the ControlPanel component and the `patch` function.
#. Update the code to display the message on click by using the dialog service. You can use
`ConfirmationDialog`.
.. image:: 02_miscellaneous/bafien_eye.png
:align: center
:scale: 60%
@ -213,21 +206,10 @@ these terms?".
- `Code: The patch function
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
addons/web/static/src/core/utils/patch.js#L16>`_
- `Code: The ControlPanel component
<{GITHUB_PATH}/addons/web/static/src/search/control_panel/control_panel.js>`_
- `The Font Awesome website <https://fontawesome.com/>`_
- `Code: The dialog service <{GITHUB_PATH}/addons/web/static/src/core/dialog/dialog_service.js>`_
- `Code: ConfirmationDialog
<{GITHUB_PATH}/addons/web/static/src/core/confirmation_dialog/confirmation_dialog.js>`_
- `Example: Using the dialog service
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
addons/board/static/src/board_controller.js#L88>`_
- `Example: XPath with t-inherit-mode="primary"
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
addons/account/static/src/components/account_move_form/account_move_form_notebook.xml#L4>`_
- `Example: XPath with t-inherit-mode="extension"
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
calendar/static/src/components/activity/activity.xml#L4>`_
6. Fetching orders from a customer
==================================
@ -240,33 +222,28 @@ from a given customer.
#. Update :file:`tshirt_service.js` to add a `loadCustomers` method, which returns a promise that
returns the list of all customers (and only performs the call once).
#. Import the `AutoComplete` component from `@web/core/autocomplete/autocomplete`.
#. Add it to the dashboard, next to the buttons in the control panel.
#. Update the code to fetch the list of customers with the tshirt service, and display it in the
autocomplete component, filtered by the `fuzzyLookup` method.
#. Add the `AutoComplete component <{GITHUB_PATH}/addons/web/static/src/core/autocomplete>`_ to
the dashboard, next to the buttons in the control panel.
#. Fetch the list of customers with the tshirt service, and display it in the AutoComplete
component, filtered by the `fuzzyLookup
<{GITHUB_PATH}/addons/web/static/src/core/utils/search.js>`_ method.
.. image:: 02_miscellaneous/autocomplete.png
:align: center
:scale: 60%
.. seealso::
- `Code: AutoComplete <{GITHUB_PATH}/addons/web/static/src/core/autocomplete/autocomplete.js>`_
- `Code: fuzzyLookup <{GITHUB_PATH}/addons/web/static/src/core/utils/search.js>`_
7. Reintroduce Kitten Mode
==========================
Let us add a special mode to Odoo: whenever the url contains `kitten=1`, we will display a kitten in
Let us add a special mode to Odoo: whenever the URL contains `kitten=1`, we will display a kitten in
the background of Odoo, because we like kittens.
.. exercise::
#. Create a :file:`kitten_mode.js` file.
#. Create a `kitten` service, which should check the content of the active url hash with the
help of the :ref:`router service <frontend/services/router>`.
#. If `kitten` is set, we are in kitten mode. This should add a class `.o-kitten-mode` on the
document body.
#. Add the following CSS in :file:`kitten_mode.scss`:
#. Create a `kitten` service, which should check the content of the active URL hash with the
help of the :ref:`router service <frontend/services/router>`. If `kitten` is set in the URL,
add the class `o-kitten-mode` to the document body.
#. Add the following SCSS in :file:`kitten_mode.scss`:
.. code-block:: css
@ -281,7 +258,7 @@ the background of Odoo, because we like kittens.
}
#. Add a command to the command palette to toggle the kitten mode. Toggling the kitten mode
should toggle the `.o-kitten-mode` class and update the current URL accordingly.
should toggle the class `o-kitten-mode` and update the current URL accordingly.
.. image:: 02_miscellaneous/kitten_mode.png
:align: center
@ -289,10 +266,10 @@ the background of Odoo, because we like kittens.
8. Lazy loading our dashboard
=============================
This is not really necessary, but the exercise is interesting. Imagine that our awesome dashboard
is a large application, with potentially multiple external libraries, lots of code/styles/templates.
Also, suppose that the dashboard is only used by some users in some business flows, so we want to
lazy load it in order to speed up the loading of the web client in most cases.
This is not really necessary, but the exercise is interesting. Imagine that our awesome dashboard is
a large application with potentially multiple external libraries and lots of code/styles/templates.
Also, suppose that the dashboard is used only by some users in some business flows. It would be
interesting to lazy load it in order to speed up the loading of the web client in most cases.
So, let us do that!
@ -300,22 +277,23 @@ So, let us do that!
#. Modify the manifest to create a new :ref:`bundle <reference/assets_bundle>`
`awesome_tshirt.dashboard`.
#. Add the awesome dashboard code to this bundle. If needed you can create folders and move
files.
#. Remove the code from the `web.assets_backend` bundle so it is not loaded twice.
#. Add the awesome dashboard code to this bundle. Create folders and move files if needed.
#. Remove the code from the `web.assets_backend` bundle so that it is not loaded twice.
So far, we removed the dashboard from the main bundle, but it should now be lazily loaded. Right
now, there is no client action registered in the action registry.
So far, we only removed the dashboard from the main bundle; we now want to lazy load it. Currently,
no client action is registered in the action registry.
.. exercise::
#. Create a new file :file:`dashboard_loader.js`.
#. Copy the code registering `AwesomeDashboard` to the dashboard loader.
#. Register `AwesomeDashboard` as a `LazyComponent`.
#. Modify the code in the dashboard loader to use the lazy component `AwesomeDashboard`.
4. Create a new file :file:`dashboard_loader.js`.
5. Copy the code registering `AwesomeDashboard` to the dashboard loader.
6. Register `AwesomeDashboard` as a `LazyComponent
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
addons/web/static/src/core/assets.js#L265-L282>`_.
7. Modify the code in the dashboard loader to use the lazy component `AwesomeDashboard`.
If you open the :guilabel:`Network` tab of your browser's dev tools, you should see that
:file:`awesome_tshirt.dashboard.min.js` is now loaded only when the Dashboard is first accessed.
.. seealso::
- :ref:`Documentation on assets <reference/assets>`
- `Code: LazyComponent
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
addons/web/static/src/core/assets.js#L255>`_
:ref:`Documentation on assets <reference/assets>`