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

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

View File

@ -80,9 +80,9 @@ route with your browser.
.. exercise:: .. exercise::
#. Modify :file:`playground.js` so that it acts as a counter like in the example above. You will #. Modify :file:`playground.js` so that it acts as a counter like in the example above. You will
need to use the `useState need to use the `useState hook
<{OWL_PATH}/doc/reference/hooks.md#usestate>`_ function so that the component is re-rendered <{OWL_PATH}/doc/reference/hooks.md#usestate>`_ so that the component is re-rendered
whenever any part of the state object has been read by this component is modified. whenever any part of the state object that has been read by this component is modified.
#. In the same component, create an `increment` method. #. In the same component, create an `increment` method.
#. Modify the template in :file:`playground.xml` so that it displays your counter variable. Use #. 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. `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. #. 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 #. 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. `Counter` in its own folder and file. Import it relatively from `./counter/counter`. Make sure
#. Make sure the template is in its own file, with the same name. the template is in its own file, with the same name.
.. important:: .. important::
Don't forget :code:`/** @odoo-module **/` in your JavaScript files. More information on this can 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**. **3. buy milk**.
#. Add the Bootstrap classes `text-muted` and `text-decoration-line-through` on the task if it is #. 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 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 #. Modify :file:`owl_playground/static/src/playground.js` and
:file:`owl_playground/static/src/playground.xml` to display your new `Todo` component with :file:`owl_playground/static/src/playground.xml` to display your new `Todo` component with
some hard-coded props to test it first. 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 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 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 <{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. 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` #. Add `props validation <{OWL_PATH}/doc/reference/props.md#props-validation>`_ to the `Todo`
component. component.
#. Make sure it passes in dev mode which is activated by default in `owl_playground`. The dev #. Open the :guilabel:`Console` tab of your browser's dev tools and make sure the props
mode can be activated and deactivated by modifying the `dev` attribute in the in the `config` 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 parameter of the `mount <{OWL_PATH}/doc/reference/app.md#mount-helper>`_ function in
:file:`owl_playground/static/src/main.js`. :file:`owl_playground/static/src/main.js`.
#. Remove `done` from the props and reload the page. The validation should fail. #. Remove `done` from the props and reload the page. The validation should fail.
@ -179,8 +180,9 @@ list.
.. exercise:: .. exercise::
#. Change the code to display a list of todos instead of just one, and use `t-foreach #. Change the code to display a list of todos instead of just one. Create a new `TodoList`
<{OWL_PATH}/doc/reference/templates.md#loops>`_ in the template. 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. #. Think about how it should be keyed with the `t-key` directive.
.. image:: 01_owl_components/todo_list.png .. 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 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 #. 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 #. 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. case, create a new todo with the current content of the input as the description and clear the
#. Make sure it has a unique id. It can be just a counter that increments at each todo. input of all content.
#. Then, 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. #. 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 .. code-block:: javascript
this.todos = useState([]); 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:: .. exercise::
#. Focus the `input` from the previous exercise when the dashboard is `mounted #. 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>`_ #. 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:: .. seealso::
`Owl: Component lifecycle <{OWL_PATH}/doc/reference/component.md#lifecycle>`_ `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 #. 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. 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 callback props `toggleState`.
#. Add a `click` event handler on the input in the `Todo` component and make sure it calls the #. Add a `click` event handler on the input in the `Todo` component and make sure it calls the
`toggleState` function with the todo id. `toggleState` function with the todo id.
@ -265,12 +271,12 @@ The final touch is to let the user delete a todo.
.. exercise:: .. exercise::
#. Add a new callback prop `removeTodo`. #. 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. #. Whenever the user clicks on it, it should call the `removeTodo` method.
.. tip:: .. tip::
If you're using an array to store your todo list, you can use the JavaScript `splice` function If you're using an array to store your todo list, you can use the JavaScript `splice`
to remove a todo from it. function to remove a todo from it.
.. code-block:: .. code-block::
@ -285,15 +291,18 @@ The final touch is to let the user delete a todo.
:scale: 70% :scale: 70%
:align: center :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 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. components. This is useful to factorize the common layout between different parts of the interface.
.. exercise:: .. 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 .. code-block:: html
@ -310,10 +319,7 @@ components. This is useful to factorize the common layout between different part
</div> </div>
#. This component should have two slots: one slot for the title, and one for the content (the #. 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
@ -332,8 +338,8 @@ components. This is useful to factorize the common layout between different part
.. seealso:: .. seealso::
`Bootstrap: documentation on cards <https://getbootstrap.com/docs/5.2/components/card/>`_ `Bootstrap: documentation on cards <https://getbootstrap.com/docs/5.2/components/card/>`_
11. Go further 11. Extensive props validation
============== ==============================
.. exercise:: .. exercise::

View File

@ -21,7 +21,7 @@ about the Odoo JavaScript framework in its entirety, as used by the web client.
:width: 50% :width: 50%
For this chapter, we will start from the empty dashboard provided by the `awesome_tshirt` 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 .. 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, 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`. <{GITHUB_PATH}/addons/web/static/src/search/layout.js>`_, available in `@web/search/layout`.
.. exercise:: .. exercise::
Update the `AwesomeDashboard` component located in :file:`awesome_tshirt/static/src/` to use the 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 `Layout` component. You can use
`display` props of the `Layout` component. :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 .. image:: 02_web_framework/new_layout.png
:align: center :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 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 Let us now use the action service for an easy access to the common views in Odoo.
need to use the action service.
:ref:`Services <frontend/services>` is a notion defined by the Odoo JavaScript framework, it is a :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 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()` hooks. services, and components can import a service with the `useService()` hook.
.. example:: .. example::
@ -92,9 +95,17 @@ services, and components can import a service with the `useService()` hooks.
exists, so you should use `its xml id exists, so you should use `its xml id
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/ <https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
odoo/addons/base/views/res_partner_views.xml#L525>`_). 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 #. 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 .. image:: 02_web_framework/navigation_buttons.png
:align: center :align: center
@ -109,9 +120,10 @@ services, and components can import a service with the `useService()` hooks.
3. Call the server, add some statistics 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 Let's improve the dashboard by adding a few cards (see the `Card` component :ref:`made in the
training) containing a few statistics. There is a route `/awesome_tshirt/statistics` that will previous chapter <tutorials/discover_js_framework/generic_card>`) containing a few statistics. There
perform some computations and return an object containing some useful information. 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 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: <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 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 `/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 `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 would prefer to do it only the first time, so we actually need to maintain some state outside of the
outside of the `Dashboard` component. This is a nice use case for a service! `Dashboard` component. This is a nice use case for a service!
.. example:: .. example::
@ -188,13 +200,12 @@ outside of the `Dashboard` component. This is a nice use case for a service!
.. exercise:: .. 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 #. It should provide a function `loadStatistics` that, once called, performs the actual rpc, and
always return the same information. always return the same information.
#. Maybe use the `memoize #. Use the `memoize <https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
addons/web/static/src/core/utils/functions.js#L11>`_ utility function from 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. #. Use this service in the `Dashboard` component.
#. Check that it works as expected #. 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 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. 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 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 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 assets bundle, or lazy load it. Lazy loading is usually better since our users will not have to load
chartjs code every time if they don't need it). the chartjs code every time if they don't need it.
.. exercise:: .. exercise::
#. Load chartjs, you can use the `loadJs #. 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"/> <field name="preview_moves" widget="account_resequence_widget"/>
.. _tutorials/master_odoo_web_framework/image_preview_field:
1. An `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 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 `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. to see the image itself 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.
.. exercise:: .. exercise::
#. Create a new `ImagePreview` component and use the `CharField` component in your template. You #. Create a new `ImagePreview` component and register it in the proper :ref:`registry
can use `t-props <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 <{OWL_PATH}/doc/reference/props.md#dynamic-props>`_ to pass props received by `ImagePreview`
received by `ImagePreview` to `CharField`. to `CharField`. Update the arch of the form view to use your new field by setting the `widget`
#. Register your field in the proper :ref:`registry <frontend/registries>`. attribute.
#. 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:: .. note::
It is possible to solve this exercise by inheriting `CharField` , but the goal of this exercise It is possible to solve this exercise by inheriting `CharField`, but the goal of this exercise is
is to create a field from scratch. to create a field from scratch.
.. image:: 01_fields_and_views/image_field.png .. image:: 01_fields_and_views/image_field.png
:align: center :align: center
@ -136,11 +133,12 @@ field, so it can be edited.
2. Improving the `image_preview` field 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:: .. exercise::
We want to improve the field of the previous task to help the staff recognize orders for which Display a warning "MISSING TSHIRT DESIGN" in red if there is no image URL specified on the order.
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.
.. image:: 01_fields_and_views/missing_image.png .. image:: 01_fields_and_views/missing_image.png
:align: center :align: center
@ -150,9 +148,9 @@ field, so it can be edited.
Let's see how to use inheritance to extend an existing component. 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 There is a `is_late`, readonly, boolean field on the order model. That would be useful information
see on the list/kanban/view. Then, let us say that we want to add a red word "Late!" next to it 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. whenever it is set to `true`.
.. exercise:: .. exercise::
@ -166,11 +164,8 @@ whenever it is set to true.
:align: center :align: center
.. seealso:: .. seealso::
- `Example: A field inheriting another
- `Example: A field inheriting another (JS) <{GITHUB_PATH}/addons/account/static/src/components/account_type_selection>`_
<{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>`_
- :ref:`Documentation on xpath <reference/views/inheritance>` - :ref:`Documentation on xpath <reference/views/inheritance>`
4. Message for some customers 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:: .. exercise::
For a super efficient workflow, we would like to display a message/warning box with some For a super efficient workflow, we would like to display an alert block with specific messages
information in the form view, with specific messages depending on some conditions: depending on some conditions:
- If the `image_url` field is not set, it should display "No image". - 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". material".
- Make sure that your widget is updated in real time. - 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 .. image:: 01_fields_and_views/warning_widget.png
:align: center :align: center
@ -197,23 +195,23 @@ insert arbitrary components in the form view. Let us see how we can use it.
- `Example: Using the tag <widget> in a form view - `Example: Using the tag <widget> in a form view
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/ <https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
addons/calendar/views/calendar_views.xml#L197>`_ addons/calendar/views/calendar_views.xml#L197>`_
- `Example: Implementation of a widget (JS) - `Example: Implementation of a widget
<{GITHUB_PATH}/addons/web/static/src/views/widgets/week_days/week_days.js>`_ <{GITHUB_PATH}/addons/web/static/src/views/widgets/week_days>`_
- `Example: Implementation of a widget (XML)
<{GITHUB_PATH}/addons/web/static/src/views/widgets/week_days/week_days.xml>`_
5. Use `markup` 5. Use `markup`
=============== ===============
Let's see how we can display raw HTML in a template. Before, there was a `t-raw` directive that Lets see how we can display raw HTML in a template. The `t-out` directive can be used for that
would just output anything as HTML. This was unsafe, and has been replaced by a `t-out propose. Indeed, `it generally acts like t-esc, unless the data has been marked explicitly with a
<{OWL_PATH}/doc/reference/templates.md#outputting-data>`_ directive that acts like a `t-esc` unless markup function <{OWL_PATH}/doc/reference/templates.md#outputting-data>`_. In that case, its value
the data has been marked explicitly with a `markup` function. is injected as HTML.
.. exercise:: .. exercise::
#. Modify the previous exercise to put the `image` and `material` words in bold. #. 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`. #. 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:: .. 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.
@ -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 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 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 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. (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` 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 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. 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 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 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 `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:: .. 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`. `awesome_tshirt.order_form_view`.
#. Add a `js_class` attribute to the arch of the form view so Odoo will load it. #. Add a `js_class="awesome_tshirt.order_form_view"` attribute to the arch of the form view so
#. Create a new template inheriting from the form controller template to add a button after the that Odoo will load it.
create button. #. Create a new template inheriting from the form controller template and add a "Print Label"
#. Add a button. Clicking on this button should call the method `print_label` from the model button after the "New" button.
`awesome_tshirt.order` with the proper id. Note: `print_label` is a mock method, it only #. Clicking on this button should call the method `print_label` from the model
displays a message in the logs. `awesome_tshirt.order` with the proper id.
#. The button should be disabled if the current order is in `create` mode (i.e., it does not
.. 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). 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 #. 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. #. Bonus point: clicking twice on the button should not trigger 2 RPCs.
.. image:: 01_fields_and_views/form_button.png .. 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>`_ - `Code: orm service <{GITHUB_PATH}/addons/web/static/src/core/orm_service.js>`_
- `Example: Using the orm service - `Example: Using the orm service
<{GITHUB_PATH}/addons/account/static/src/components/open_move_widget/open_move_widget.js>`_ <{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 7. Auto-reload the kanban view
============================== ==============================

View File

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