mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[DOC] doc: rewrite component/quick start documentation
This commit is contained in:
+205
-51
@@ -1,6 +1,17 @@
|
||||
# Component
|
||||
# 🦉 OWL Component 🦉
|
||||
|
||||
The component system is designed to be:
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Example](#example)
|
||||
- [Reference](#reference)
|
||||
- [Properties](#properties)
|
||||
- [Methods](#methods)
|
||||
- [Lifecycle](#lifecycle)
|
||||
|
||||
## 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.
|
||||
@@ -15,98 +26,232 @@ The component system is designed to be:
|
||||
4. **uses QWeb as a template system:** the templates are described in XML
|
||||
and follow the QWeb specification. This is a requirement for Odoo.
|
||||
|
||||
Components are the reusable, composable widgets. They are designed to be low
|
||||
level, to be declarative, and with asynchronous rendering.
|
||||
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).
|
||||
|
||||
For example:
|
||||
## Example
|
||||
|
||||
Let us have a look at a simple component:
|
||||
|
||||
```javascript
|
||||
export class Counter extends Component {
|
||||
template = "counter";
|
||||
state = { counter: 0 };
|
||||
class ClickCounter extends owl.Component {
|
||||
template = "click-counter";
|
||||
state = { value: 0 };
|
||||
|
||||
constructor(parent, props) {
|
||||
super(parent, props);
|
||||
this.state.counter = props.initialState || 0;
|
||||
}
|
||||
|
||||
increment(delta) {
|
||||
this.state.counter += delta;
|
||||
increment() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```xml
|
||||
<div t-name="counter">
|
||||
<button t-on-click="increment(-1)">-</button>
|
||||
<span style="font-weight:bold">Value: <t t-esc="state.counter"/></span>
|
||||
<button t-on-click="increment(1)">+</button>
|
||||
</div>`;
|
||||
<button t-name="click-counter" t-on-click="increment">
|
||||
Click Me! [<t t-esc="state.value"/>]
|
||||
</button>
|
||||
```
|
||||
|
||||
## Lifecycle
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
||||
- **`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.
|
||||
|
||||
- **`updateProps(nextProps)`**: should not be called manually, except on the root
|
||||
component. This method is only supposed to be called by the framework whenever
|
||||
a parent is rerendered.
|
||||
|
||||
- **`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 description of the lifecycle of a owl
|
||||
component:
|
||||
developers write components. Here is a complete description of the lifecycle of
|
||||
a owl component:
|
||||
|
||||
- **[constructor](#constructor)**
|
||||
- **[willStart](#willStart)**
|
||||
- **[mounted](#mounted)**
|
||||
- **[willPatch](#willPatch)**
|
||||
- **[updated](#updated)**
|
||||
- **[willUnmount](#willUnmount)**
|
||||
| Method | Description |
|
||||
| --------------------------------------- | --------------------------------------- |
|
||||
| **[constructor](#constructor)** | constructor |
|
||||
| **[willStart](#willStart)** | async, before first rendering |
|
||||
| **[mounted](#mounted)** | when component is render and in DOM |
|
||||
| **[willUpdateProps](#willUpdateProps)** | async, before props update |
|
||||
| **[willPatch](#willPatch)** | just before the DOM is patched |
|
||||
| **[patched](#patched)** | just after the DOM is patched |
|
||||
| **[willUnmount](#willUnmount)** | just before removing component from DOM |
|
||||
|
||||
Note: no hook method should ever be called manually. They are supposed to be
|
||||
called by the owl framework whenever it is required.
|
||||
|
||||
### constructor
|
||||
#### `constructor(parent, props)`
|
||||
|
||||
The constructor is not exactly a hook, it is the constructor of the component.
|
||||
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.
|
||||
|
||||
### willStart
|
||||
This is usually where you would set the initial state and the template of the
|
||||
component.
|
||||
|
||||
willStart is an asynchronous hook that can be implemented to perform some
|
||||
action before the initial rendering of a 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.
|
||||
|
||||
Note that a slow willStart method will slow down the rendering of the user
|
||||
interface. Therefore, some effort should be made to make this method as
|
||||
```javascript
|
||||
willStart() {
|
||||
// we assume that utils.lazyLoad return a promise
|
||||
return utils.lazyLoad('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.
|
||||
|
||||
### mounted
|
||||
The widget rendering will take place after `willStart` is completed.
|
||||
|
||||
mounted is a hook that is called each time a component is attached to the
|
||||
DOM. This is a good place to add some listeners, or to interact with the
|
||||
#### `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
|
||||
be unmounted at some point.
|
||||
It is the opposite of `willUnmount`. If a component has been mounted, it will
|
||||
always be unmounted at some point in the future.
|
||||
|
||||
### willUpdateProps
|
||||
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
|
||||
#### `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 get some
|
||||
information which are in the DOM. For example, the current position of the
|
||||
scrollbar
|
||||
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.
|
||||
|
||||
### patched
|
||||
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.
|
||||
|
||||
This hook is called whenever a component did actually update its props,
|
||||
state or env.
|
||||
#### `patched()`
|
||||
|
||||
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
|
||||
@@ -117,10 +262,19 @@ 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()`
|
||||
|
||||
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.
|
||||
|
||||
This is the opposite method of _mounted_. The _willUnmount_ method will be
|
||||
```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`. The `willUnmount` method will be
|
||||
called in reverse order: first the children, then the parents.
|
||||
|
||||
+5
-1
@@ -1,2 +1,6 @@
|
||||
# Observer
|
||||
# 🦉 Observer 🦉
|
||||
|
||||
|
||||
## Technical Limitations
|
||||
|
||||
Cannot observe new keys => need to use special method.
|
||||
+84
-44
@@ -1,11 +1,67 @@
|
||||
# Quick Start
|
||||
# 🦉 Quick Start 🦉
|
||||
|
||||
## Static server
|
||||
|
||||
Let us assume that we have a static server running somewhere. We could then
|
||||
simply add an html page with a few extra files.
|
||||
|
||||
### HTML and CSS
|
||||
|
||||
In a file `index.html`:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>My OWL App</title>
|
||||
<link href="app.css" rel="stylesheet" />
|
||||
<script src="owl-X.Y.Z.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="main"></div>
|
||||
<script src="app.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
In `app.css`:
|
||||
|
||||
```css
|
||||
button {
|
||||
color: darkred;
|
||||
font-size: 30px;
|
||||
width: 220px;
|
||||
}
|
||||
```
|
||||
|
||||
Also, let's not forget to add a release of OWL (`owl-X.Y.Z.js`)
|
||||
|
||||
### XML
|
||||
|
||||
In `templates.xml`:
|
||||
|
||||
```xml
|
||||
<templates>
|
||||
<button t-name="clickcounter" t-on-click="increment">
|
||||
Click Me! [<t t-esc="state.value"/>]
|
||||
</button>
|
||||
</templates>
|
||||
```
|
||||
|
||||
### JS
|
||||
|
||||
To build an application (or a sub-part of an application), we need two things:
|
||||
|
||||
- an environment
|
||||
- a root widget
|
||||
- an environment: it is the global context in which we are working. It needs to
|
||||
contain a QWeb instance (preloaded with templates), and anything else that we
|
||||
need. In practice, it could context some user session information, some
|
||||
configuration keys (for example, isMobile = true/false if we are in mobile mode).
|
||||
|
||||
Here are a few steps that may be useful to get started:
|
||||
- a description of the user interface: there should be a root widget, which can
|
||||
have sub widgets
|
||||
|
||||
Here are a few steps that we may take to get started:
|
||||
|
||||
- get the templates
|
||||
- create a qweb engine, with the templates
|
||||
@@ -13,49 +69,33 @@ Here are a few steps that may be useful to get started:
|
||||
- create an instance of the root widget
|
||||
- mount the root widget to a DOM element
|
||||
|
||||
## Simple example
|
||||
|
||||
In a oversimplified example, here is what it could look like:
|
||||
Let us now add the javascript to make it work, in `app.js`:
|
||||
|
||||
```javascript
|
||||
const templates = await loadTemplates();
|
||||
class ClickCounter extends owl.Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.template = "clickcounter";
|
||||
this.state = { value: 0 };
|
||||
}
|
||||
|
||||
const qweb = new QWeb();
|
||||
qweb.loadTemplates(templates);
|
||||
increment() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
|
||||
const env = {
|
||||
qweb: qweb
|
||||
};
|
||||
//------------------------------------------------------------------------------
|
||||
// Application initialization
|
||||
//------------------------------------------------------------------------------
|
||||
async function start() {
|
||||
const templates = await owl.utils.loadTemplates("templates.xml");
|
||||
const env = {
|
||||
qweb: new owl.QWeb(templates)
|
||||
};
|
||||
const counter = new ClickCounter(env);
|
||||
const target = document.getElementById("main");
|
||||
await counter.mount(target);
|
||||
}
|
||||
|
||||
const root = new RootWidget(env, { initialState: 17 });
|
||||
|
||||
const target = document.getElementById("app");
|
||||
root.mount(target);
|
||||
```
|
||||
|
||||
## Managing state
|
||||
|
||||
The environment is propagated to each subchildren. So, it is convenient to use
|
||||
to give a reference to some global object. This is potentially very useful
|
||||
in a redux-like architecture:
|
||||
|
||||
```javascript
|
||||
...
|
||||
|
||||
const store = new Store();
|
||||
|
||||
const env = {
|
||||
qweb: qweb,
|
||||
store: store,
|
||||
};
|
||||
|
||||
const state = store.getState()
|
||||
const root = new RootWidget(env, state);
|
||||
|
||||
store.on('state_change', nextState => {
|
||||
root.updateProps(nextState);
|
||||
});
|
||||
|
||||
const target = document.getElementById('app');
|
||||
root.mount(target);
|
||||
start();
|
||||
```
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# QWeb
|
||||
# 🦉 QWeb 🦉
|
||||
|
||||
QWeb is a template specification. The QWeb class in this repository is:
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Odoo Web Library Documentation
|
||||
# 🦉 Odoo Web Library 🦉
|
||||
|
||||
|
||||
## Reference
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# OWL: State Management
|
||||
# 🦉 Store 🦉
|
||||
|
||||
Managing the state in an application is not an easy task. Many different
|
||||
architectures/designs/systems/... have been created. We propose here to use
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
# VDom
|
||||
# 🦉 VDom 🦉
|
||||
|
||||
todo
|
||||
Owl's virtual dom is a fork of [snabbdom](https://github.com/snabbdom/snabbdom).
|
||||
Reference in New Issue
Block a user