From 3b8bb15933b8e8604c9f3d9e6fd165ef478ddcde Mon Sep 17 00:00:00 2001 From: "Antoine Vandevenne (anv)" Date: Thu, 7 Mar 2024 16:55:45 +0100 Subject: [PATCH] [FIX] tutorials/discover_js_framework: clarify instructions closes odoo/documentation#8182 X-original-commit: 632add350d5fd9f8c0f9da2a92b88cfb468291e7 Signed-off-by: Antoine Vandevenne (anv) --- .../01_owl_components.rst | 70 ++++---- .../02_build_a_dashboard.rst | 150 +++++++++--------- 2 files changed, 110 insertions(+), 110 deletions(-) diff --git a/content/developer/tutorials/discover_js_framework/01_owl_components.rst b/content/developer/tutorials/discover_js_framework/01_owl_components.rst index 827f07ce0..6cf7f6638 100644 --- a/content/developer/tutorials/discover_js_framework/01_owl_components.rst +++ b/content/developer/tutorials/discover_js_framework/01_owl_components.rst @@ -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 `. + .. 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 `` 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 `. - .. _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: `` with `this.html = "
some content
""`, 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 `, 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) diff --git a/content/developer/tutorials/discover_js_framework/02_build_a_dashboard.rst b/content/developer/tutorials/discover_js_framework/02_build_a_dashboard.rst index 9f3c21c36..64f926760 100644 --- a/content/developer/tutorials/discover_js_framework/02_build_a_dashboard.rst +++ b/content/developer/tutorials/discover_js_framework/02_build_a_dashboard.rst @@ -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 `, 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 `_). -#. 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 + `_. .. 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 `. @@ -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 - `_ + `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 `_ 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 `_ function to load :file:`/web/static/lib/Chart/Chart.js`. #. Use the `PieChart` component in a `DashboardItem` to display a `pie chart `_ 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: -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 ` (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