[FIX] tutorials/discover_js_framework: clarify instructions

closes odoo/documentation#8182

X-original-commit: 632add350d
Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
This commit is contained in:
Antoine Vandevenne (anv) 2024-03-07 16:55:45 +01:00
parent cb511183db
commit 3b8bb15933
2 changed files with 110 additions and 110 deletions

View File

@ -22,8 +22,7 @@ into the exercises, make sure you have followed all the steps described in this
In this chapter, we use the `awesome_owl` addon, which provides a simplified environment that
only contains Owl and a few other files. The goal is to learn Owl itself, without relying on Odoo
web client code. To get started, open the `/awesome_owl` route with your browser: it
should display an Owl component with the text *hello world*.
web client code.
.. spoiler:: Solutions
@ -41,9 +40,11 @@ button.
.. code-block:: js
/** @odoo-module **/
import { Component, useState } from "@odoo/owl";
class Counter extends Component {
export class Counter extends Component {
static template = "my_module.Counter";
setup() {
@ -79,8 +80,8 @@ As a first exercise, let us modify the `Playground` component located in
`/awesome_owl` route with your browser.
#. Modify :file:`playground.js` so that it acts as a counter like in the example above. You will
need to use the `useState hook
#. Modify :file:`playground.js` so that it acts as a counter like in the example above.
Keep `Playground` for the class name. You will 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.
@ -90,6 +91,10 @@ As a first exercise, let us modify the `Playground` component located in
<{OWL_PATH}/doc/reference/event_handling.md#event-handling>`_ attribute in the button to
trigger the `increment` method whenever the button is clicked.
.. important::
Don't forget :code:`/** @odoo-module **/` in your JavaScript files. More information on this can
be found :ref:`here <frontend/modules/native_js>`.
.. tip::
The Odoo JavaScript files downloaded by the browser are minified. For debugging purpose, it's
easier when the files are not minified. Switch to
@ -110,7 +115,8 @@ see how to create a `sub-component <{OWL_PATH}/doc/reference/component.md#sub-co
#. You can do it in the same file first, but once it's done, update your code to move the
`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.
#. Add two counters in your playground.
#. Use `<Counter/>` in the template of the `Playground` component to add two counters in your
playground.
.. image:: 01_owl_components/double_counter.png
:align: center
@ -120,10 +126,6 @@ see how to create a `sub-component <{OWL_PATH}/doc/reference/component.md#sub-co
as the component. For example, if we have a `TodoList` component, its code should be in
`todo_list.js`, `todo_list.xml` and if necessary, `todo_list.scss`
.. important::
Don't forget :code:`/** @odoo-module **/` in your JavaScript files. More information on this can
be found :ref:`here <frontend/modules/native_js>`.
.. _tutorials/discover_js_framework/simple_card:
3. A simple `Card` component
@ -163,7 +165,7 @@ The above example should produce some html using bootstrap that look like this:
4. Using `markup` to display html
=================================
If you used `t-esc` in the previous exercise, then you may have noticed that Owl will automatically escape
If you used `t-esc` in the previous exercise, then you may have noticed that Owl automatically escapes
its content. For example, if you try to display some html like this: `<Card title="'my title'" content="this.html"/>`
with `this.html = "<div>some content</div>""`,
the resulting output will simply display the html as a string.
@ -233,7 +235,7 @@ be called whenever the `Counter` component is incremented.
.. important::
There is a subtlety with callback props: they usually should be defined with the `.bind`
suffix. See the `documentation <{OWL_PATH}/doc/reference/props.md#binding-function-props>`_
suffix. See the `documentation <{OWL_PATH}/doc/reference/props.md#binding-function-props>`_.
7. A todo list
==============
@ -249,7 +251,7 @@ For this tutorial, a `todo` is an object that contains three values: an `id` (nu
{ id: 3, description: "buy milk", isCompleted: false }
#. Create a `TodoList` and a `TodoItem` components
#. Create a `TodoList` and a `TodoItem` components.
#. The `TodoItem` component should receive a `todo` as a prop, and display its `id` and `description` in a `div`.
#. For now, hardcode the list of todos:
@ -258,20 +260,20 @@ For this tutorial, a `todo` is an object that contains three values: an `id` (nu
// in TodoList
this.todos = useState([{ id: 3, description: "buy milk", isCompleted: false }]);
#. Use `t-foreach <{OWL_PATH}/doc/reference/templates.md#loops>`_ to display each todo in a `TodoItem`
#. Display a `TodoList` in the playground
#. Add props validation to `TodoItem`
#. Use `t-foreach <{OWL_PATH}/doc/reference/templates.md#loops>`_ to display each todo in a `TodoItem`.
#. Display a `TodoList` in the playground.
#. Add props validation to `TodoItem`.
.. image:: 01_owl_components/todo_list.png
:align: center
Note that the `t-foreach` directive is not exactly the same in Owl as the QWeb python implementation: it
requires a `t-key` unique value, so Owl can properly reconciliate each element.
.. tip::
Since the `TodoList` and `TodoItem` components are so tightly coupled, it makes
sense to put them in the same folder
sense to put them in the same folder.
.. note::
The `t-foreach` directive is not exactly the same in Owl as the QWeb python implementation: it
requires a `t-key` unique value, so that Owl can properly reconcile each element.
8. Use dynamic attributes
=========================
@ -281,7 +283,7 @@ using a `dynamic attributes <{OWL_PATH}/doc/reference/templates.md#dynamic-attri
#. Add the Bootstrap classes `text-muted` and `text-decoration-line-through` on the `TodoItem` root element
if it is completed.
#. Change the hardcoded `todo` value to check that it is properly displayed.
#. Change the hardcoded `this.todos` value to check that it is properly displayed.
Even though the directive is named `t-att` (for attribute), it can be used to set a `class` value (and
html properties such as the `value` of an input).
@ -305,7 +307,7 @@ html properties such as the `value` of an input).
So far, the todos in our list are hard-coded. Let us make it more useful by allowing the user to add
a todo to the list.
#. Remove the hardcoded values in the `TodoList` component
#. Remove the hardcoded values in the `TodoList` component:
.. code-block:: javascript
@ -381,7 +383,7 @@ hook functions have to be called in the `setup` method, and no later!
An Owl component goes through a lot of phases: it can be instantiated, rendered,
mounted, updated, detached, destroyed, ... This is the `component lifecycle <{OWL_PATH}/doc/reference/component.md#lifecycle>`_.
mounted, updated, detached, destroyed... This is the `component lifecycle <{OWL_PATH}/doc/reference/component.md#lifecycle>`_.
The figure above show the most important events in the life of a component (hooks are shown in purple).
Roughly speaking, a component is created, then updated (potentially many times), then is destroyed.
@ -444,7 +446,7 @@ component is mounted.
.. code-block:: js
this.inputRef = useRef('refname');
this.inputRef = useRef('input');
11. Toggling todos
==================
@ -452,7 +454,7 @@ component is mounted.
Now, let's add a new feature: mark a todo as completed. This is actually trickier than one might
think. The owner of the state is not the same as the component that displays it. So, the `TodoItem`
component needs to communicate to its parent that the todo state needs to be toggled. One classic
way to do this is by using a `callback prop
way to do this is by adding a `callback prop
<{OWL_PATH}/doc/reference/props.md#binding-function-props>`_ `toggleState`.
#. Add an input with the attribute :code:`type="checkbox"` before the id of the task, which must
@ -463,7 +465,7 @@ way to do this is by using a `callback prop
falsy value.
#. Add a callback props `toggleState` to `TodoItem`.
#. Add a `click` event handler on the input in the `TodoItem` component and make sure it calls the
#. Add a `change` event handler on the input in the `TodoItem` component and make sure it calls the
`toggleState` function with the todo id.
#. Make it work!
@ -503,19 +505,19 @@ The final touch is to let the user delete a todo.
In a :ref:`previous exercise <tutorials/discover_js_framework/simple_card>`, we built
a simple `Card` component. But it is honestly quite limited. What if we want
to display some arbitrary content inside a card, such as a sub component? Well,
to display some arbitrary content inside a card, such as a sub-component? Well,
it does not work, since the content of the card is described by a string. It would
however be very convenient if we could describe the content as a piece of template.
This is exactly what Owl `slot <{OWL_PATH}/doc/reference/slots.md>`_ system is designed
This is exactly what Owl's `slot <{OWL_PATH}/doc/reference/slots.md>`_ system is designed
for: allowing to write generic components.
Let us modify the `Card` component to use slots:
#. Remove the `content` prop
#. Use the default slot to define the body
#. Insert a few cards with arbitrary content, such as a `Counter` component
#. (bonus) Add prop validation
#. Remove the `content` prop.
#. Use the default slot to define the body.
#. Insert a few cards with arbitrary content, such as a `Counter` component.
#. (bonus) Add prop validation.
.. image:: 01_owl_components/generic_card.png
:align: center
@ -526,6 +528,8 @@ Let us modify the `Card` component to use slots:
14. Minimizing card content
===========================
.. TODO: This exercise shows no new concept; it should probably be removed.
Finally, let's add a feature to the `Card` component, to make it more interesting: we
want a button to toggle its content (show it or hide it)

View File

@ -74,10 +74,10 @@ Theory: Services
In practice, every component (except the root component) may be destroyed at any time and replaced
(or not) with another component. This means that each component internal state is not persistent.
This is fine in many cases, but there certainly are situations where we want to keep some data around.
For example, all discuss messages should not be reloaded every time we display a channel.
For example, all Discuss messages should not be reloaded every time we display a channel.
Also, it may happen that we need to write some code that is not a component. Maybe something that
process all barcodes, or that manages the user configuration (context, ...).
process all barcodes, or that manages the user configuration (context, etc.).
The Odoo framework defines the idea of a :ref:`service <frontend/services>`, which is a persistent
piece of code that exports state and/or functions. Each service can depend on other services, and
@ -90,13 +90,13 @@ The following example registers a simple service that displays a notification ev
import { registry } from "@web/core/registry";
const myService = {
dependencies: ["notification"],
start(env, { notification }) {
let counter = 1;
setInterval(() => {
notification.add(`Tick Tock ${counter++}`);
}, 5000);
},
dependencies: ["notification"],
start(env, { notification }) {
let counter = 1;
setInterval(() => {
notification.add(`Tick Tock ${counter++}`);
}, 5000);
},
};
registry.category("services").add("myService", myService);
@ -110,18 +110,17 @@ state:
import { registry } from "@web/core/registry";
const sharedStateService = {
start(env) {
let state = {};
return {
getValue(key) {
return state[key];
},
setValue(key, value) {
state[key] = value;
},
};
},
start(env) {
let state = {};
return {
getValue(key) {
return state[key];
},
setValue(key, value) {
state[key] = value;
},
};
},
};
registry.category("services").add("shared_state", sharedStateService);
@ -141,6 +140,8 @@ Then, any component can do this:
2. Add some buttons for quick navigation
========================================
.. TODO: Add ref to the action service when it's documented.
One important service provided by Odoo is the `action` service: it can execute
all kind of standard actions defined by Odoo. For example, here is how one
component could execute an action by its xml id:
@ -163,39 +164,39 @@ Let us now add two buttons to our control panel:
exists, so you should use `its xml id
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/odoo/addons/base/views/res_partner_views.xml#L510>`_).
#. A button `Leads`, which opens a dynamic action on the `crm.lead` model with a list and a form view.
#. A button `Leads`, which opens a dynamic action on the `crm.lead` model with a list and a form
view. Follow the example of `this use of the action service
<https://github.com/odoo/odoo/blob/ef424a9dc22a5abbe7b0a6eff61cf113826f04c0/addons/account
/static/src/components/journal_dashboard_activity/journal_dashboard_activity.js#L28-L35>`_.
.. image:: 02_web_framework/navigation_buttons.png
:align: center
.. seealso::
- `Example: doAction use
<{GITHUB_PATH}/addons/account/static/src/components/journal_dashboard_activity
/journal_dashboard_activity.js#L35>`_
- `Code: action service
<{GITHUB_PATH}/addons/web/static/src/webclient/actions/action_service.js>`_
`Code: action service
<{GITHUB_PATH}/addons/web/static/src/webclient/actions/action_service.js>`_
3. Add a DashboardItem
======================
3. Add a dashboard item
=======================
Let us now improve our content.
#. Create a generic `DashboardItem` component that display its default slot in a nice card layout
It should take an optional `size` number props, that default to `1`
The width should be hardcoded to `(18*size)rem`.
#. Add a few cards in the dashboard, with no size and a size of 2.
#. Create a generic `DashboardItem` component that display its default slot in a nice card layout.
It should take an optional `size` number props, that default to `1`. The width should be
hardcoded to `(18*size)rem`.
#. Add two cards to the dashboard. One with no size, and the other with a size of 2.
.. image:: 02_web_framework/dashboard_item.png
:align: center
.. seealso::
- `Owl slot system <{OWL_PATH}/doc/reference/slots.md>`_
`Owl's slot system <{OWL_PATH}/doc/reference/slots.md>`_
4. Call the server, add some statistics
=======================================
Let's improve the dashboard by adding a few dashboard items to display *real* business data.
The *awesome_dashboard* addon provides a `/awesome_dashboard/statistics` route that is meant
The `awesome_dashboard` addon provides a `/awesome_dashboard/statistics` route that is meant
to return some interesting information.
To call a specific controller, we need to use the :ref:`rpc service <frontend/services/rpc>`.
@ -226,11 +227,7 @@ A basic request could look like this:
:align: center
.. seealso::
- `Code: rpc service <{GITHUB_PATH}/addons/web/static/src/core/network/rpc_service.js>`_
- `Example: calling a route in onWillStart
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
addons/lunch/static/src/views/search_model.js#L21>`_
`Code: rpc service <{GITHUB_PATH}/addons/web/static/src/core/network/rpc_service.js>`_
5. Cache network calls, create a service
========================================
@ -246,9 +243,9 @@ would prefer to do it only the first time, so we actually need to maintain some
always return the same information.
#. 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` that will allow caching the statistics.
`@web/core/utils/functions` that allows caching the statistics.
#. Use this service in the `Dashboard` component.
#. Check that it works as expected
#. Check that it works as expected.
.. seealso::
- `Example: simple service <{GITHUB_PATH}/addons/web/static/src/core/network/http_service.js>`_
@ -266,15 +263,15 @@ by the graph view. However, it is not loaded by default, so we will need to eith
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.
#. Create a `PieChart` component
#. Create a `PieChart` component.
#. In its `onWillStart` method, load chartjs, you can use the `loadJs
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
addons/web/static/src/core/assets.js#L23>`_ function to load
:file:`/web/static/lib/Chart/Chart.js`.
#. Use the `PieChart` component in a `DashboardItem` to display a `pie chart
<https://www.chartjs.org/docs/2.8.0/charts/doughnut.html>`_ that shows the
correct quantity for each sold t-shirts in each size (that information is available in the
statistics route). Note that you can use the `size` property to make it look larger
quantity for each sold t-shirts in each size (that information is available in the
`/statistics` route). Note that you can use the `size` property to make it look larger.
#. The `PieChart` component will need to render a canvas, and draw on it using `chart.js`.
#. Make it work!
@ -293,11 +290,11 @@ the chartjs code every time if they don't need it.
7. Real life update
===================
Since we moved the data loading in a cache, it does not ever updates. But let us say that we
Since we moved the data loading in a cache, it never updates. But let us say that we
are looking at fast moving data, so we want to periodically (for example, every 10min) reload
fresh data.
This is quite simple to implement, with a `setTimeout` or `setInterval` in the dashboard service.
This is quite simple to implement, with a `setTimeout` or `setInterval` in the statistics service.
However, here is the tricky part: if the dashboard is currently being displayed, it should be
updated immediately.
@ -306,7 +303,7 @@ but not linked to any component. A component can then do a `useState` on it to s
changes.
#. Update the dashboard service to reload data every 10 minutes (to test it, use 10s instead!)
#. Update the statistics service to reload data every 10 minutes (to test it, use 10s instead!)
#. Modify it to return a `reactive <{OWL_PATH}/doc/reference/reactivity.md#reactive>`_ object.
Reloading data should update the reactive object in place.
#. The `Dashboard` component can now use it with a `useState`
@ -328,13 +325,13 @@ look at it.
To do that, we will need to create a new bundle containing all our dashboard assets,
then use the `LazyComponent` (located in `@web/core/assets`).
#. Move all dashboard assets into a sub folder `/dashboard` to make it easier to
#. Move all dashboard assets into a sub folder :file:`/dashboard` to make it easier to
add to a bundle.
#. Create a `awesome_dashboard.dashboard` assets bundle containing all content of
the `/dashboard` folder
#. Modify `dashboard.js` to register itself in the `lazy_components` registry, and not
the `/dashboard` folder.
#. Modify :file:`dashboard.js` to register itself in the `lazy_components` registry, and not
in the `action` registry.
#. Add in `src/` a file `dashboard_action` that import `LazyComponent` and register
#. Add in :file:`src/` a file :file:`dashboard_action` that imports `LazyComponent` and registers
it to the `action` registry
9. Making our dashboard generic
@ -342,15 +339,15 @@ then use the `LazyComponent` (located in `@web/core/assets`).
So far, we have a nice working dashboard. But it is currently hardcoded in the dashboard
template. What if we want to customize our dashboard? Maybe some users have different
needs, and want to see some other data.
needs and want to see other data.
So, the next step is then to make our dashboard generic: instead of hardcoding its content
So, the next step is to make our dashboard generic: instead of hard-coding its content
in the template, it can just iterate over a list of dashboard items. But then, many
questions comes up: how to represent a dashboard item, how to register it, what data
questions come up: how to represent a dashboard item, how to register it, what data
should it receive, and so on. There are many different ways to design such a system,
with different trade offs.
with different trade-offs.
For this tutorial, we will say that a dashboard item is an object with the folowing structure:
For this tutorial, we will say that a dashboard item is an object with the following structure:
.. code-block:: js
@ -367,7 +364,7 @@ For this tutorial, we will say that a dashboard item is an object with the folow
};
The `description` value will be useful in a later exercise to show the name of items that the
user can choose to add to his dashboard. The `size` number is optional, and simply describes
user can add to their dashboard. The `size` number is optional, and simply describes
the size of the dashboard item that will be displayed. Finally, the `props` function is optional.
If not given, we will simply give the `statistics` object as data. But if it is defined, it will
be used to compute specific props for the component.
@ -383,16 +380,16 @@ The goal is to replace the content of the dashboard with the following snippet:
</DashboardItem>
</t>
Note that the above example features two advanced features of Owl: dynamic components, and dynamic props.
Note that the above example features two advanced features of Owl: dynamic components and dynamic props.
We currently have two kinds of item components: number cards, with a title and a number, and pie cards, with
We currently have two kinds of item components: number cards with a title and a number, and pie cards with
some label and a pie chart.
#. create and implement two components: `NumberCard` and `PieChartCard`, with the corresponding props
#. create a file `dashboard_items.js` in which you define and export a list of items, using `NumberCard`
and `PieChartCard` to recreate our current dashboard
#. import that list of items in our `Dashboard` component, add it to the component, and update the template
to use a `t-foreach` like shown above
#. Create and implement two components: `NumberCard` and `PieChartCard`, with the corresponding props.
#. Create a file :file:`dashboard_items.js` in which you define and export a list of items, using `NumberCard`
and `PieChartCard` to recreate our current dashboard.
#. Import that list of items in our `Dashboard` component, add it to the component, and update the template
to use a `t-foreach` like shown above.
.. code-block:: js
@ -410,27 +407,26 @@ However, the content of our item list is still hardcoded. Let us fix that by usi
#. Instead of exporting a list, register all dashboard items in a `awesome_dashboard` registry
#. Import all the items of the `awesome_dashboard` registry in the `Dashboard` component
The dashboard is now easily extensible. Any other odoo addon that want to register a new item to the
The dashboard is now easily extensible. Any other Odoo addon that wants to register a new item to the
dashboard can just add it to the registry.
11. Add and remove dashboard items
==================================
Let us see how we can make our dashboard customizable. To make it simple, we will save the user
dashboard configuration in the local storage, so it is persistent, but we don't have to deal
dashboard configuration in the local storage so that it is persistent, but we don't have to deal
with the server for now.
The dashboard configuration will be saved as a list of removed item ids.
#. Add a button in the control panel with a gear icon, to indicate that it is a settings button
#. Clicking on that button should open a dialog
#. In that dialog, we want to see a list of all existing dashboard items, each with a checkbox
#. Add a button in the control panel with a gear icon to indicate that it is a settings button.
#. Clicking on that button should open a dialog.
#. In that dialog, we want to see a list of all existing dashboard items, each with a checkbox.
#. There should be a `Apply` button in the footer. Clicking on it will build a list of all item ids
that are unchecked
#. We want to store that value in the local storage
that are unchecked.
#. We want to store that value in the local storage.
#. And modify the `Dashboard` component to filter the current items by removing the ids of items
from the configuration
from the configuration.
.. image:: 02_web_framework/items_configuration.png
:width: 80%
@ -443,10 +439,10 @@ Here is a list of some small improvements you could try to do if you have the ti
#. Make sure your application can be :ref:`translated <reference/translations>` (with
`env._t`).
#. Clicking on a section of the pie chart should open a list view of all orders which have the
#. Clicking on a section of the pie chart should open a list view of all orders that have the
corresponding size.
#. Save the content of the dashboard in a user settings on the server!
#. Make it responsive: in mobile mode, each card should take 100% of the width
#. Save the content of the dashboard in a user setting on the server!
#. Make it responsive: in mobile mode, each card should take 100% of the width.
.. seealso::
- `Example: use of env._t function