[IMP] components: add hooks mechanism

part of #194
This commit is contained in:
Géry Debongnie
2019-09-24 14:06:53 +02:00
parent 4513e3f31c
commit 5335fb8fba
28 changed files with 781 additions and 358 deletions
+51 -39
View File
@@ -45,16 +45,16 @@ exclusively done by a [QWeb](qweb.md) template (which needs to be preloaded in Q
Rendering a component generates a virtual dom representation
of the component, 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
const { useState } = owl.hooks;
class ClickCounter extends owl.Component {
state = { value: 0 };
state = useState({ value: 0 });
increment() {
this.state.value++;
@@ -74,9 +74,27 @@ latest browsers without a transpilation step.
This example show how a component should be defined: it simply subclasses the
Component class. If no static `template` key is defined, then
Owl will use the component's name as template name. Here,
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.
a state object is defined, by using the `useState` hook. It is not mandatory to use the state object, but it is certainly encouraged. The result of the `useState` call is
[observed](observer.md), and any change to it will cause a rerendering.
## Reactive system
OWL components can be made reactive by observing some part of their state. See the [hooks](hooks.md) section for more details.
The main idea is that the `useState` hook generate a proxy version of an object
(this is done by an [observer](observer.md)), which allows the component to
react to any change.
```javascript
const { useState } = owl.hooks;
class SomeComponent extends owl.Component {
state = useState({ a: 0, b: 1 });
}
```
Note that there is an important limitation: hooks need to be called in the
constructor.
## Reference
@@ -96,10 +114,6 @@ find a template with the component name (or one of its ancestor).
- **`env`** (Object): the component environment, which contains a QWeb instance.
- **`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 component 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.
@@ -236,7 +250,7 @@ component.
```javascript
constructor(parent, props) {
super(parent, props);
this.state = {someValue: true};
this.state = useState({someValue: true});
this.template = 'mytemplate';
}
```
@@ -246,7 +260,7 @@ implemented in most cases:
```javascript
class ClickCounter extends owl.Component {
state = { value: 0 };
state = useState({ value: 0 });
...
}
@@ -271,9 +285,6 @@ At this point, the component is not yet rendered. Note that a slow `willStart` m
interface. Therefore, some care should be made to make this method as
fast as possible.
After the `willStart` method is completed, the state will be observed with a
new `Observer`. Then, the component will be rendered by `QWeb`.
#### `mounted()`
`mounted` is called each time a component is attached to the
@@ -287,10 +298,9 @@ 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.
It is 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)`
@@ -315,7 +325,7 @@ 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
Note that modifying the state 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 component is not in the DOM (this can
happen with components with `t-keepalive`).
@@ -397,9 +407,9 @@ easy to test a component.
Updating the environment is not as simple as changing a component's state: its
content is not observed, so updates will not be reflected immediately in the
user interface. There is however a mechanism to force root widgets to rerender
themselves whenever the environment is modified: one only needs to trigger the
`update` event on the QWeb instance. For example, a responsive environment
could be programmed like this:
themselves whenever the environment is modified: one only needs to call the
`forceUpdate` method on the QWeb instance. For example, a responsive environment
could be done like this:
```js
function setupResponsivePlugin(env) {
@@ -408,7 +418,7 @@ function setupResponsivePlugin(env) {
const updateEnv = owl.utils.debounce(() => {
if (env.isMobile !== isMobile()) {
env.isMobile = !env.isMobile;
env.qweb.trigger("update");
env.qweb.forceUpdate();
}
}, 15);
window.addEventListener("resize", updateEnv);
@@ -442,7 +452,8 @@ dynamic. If it is necessary to give a string, this can be done by quoting it:
`someString="'somevalue'"`.
Note that the rendering context for the template is the component itself. This means
that the template can access `state`, `props`, `env`, or any methods defined in the component.
that the template can access `state` (if it exists), `props`, `env`, or any
methods defined in the component.
```xml
<div t-name="ParentComponent">
@@ -453,7 +464,7 @@ that the template can access `state`, `props`, `env`, or any methods defined in
```js
class ParentComponent {
static components = { ChildComponent };
state = { val: 4 };
state = useState({ val: 4 });
}
```
@@ -642,7 +653,7 @@ form!). A possible way to do this is to do it by hand:
```js
class Form extends owl.Component {
state = { text: "" };
state = useState({ text: "" });
_updateInputValue(event) {
this.state.text = event.target.value;
@@ -662,8 +673,9 @@ plumbing code is slightly different if you need to interact with a checkbox,
or with radio buttons, or with select tags.
To help with this situation, Owl has a builtin directive `t-model`: its value
is the (top-level) name in the state object. With the `t-model` directive, we
can write a shorter code, equivalent to the previous example:
should be an observed value in the component (usually `state.someValue`). With
the `t-model` directive, we can write a shorter code, equivalent to the previous
example:
```js
class Form extends owl.Component {
@@ -673,7 +685,7 @@ class Form extends owl.Component {
```xml
<div>
<input t-model="text" />
<input t-model="state.text" />
<span t-esc="state.text" />
</div>
```
@@ -683,11 +695,11 @@ The `t-model` directive works with `<input>`, `<input type="checkbox">`,
```xml
<div>
<div>Text in an input: <input t-model="someVal"/></div>
<div>Textarea: <textarea t-model="otherVal"/></div>
<div>Boolean value: <input type="checkbox" t-model="someFlag"/></div>
<div>Text in an input: <input t-model="state.someVal"/></div>
<div>Textarea: <textarea t-model="state.otherVal"/></div>
<div>Boolean value: <input type="checkbox" t-model="state.someFlag"/></div>
<div>Selection:
<select t-model="color">
<select t-model="state.color">
<option value="">Select a color</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
@@ -696,11 +708,11 @@ The `t-model` directive works with `<input>`, `<input type="checkbox">`,
<div>
Selection with radio buttons:
<span>
<input type="radio" name="color" id="red" value="red" t-model="color"/>
<input type="radio" name="color" id="red" value="red" t-model="state.color"/>
<label for="red">Red</label>
</span>
<span>
<input type="radio" name="color" id="blue" value="blue" t-model="color" />
<input type="radio" name="color" id="blue" value="blue" t-model="state.color" />
<label for="blue">Blue</label>
</span>
</div>
@@ -718,7 +730,7 @@ Like event handling, the `t-model` directive accepts some modifiers:
For example:
```xml
<input t-model.lazy="someVal" />
<input t-model.lazy="state.someVal" />
```
These modifiers can be combined. For instance, `t-model.lazy.number` will only
@@ -1117,8 +1129,8 @@ For example, here is how we could implement an `ErrorBoundary` component:
```
```js
class ErrorBoundary extends Widget {
state = { error: false };
class ErrorBoundary extends Component {
state = useState({ error: false });
catchError() {
this.state.error = true;
+136
View File
@@ -0,0 +1,136 @@
# 🦉 Hooks 🦉
## Content
- [Overview](#overview)
- [Example](#example)
- [Reference](#reference)
- [One Rule](#one-rule)
- [`useState`](#usestate)
- [`onMounted`](#onmounted)
- [`onWillUnmount`](#onWillUnmount)
## Overview
Hooks were popularised by React as a way to solve the following issues:
- help reusing stateful logic between components
- help organizing code by feature in complex components
- use state in functional components, without writing a class.
Owl hooks serve the same purpose, except that they work for class components
(note: React hooks do not work on class components, and maybe because of that,
there seems to be the misconception that hooks are in opposition to class. This
is clearly not true, as shown by Owl hooks).
Hooks works beautifully with Owl components: they solve the problems mentioned
above, and in particular, they are the perfect way to make your component
reactive.
## Example
Here is the classical example of a non trivial hook to track the mouse position.
```js
const {useState, onMounted, onWillUnmount} = owl.hooks;
// We define here a custom behaviour: this hook tracks the state of the mouse
// position
function useMouse() {
const position = useState({x:0, y: 0});
function update(e) {
position.x = e.clientX;
position.y = e.clientY;
}
onMounted(() => {
window.addEventListener('mousemove', update);
});
onWillUnmount(() => {
window.removeEventListener('mousemove', update);
});
return position;
}
// Main root component
class App extends owl.Component {
static template = xml`
<div t-name="App">
<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>
</div>`;
// this hooks is bound to the 'mouse' property.
mouse = useMouse();
}
```
Note that we use the prefix `use` for hooks, just like in React. This is just
a convention.
## Reference
### One rule
There is only one rule: every hook for a component have to be called in the
constructor (or in class fields):
```js
// ok
class SomeComponent extends Component {
state = useState();
}
// also ok
class SomeComponent extends Component {
constructor(...args) {
super(...args);
this.state = useState();
}
}
// not ok: this is executed after the constructor is called
class SomeComponent extends Component {
async willStart() {
this.state = useState();
}
}
```
### `useState`
The `useState` hook is certainly the most important hooks for Owl components:
this is what enables component to be reactive, to react to state change.
The `useState` hook has to be given an object or an array, and will return
an observed version of it (using a `Proxy`).
```javascript
const { useState } = owl.hooks;
class Counter extends owl.Component {
static template = xml`
<button t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>`;
state = useState({ value: 0 });
increment() {
this.state.value++;
}
}
```
### `onMounted`
`onMounted` is not an hook, but is a building block designed to help make useful
hooks. `onMounted` register a callback, which will be called when the component
is mounted (see example on top of this page).
### `onWillUnmount`
`onWillUnmount` is not an hook, but is a building block designed to help make useful
hooks. `onWillUnmount` register a callback, which will be called when the component
is unmounted (see example on top of this page).
+3 -33
View File
@@ -11,40 +11,10 @@ For example, this code will display `update` in the console:
```javascript
const observer = new owl.Observer();
observer.notifyCB = () => console.log("update");
observer.observe(obj);
const obj = observer.observe( { a: { b: 1 } });
const obj = { a: { b: 1 } };
obj.a.b = 2;
```
## Technical Limitations
Since the observer uses getters and setters, it is actually unable to react to
changes in three situations:
- adding a key to an object
- deleting a key from an object
- modifying an array by setting a new value at a given index
In those situations, we need a way to tell the observer that something happened.
This can be done by using the `set` and `delete` (only for objects) static
methods of the `Observer`.
```javascript
const observer = new owl.Observer();
const obj = { a: 1 };
observer.observe(obj);
obj.b = 2; // won't notify the change
owl.Observer.set(obj, "b", 2); // will notify the change
delete obj.b; // won't notify the change
owl.Observer.delete(obj, "b"); // will notify the change
```
```javascript
const observer = new owl.Observer();
const arr = ["a"];
observer.observe(arr);
arr[0] = "b"; // won't notify the change
owl.Observer.set(arr, 0, "b"); // will notify the change
```
The observer is implemented with the native `Proxy` object. Note that this
means that it will not work on older browsers.
+3 -1
View File
@@ -72,11 +72,13 @@ Here are a few steps that we may take to get started:
Let us now add the javascript to make it work, in `app.js`:
```javascript
const useState = owl.hooks.useState;
class ClickCounter extends owl.Component {
static template = "clickcounter";
constructor() {
super(...arguments);
this.state = { value: 0 };
this.state = useState({ value: 0 });
}
increment() {
+8
View File
@@ -9,9 +9,14 @@ build applications. Here is a complete representation of its content:
owl
Component
QWeb
useState
core
EventBus
Observer
hooks
onMounted
onWillUnmount
useState
router
Link
RouteComponent
@@ -29,11 +34,14 @@ owl
whenReady
```
Note that for convenience, the `useState` hook is also exported at the root of the `owl` object.
## Reference
- [Animations](animations.md)
- [Component](component.md)
- [Event Bus](event_bus.md)
- [Hooks](hooks.md)
- [Observer](observer.md)
- [QWeb](qweb.md)
- [Router](router.md)