mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
460 lines
17 KiB
Markdown
460 lines
17 KiB
Markdown
# 🦉 OWL Component 🦉
|
|
|
|
## Content
|
|
|
|
- [Overview](#overview)
|
|
- [Example](#example)
|
|
- [Composition](#composition)
|
|
- [Reference](#reference)
|
|
- [Properties](#properties)
|
|
- [Methods](#methods)
|
|
- [Lifecycle](#lifecycle)
|
|
- [Semantics](#semantics)
|
|
- [Asynchronous rendering](#asynchronous-rendering)
|
|
|
|
## Overview
|
|
|
|
OWL components are the building blocks for user interface. They are designed to be:
|
|
|
|
1. **declarative:** the user interface should be described in term of the state
|
|
of the application, not as a sequence of imperative steps.
|
|
|
|
2. **composable:** each widget can seamlessly be created in a parent widget by
|
|
a simple directive in its template.
|
|
|
|
3. **asynchronous rendering:** the framework will transparently wait for each
|
|
subwidgets to be ready before applying the rendering. It uses native promises
|
|
under the hood.
|
|
|
|
4. **uses QWeb as a template system:** the templates are described in XML
|
|
and follow the QWeb specification. This is a requirement for Odoo.
|
|
|
|
OWL components are defined as a subclass of Component. The rendering is
|
|
exclusively done by a [QWeb](qweb.md) template (either defined inline or preloaded in QWeb).
|
|
Rendering a component generates a virtual dom representation
|
|
of the widget, which is then patched to the DOM, in order to apply the changes in an efficient way.
|
|
|
|
OWL components observe their states, and rerender themselves whenever it is
|
|
changed. This is done by an [observer](observer.md).
|
|
|
|
## Example
|
|
|
|
Let us have a look at a simple component:
|
|
|
|
```javascript
|
|
class ClickCounter extends owl.Component {
|
|
template = "click-counter";
|
|
state = { value: 0 };
|
|
|
|
increment() {
|
|
this.state.value++;
|
|
}
|
|
}
|
|
```
|
|
|
|
```xml
|
|
<button t-name="click-counter" t-on-click="increment">
|
|
Click Me! [<t t-esc="state.value"/>]
|
|
</button>
|
|
```
|
|
|
|
Note that this code is written in ESNext style, so it will only run on the
|
|
latest browsers without a transpilation step.
|
|
|
|
This example show how a component should be defined: it simply subclasses the
|
|
Component class. It needs to define a template (`click-counter` here). Also,
|
|
a state object is defined. It is not mandatory to use the state object, but it
|
|
is certainly encouraged. The state object is [observed](observer.md), and any
|
|
change to it will cause a rerendering.
|
|
|
|
## Composition
|
|
|
|
The example above shows a QWeb template with a `t-on-click` directive. Widget
|
|
templates are standard [QWeb](qweb.md) templates, but with an extra directive:
|
|
`t-widget`. With the `t-widget` directive, widget templates can declare sub
|
|
widgets:
|
|
|
|
```xml
|
|
<div t-name="parent">
|
|
<span>some text</span>
|
|
<t t-widget="MyWidget" t-props="{info: 13}"/>
|
|
</div>
|
|
```
|
|
|
|
```js
|
|
class ParentWidget extends owl.Component {
|
|
template = 'parent';
|
|
widgets = { MyWidget: MyWidget};
|
|
...
|
|
}
|
|
```
|
|
|
|
In this example, the `ParentWidget`'s template creates a widget `MyWidget` just
|
|
after the span. See the [QWeb](qweb.md) documentation for more information on the
|
|
`t-widget` directive.
|
|
|
|
Note that the rendering context for the template is the widget itself. This means
|
|
that the template can access `state`, `props`, `env`, or any methods defined in the widget.
|
|
|
|
**CSS and style:** there is some specific support to allow the parent to declare
|
|
additional css classes or style for the sub widget: css declared in `class`, `style`, `t-att-class` or `t-att-style` will be added to the
|
|
root widget element.
|
|
|
|
```xml
|
|
<div t-name="parent">
|
|
<t t-widget="MyWidget" class="someClass" style="font-weight:bold;" t-props="{info: 13}"/>
|
|
</div>
|
|
```
|
|
|
|
Warning: there is a small caveat with dynamic class attributes: since Owl needs
|
|
to be able to add/remove proper classes whenever necessary, it needs to be aware
|
|
of the possible classes. Otherwise, it will not be able to make the difference
|
|
between a valid css class added by the component, or some custom code, and a
|
|
class that need to be removed. This is why we only support the explicit syntax
|
|
with a class object:
|
|
|
|
```js
|
|
<t t-widget="MyWidget" t-att-class="{a: state.flagA, b: state.flagB}" />
|
|
```
|
|
|
|
## Reference
|
|
|
|
An Owl component is a small class which represent a widget or some UI element.
|
|
It exists in the context of an environment (`env`), which is propagated from a
|
|
parent to its children. The environment needs to have a QWeb instance, which
|
|
will be used to render the component template.
|
|
|
|
### Properties
|
|
|
|
- **`el`** (HTMLElement | null): reference to the DOM root node of the element. It is `null` when the
|
|
component is not mounted.
|
|
|
|
- **`env`** (Object): the component environment, which contains a QWeb instance.
|
|
|
|
- **`template`** (string): a string, which is the name of the QWeb template that will render
|
|
the component.
|
|
|
|
- **`inlineTemplate`** (string, optional): a string that represents a xml template. If set,
|
|
this will be loaded into QWeb and used instead of the `template` property.
|
|
|
|
- **`state`** (Object): this is the location of the component's state, if there is
|
|
any. After the willStart method, the `state` property is observed, and each
|
|
change will cause the widget to rerender itself.
|
|
|
|
- **`props`** (Object): this is an object given (in the constructor) by the parent
|
|
to configure the component. It can be dynamically changed later by the parent,
|
|
in some case. Note that `props` are owned by the parent, not by the component.
|
|
As such, it should not ever be modified by the component.
|
|
|
|
- **`refs`** (Object): the `refs` object contains all references to sub DOM nodes
|
|
or sub widgets defined by a `t-ref` directive in the component's template.
|
|
|
|
### Methods
|
|
|
|
- **`mount(target)`** (async): this is the main way a component's hierarchy is added to the
|
|
DOM: the root component is mounted to a target HTMLElement. Obviously, this
|
|
is asynchronous, since each children need to be created as well. Most applications
|
|
will need to call `mount` exactly once, on the root component.
|
|
|
|
- **`unmount()`**: in case a component need to be detached/removed from the DOM, this
|
|
method can be used. Most applications should not call `unmount`, this is more
|
|
useful to the underlying component system.
|
|
|
|
- **`render()`** (async): calling this method directly will cause a rerender. Note
|
|
that this should be very rare to have to do it manually, the Owl framework is
|
|
most of the time responsible for doing that at an appropriate moment.
|
|
|
|
Note that the render method is asynchronous, so one cannot observe the updated
|
|
DOM in the same stack frame.
|
|
|
|
- **`shouldUpdate(nextProps)`**: this method is called each time a component's props
|
|
are updated. It returns a boolean, which indicates if the widget should
|
|
ignore a props update. If it returns false, then `willUpdateProps` will not
|
|
be called, and no rendering will occur. Its default implementation is to
|
|
always return true. This is an optimization, similar to React's `shouldComponentUpdate`. Most of the time, this should not be used, but it
|
|
can be useful if we are handling large number of components.
|
|
|
|
- **`updateEnv(nextEnv)`**: update the environment of a component and all its
|
|
children. This forces a complete rerender. For example, this could be useful
|
|
if we have a `isMobile` key in the environment, to decide if we want a mobile
|
|
interface or a destkop one.
|
|
|
|
- **`set(target, key, value)`**. This method is necessary in some cases when we
|
|
need to modify the state of the component in a way that is not visible to the
|
|
observer (see [observer's technical limitations](observer.md#technical-limitations)).
|
|
For example, if we need to add a key to the state.
|
|
|
|
- **`destroy()`**. As its name suggests, this method will remove the component,
|
|
and perform all necessary cleanup, such as unmounting the component, its children,
|
|
removing the parent/children relationship. This method should almost never be
|
|
called directly (except maybe on the root component), but should be done by the
|
|
framework instead.
|
|
|
|
### Lifecycle
|
|
|
|
A solid and robust component system needs useful hooks/methods to help
|
|
developers write components. Here is a complete description of the lifecycle of
|
|
a owl component:
|
|
|
|
| Method | Description |
|
|
| ------------------------------------------------ | ----------------------------------------------------- |
|
|
| **[constructor](#constructor)** | constructor |
|
|
| **[willStart](#willStart)** | async, before first rendering |
|
|
| **[mounted](#mounted)** | just after component is rendered and added to the DOM |
|
|
| **[willUpdateProps](#willupdatepropsnextprops)** | async, before props update |
|
|
| **[willPatch](#willpatch)** | just before the DOM is patched |
|
|
| **[patched](#patchedsnapshot)** | just after the DOM is patched |
|
|
| **[willUnmount](#willUnmount)** | just before removing component from DOM |
|
|
|
|
Notes:
|
|
|
|
- hooks call order is precisely defined: `[willX]` hooks are called first on parent,
|
|
then on children, and `[Xed]` are called in the reverse order: first children,
|
|
then parent.
|
|
- no hook method should ever be called manually. They are supposed to be
|
|
called by the owl framework whenever it is required.
|
|
|
|
#### `constructor(parent, props)`
|
|
|
|
The constructor is not exactly a hook, it is the regular,
|
|
normal, constructor of the component. Since it is not a hook, you need to make
|
|
sure that `super` is called.
|
|
|
|
This is usually where you would set the initial state and the template of the
|
|
component.
|
|
|
|
```javascript
|
|
constructor(parent, props) {
|
|
super(parent, props);
|
|
this.state = {someValue: true};
|
|
this.template = 'mytemplate';
|
|
}
|
|
```
|
|
|
|
Note that with ESNext class fields, the constructor method does not need to be
|
|
implemented in most cases:
|
|
|
|
```javascript
|
|
class ClickCounter extends owl.Component {
|
|
template = "click-counter";
|
|
state = { value: 0 };
|
|
|
|
...
|
|
}
|
|
```
|
|
|
|
#### `willStart()`
|
|
|
|
willStart is an asynchronous hook that can be implemented to
|
|
perform some action before the initial rendering of a component.
|
|
|
|
It will be called exactly once before the initial rendering. It is useful
|
|
in some cases, for example, to load external assets (such as a JS library)
|
|
before the widget is rendered. Another use case is to load data from a server.
|
|
|
|
```javascript
|
|
async willStart() {
|
|
await owl.utils.loadJS("my-awesome-lib.js");
|
|
}
|
|
```
|
|
|
|
At this point, the component is not yet rendered. Note that a slow `willStart` method will slow down the rendering of the user
|
|
interface. Therefore, some care should be made to make this method as
|
|
fast as possible.
|
|
|
|
The widget rendering will take place after `willStart` is completed.
|
|
|
|
#### `mounted()`
|
|
|
|
`mounted` is called each time a component is attached to the
|
|
DOM, after the initial rendering and possibly later if the component was unmounted
|
|
and remounted. At this point, the component is considered _active_. This is a good place to add some listeners, or to interact with the
|
|
DOM, if the component needs to perform some measure for example.
|
|
|
|
It is the opposite of `willUnmount`. If a component has been mounted, it will
|
|
always be unmounted at some point in the future.
|
|
|
|
The mounted method will be called recursively on each of its children. First,
|
|
the parent, then all its children.
|
|
|
|
Note that the state is now observed. It is however allowed (but not encouraged)
|
|
to modify the state in the `mounted` hook. Doing so will cause a rerender,
|
|
which will not be perceptible by the user, but will slightly slow down the
|
|
component.
|
|
|
|
#### `willUpdateProps(nextProps)`
|
|
|
|
The willUpdateProps is an asynchronous hook, called just before new props
|
|
are set. This is useful if the component needs some asynchronous task
|
|
performed, depending on the props (for example, assuming that the props are
|
|
some record Id, fetching the record data).
|
|
|
|
```javascript
|
|
willUpdateProps(nextProps) {
|
|
return this.loadData({id: nextProps.id});
|
|
}
|
|
```
|
|
|
|
This hook is not called during the first render (but willStart is called
|
|
and performs a similar job).
|
|
|
|
#### `willPatch()`
|
|
|
|
The willPatch hook is called just before the DOM patching process starts.
|
|
It is not called on the initial render. This is useful to read some
|
|
information from the DOM. For example, the current position of the
|
|
scrollbar.
|
|
|
|
Note that modifying the state object is not allowed here. This method is called just
|
|
before an actual DOM patch, and is only intended to be used to save some local
|
|
DOM state. Also, it will not be called if the widget is not in the DOM (this can
|
|
happen with widgets with `t-keepalive`).
|
|
|
|
The return value of this method will be given as the first argument of the
|
|
corresponding `patched` call.
|
|
|
|
#### `patched(snapshot)`
|
|
|
|
This hook is called whenever a component did actually update its DOM (most
|
|
likely via a change in its state/props or environment).
|
|
|
|
This method is not called on the initial render. It is useful to interact
|
|
with the DOM (for example, through an external library) whenever the
|
|
component was patched. Note that this hook will not be called if the widget is
|
|
not in the DOM (this can happen with widgets with `t-keepalive`).
|
|
|
|
The `snapshot` parameter is the result of the previous `willPatch` call.
|
|
|
|
Updating the widget state in this hook is possible, but not encouraged.
|
|
One need to be careful, because updates here will cause rerender, which in
|
|
turn will cause other calls to patched. So, we need to be particularly
|
|
careful at avoiding endless cycles.
|
|
|
|
#### `willUnmount()`
|
|
|
|
willUnmount is a hook that is called each time just before a component is unmounted from
|
|
the DOM. This is a good place to remove some listeners, for example.
|
|
|
|
```javascript
|
|
mounted() {
|
|
this.env.bus.on('someevent', this, this.doSomething);
|
|
}
|
|
willUnmount() {
|
|
this.env.bus.off('someevent', this, this.doSomething);
|
|
}
|
|
```
|
|
|
|
This is the opposite method of `mounted`.
|
|
|
|
### Semantics
|
|
|
|
We give here an informal description of the way components are created/updated
|
|
in an application. Here, ordered lists describe actions that are executed
|
|
sequentially, bullet lists describe actions that are executed in parallel.
|
|
|
|
**Scenario 1: initial rendering** Imagine we want to render the following component tree:
|
|
|
|
```
|
|
A
|
|
/ \
|
|
B C
|
|
/ \
|
|
D E
|
|
```
|
|
|
|
Here is what happen whenever we mount the root
|
|
component (with some code like `app.mount(document.body)`).
|
|
|
|
1. `willStart` is called on `A`
|
|
|
|
2. when it is done, template `A` is rendered.
|
|
|
|
- widget `B` is created
|
|
1. `willStart` is called on `B`
|
|
2. template `B` is rendered
|
|
- widget `C` is created
|
|
1. `willStart` is called on `C`
|
|
2. template `C` is rendered
|
|
- widget `D` is created
|
|
1. `willStart` is called on `D`
|
|
2. template `D` is rendered
|
|
- widget `E` is created
|
|
1. `willStart` is called on `E`
|
|
2. template `E` is rendered
|
|
|
|
3. widget `A` is patched into a detached DOM element. This will create the actual
|
|
widget `A` DOM structure. The patching process will cause recursively the
|
|
patching of the `B`, `C`, `D` and `E` DOM trees. (so the actual full DOM tree is created
|
|
in one pass)
|
|
|
|
4. the widget `A` root element is actually appended to `document.body`
|
|
|
|
5. The method `mounted` is called recursively on all widgets in the following
|
|
order: `B`, `D`, `E`, `C`, `A`.
|
|
|
|
**Scenario 2: rerendering a component**. Now, let's assume that the user clicked on some
|
|
button in `C`, and this results in a state update, which is supposed to:
|
|
|
|
- update `D`,
|
|
- remove `E`,
|
|
- add new widget `F`.
|
|
|
|
So, the component tree should look like this:
|
|
|
|
```
|
|
A
|
|
/ \
|
|
B C
|
|
/ \
|
|
D F
|
|
```
|
|
|
|
Here is what Owl will do:
|
|
|
|
1. because of a state change, the method `render` is called on `C`
|
|
2. template `C` is rendered again
|
|
|
|
- widget `D` is updated:
|
|
1. hook `willUpdateProps` is called on `D` (async)
|
|
2. template `D` is rerendered
|
|
- widget `F` is created:
|
|
1. hook `willStart` is called on `E` (async)
|
|
2. template `F` is rendered
|
|
|
|
3. `willPatch` hooks are called recursively on widgets `C`, `D` (not on `F`,
|
|
because it is not mounted yet)
|
|
|
|
4. widget `C` is patched, which will cause recursively:
|
|
|
|
2. `willUnmount` hook on `E`, then destruction of `E`,
|
|
3. (initial) patching of `F`, then hook `mounted` is called on `F`
|
|
|
|
5. patching of `D`
|
|
|
|
6. `patched` hooks are called on `D`, `C`
|
|
|
|
## Asynchronous rendering
|
|
|
|
Working with asynchronous code always adds a lot of complexity to a system. Whenever
|
|
different parts of a system are active at the same time, one needs to think
|
|
carefully about all possible interactions. Clearly, this is also true for Owl
|
|
components.
|
|
|
|
There are two different common problems with Owl asynchronous rendering model:
|
|
|
|
- any widget can delay the rendering (initial and subsequent) of the whole
|
|
application
|
|
- for a given widget, there are two independant situations that will trigger an
|
|
asynchronous rerendering: a change in the state, or a change in the props.
|
|
These changes may be done at different times, and Owl has no way of knowing
|
|
how to reconcile the resulting renderings.
|
|
|
|
Here are a few tips on how to work with asynchronous widgets:
|
|
|
|
1. minimize the use of asynchronous widgets!
|
|
2. Maybe move the asynchronous logic in a store, which then triggers (mostly)
|
|
synchronous renderings
|
|
3. Lazy loading external libraries is a good use case for async rendering. This
|
|
is mostly fine, because we can assume that it will only takes a fraction of a
|
|
second, and only once (see `owl.utils.loadJS`)
|