[DOC] doc: rewrite component/quick start documentation

This commit is contained in:
Géry Debongnie
2019-04-24 11:54:26 +02:00
parent 379cd2a90f
commit d509373228
7 changed files with 299 additions and 101 deletions
+205 -51
View File
@@ -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
View File
@@ -1,2 +1,6 @@
# Observer
# 🦉 Observer 🦉
## Technical Limitations
Cannot observe new keys => need to use special method.
+84 -44
View File
@@ -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
View File
@@ -1,4 +1,4 @@
# QWeb
# 🦉 QWeb 🦉
QWeb is a template specification. The QWeb class in this repository is:
+1 -1
View File
@@ -1,4 +1,4 @@
# Odoo Web Library Documentation
# 🦉 Odoo Web Library 🦉
## Reference
+1 -1
View File
@@ -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
View File
@@ -1,3 +1,3 @@
# VDom
# 🦉 VDom 🦉
todo
Owl's virtual dom is a fork of [snabbdom](https://github.com/snabbdom/snabbdom).