[DOC] improve the documentation

closes #340
closes #344
closes #352
closes #355
This commit is contained in:
Géry Debongnie
2019-10-20 09:01:38 +02:00
parent 1bb4577ec1
commit 29c2c5b9c9
7 changed files with 122 additions and 91 deletions
+32 -30
View File
@@ -91,22 +91,22 @@ find a template with the component name (or one of its ancestors).
### Reactive system ### Reactive system
OWL components are normal javascript classes. So, changing a component internal OWL components are normal javascript classes. So, changing a component internal
state does nothing more: state does nothing more:
```js ```js
class Counter extends Component { class Counter extends Component {
static template = xml`<div t-on-click="increment"><t t-esc="state.value"/></div>`; static template = xml`<div t-on-click="increment"><t t-esc="state.value"/></div>`;
state = {value: 0}; state = { value: 0 };
increment() { increment() {
this.state.value++; this.state.value++;
} }
} }
``` ```
Clicking on the `Counter` component defined above will call the `increment` Clicking on the `Counter` component defined above will call the `increment`
method, but it will not rerender the component. To fix that, one could add an method, but it will not rerender the component. To fix that, one could add an
explicit call to `render` in `increment`: explicit call to `render` in `increment`:
```js ```js
@@ -122,7 +122,7 @@ method.
A better way is to use the reactive system: by using the `useState` hook (see the A better way is to use the reactive system: by using the `useState` hook (see the
[hooks](hooks.md) section for more details), one can make Owl react to state [hooks](hooks.md) section for more details), one can make Owl react to state
changes. The `useState` hook generates a proxy version of an object changes. The `useState` hook generates a proxy version of an object
(this is done by an [observer](observer.md)), which allows the component to (this is done by an [observer](observer.md)), which allows the component to
react to any change. So, the `Counter` example above can be improved like this: react to any change. So, the `Counter` example above can be improved like this:
@@ -130,31 +130,32 @@ react to any change. So, the `Counter` example above can be improved like this:
const { useState } = owl.hooks; const { useState } = owl.hooks;
class Counter extends Component { class Counter extends Component {
static template = xml`<div t-on-click="increment"><t t-esc="state.value"/></div>`; static template = xml`<div t-on-click="increment"><t t-esc="state.value"/></div>`;
state = useState({value: 0}); state = useState({ value: 0 });
increment() { increment() {
this.state.value++; this.state.value++;
} }
} }
``` ```
Obviously, we can call the `useState` hook more than once: Obviously, we can call the `useState` hook more than once:
```js ```js
const { useState } = owl.hooks; const { useState } = owl.hooks;
class Counter extends Component { class Counter extends Component {
static template = xml` static template = xml`
<div> <div>
<span t-on-click="increment(counter1)"><t t-esc="counter1.value"/></span> <span t-on-click="increment(counter1)"><t t-esc="counter1.value"/></span>
<span t-on-click="increment(counter2)"><t t-esc="counter2.value"/></span> <span t-on-click="increment(counter2)"><t t-esc="counter2.value"/></span>
</div>`; </div>`;
counter1 = useState({value: 0}); counter1 = useState({ value: 0 });
counter2 = useState({value: 0}); counter2 = useState({ value: 0 });
increment(counter) { increment(counter) {
counter.value++; counter.value++;
} }
} }
``` ```
@@ -169,7 +170,7 @@ to be called in the constructor.
- **`env`** (Object): the component environment, which contains a QWeb instance. - **`env`** (Object): the component environment, which contains a QWeb instance.
- **`props`** (Object): this is an object containing all the properties given by - **`props`** (Object): this is an object containing all the properties given by
the parent to a child component. For example, in the following situation, the parent to a child component. For example, in the following situation,
the parent component gives a `user` and a `color` value to the `ChildComponent`. the parent component gives a `user` and a `color` value to the `ChildComponent`.
```xml ```xml
@@ -182,7 +183,7 @@ to be called in the constructor.
As such, it should not ever be modified by the component (otherwise you risk As such, it should not ever be modified by the component (otherwise you risk
unintended effects, since the parent may not be aware of the change)!! unintended effects, since the parent may not be aware of the change)!!
The `props` can be modified dynamically by the parent. In that case, the The `props` can be modified dynamically by the parent. In that case, the
component will go through the following lifecycle methods: `willUpdateProps`, component will go through the following lifecycle methods: `willUpdateProps`,
`willPatch` and `patched`. `willPatch` and `patched`.
@@ -405,8 +406,8 @@ component was patched. Note that this hook will not be called if the compoent is
not in the DOM (this can happen with components with `t-keepalive`). not in the DOM (this can happen with components with `t-keepalive`).
Updating the component state in this hook is possible, but not encouraged. Updating the component state in this hook is possible, but not encouraged.
One need to be careful, because updates here will cause rerender, which in One needs to be careful, because updates here will create an additional rendering, which in
turn will cause other calls to patched. So, we need to be particularly turn will cause other calls to the `patched` method. So, we need to be particularly
careful at avoiding endless cycles. careful at avoiding endless cycles.
#### `willUnmount()` #### `willUnmount()`
@@ -429,7 +430,7 @@ This is the opposite method of `mounted`.
The `catchError` method is useful when we need to intercept and properly react The `catchError` method is useful when we need to intercept and properly react
to (rendering) errors that occur in some sub components. See the section on to (rendering) errors that occur in some sub components. See the section on
[error handling](#error-handling) [error handling](#error-handling).
### Root Component ### Root Component
@@ -783,7 +784,7 @@ Like event handling, the `t-model` directive accepts some modifiers:
| Modifier | Description | | Modifier | Description |
| --------- | -------------------------------------------------------------------- | | --------- | -------------------------------------------------------------------- |
| `.lazy` | update the value on the `change` event (default is on `input` event) | | `.lazy` | update the value on the `change` event (default is on `input` event) |
| `.number` | tries to parse the value to a number (using `parseFloat`) | | `.number` | try to parse the value to a number (using `parseFloat`) |
| `.trim` | trim the resulting value | | `.trim` | trim the resulting value |
For example: For example:
@@ -799,9 +800,10 @@ Note: the online playground has an example to show how it works.
### `t-key` Directive ### `t-key` Directive
Even though Owl tries to be as declarative as possible, some DOM state is still Even though Owl tries to be as declarative as possible, the DOM does not fully
locked inside the DOM: for example, the scrolling state, the current user selection, expose its state declaratively in the DOM tree. For example, the scrolling state,
the focused element or the state of an input. This is why we use a virtual dom the current user selection, the focused element or the state of an input are not
set as attribute in the DOM tree. This is why we use a virtual dom
algorithm to keep the actual DOM node as much as possible. However, this is algorithm to keep the actual DOM node as much as possible. However, this is
sometimes not enough, and we need to help Owl decide if an element is actually sometimes not enough, and we need to help Owl decide if an element is actually
the same, or is different. The `t-key` directive is used to give an identity to an element. the same, or is different. The `t-key` directive is used to give an identity to an element.
@@ -913,7 +915,7 @@ Here is what Owl will do:
As an application becomes complex, it may be quite unsafe to define props in an informal way. This leads to two issues: As an application becomes complex, it may be quite unsafe to define props in an informal way. This leads to two issues:
- hard to tell how a component should be used, by looking at its code. - hard to tell how a component should be used, by looking at its code.
- unsafe, it is easy to send wrong props into a component, either by refactoring a component, or one of its parent. - unsafe, it is easy to send wrong props into a component, either by refactoring a component, or one of its parents.
A props type system would solve both issues, by describing the types and shapes A props type system would solve both issues, by describing the types and shapes
of the props. Here is how it works in Owl: of the props. Here is how it works in Owl:
@@ -1169,7 +1171,7 @@ from lifecycle hooks): the `catchError` hook.
Whenever the `catchError` lifecycle hook is implemented, all errors coming from Whenever the `catchError` lifecycle hook is implemented, all errors coming from
sub components rendering and/or lifecycle method calls will be caught and given sub components rendering and/or lifecycle method calls will be caught and given
to the `catchError` method. This allow us to properly handle the error, and to to the `catchError` method. This allows us to properly handle the error, and to
not break the application. not break the application.
For example, here is how we could implement an `ErrorBoundary` component: For example, here is how we could implement an `ErrorBoundary` component:
+1 -1
View File
@@ -11,7 +11,7 @@
## Overview ## Overview
The `Context` object provides a way to share data between an arbitrary number The `Context` object provides a way to share data between an arbitrary number
of component. Usually, data is passed from a parent to its children component, of components. Usually, data is passed from a parent to its children component,
but when we have to deal with some mostly global information, this can be but when we have to deal with some mostly global information, this can be
annoying, since each component will need to pass the information to each children, annoying, since each component will need to pass the information to each children,
even though some or most of them will not use the information. even though some or most of them will not use the information.
+24 -24
View File
@@ -220,18 +220,18 @@ be ready whenever the component is rendered:
```js ```js
function useLoader() { function useLoader() {
const component = Component.current; const component = Component.current;
const record = useState({}); const record = useState({});
onWillStart(async () => { onWillStart(async () => {
const recordId = component.props.id; const recordId = component.props.id;
Object.assign(record, await fetchSomeRecord(recordId)); Object.assign(record, await fetchSomeRecord(recordId));
}); });
return record; return record;
} }
``` ```
Note that this example does not update the record value whenever props are Note that this example does not update the record value whenever props are
updated. For that situation, we need to use the `onWillUpdateProps` hook. updated. For that situation, we need to use the `onWillUpdateProps` hook.
### `onWillUpdateProps` ### `onWillUpdateProps`
@@ -241,17 +241,17 @@ useful to perform some asynchronous task such as fetching updated data.
```js ```js
function useLoader() { function useLoader() {
const component = Component.current; const component = Component.current;
const record = useState({}); const record = useState({});
async function updateRecord(id) { async function updateRecord(id) {
Object.assign(record, await fetchSomeRecord(id)); Object.assign(record, await fetchSomeRecord(id));
} }
onWillStart(() => updateRecord(component.props.id)); onWillStart(() => updateRecord(component.props.id));
onWillUpdateProps(nextProps => updateRecord(nextProps.id)); onWillUpdateProps(nextProps => updateRecord(nextProps.id));
return record; return record;
} }
``` ```
@@ -359,7 +359,7 @@ getters. See the [store documentation](store.md) for more information.
### Making customized hooks ### Making customized hooks
Hooks are a wonderful way to organize the code of a complex component by feature Hooks are a wonderful way to organize the code of a complex component by feature
instead of by lifecycle methods. They are like mixins, except that they can be instead of by lifecycle methods. They are like mixins, except that they can be
easily composed together. easily composed together.
But, like every good things in life, hooks should be used with moderation. They are But, like every good things in life, hooks should be used with moderation. They are
@@ -373,16 +373,16 @@ not the solution to every problem.
// maybe overkill // maybe overkill
class A extends Component { class A extends Component {
constructor(...args) { constructor(...args) {
super(...args); super(...args);
useMySpecificHook() useMySpecificHook();
} }
} }
// ok // ok
class B extends Component { class B extends Component {
constructor(...args) { constructor(...args) {
super(...args); super(...args);
this.performSpecificTask() this.performSpecificTask();
} }
} }
``` ```
@@ -391,7 +391,7 @@ not the solution to every problem.
- they may be harder to test: if a customized hook injects some external side - they may be harder to test: if a customized hook injects some external side
effect dependency, then it is harder to test without doing some non obvious effect dependency, then it is harder to test without doing some non obvious
manipulation. For example, assume that we want to give a reference to a manipulation. For example, assume that we want to give a reference to a
router in a `useRouter` hook. We could do this: router in a `useRouter` hook. We could do this:
```js ```js
@@ -402,7 +402,7 @@ not the solution to every problem.
} }
``` ```
As you can see, this does not *hook* into the internal of the component. It As you can see, this does not _hook_ into the internal of the component. It
simply returns a global object, which is difficult to mock. simply returns a global object, which is difficult to mock.
A better way would be to do something like this: get the reference from the A better way would be to do something like this: get the reference from the
@@ -410,7 +410,7 @@ not the solution to every problem.
```js ```js
function useRouter() { function useRouter() {
return Component.current.env.router; return Component.current.env.router;
} }
``` ```
+38 -6
View File
@@ -1,10 +1,16 @@
# 🦉 Observer 🦉 # 🦉 Observer 🦉
Owl needs to be able to react to state changes. For example, whenever the state Owl needs to be able to react to state changes. For example, whenever the state
of a component is changed, we need to rerender it. To help with that, we have of a component is changed, Owl needs to rerender it. To help with that, there is
an Observer class. Its job is to observe some object state, and react to any an Observer class. Its job is to observe the state of an object (or array), and
change. To do that, it recursively replaces all keys of the observed state by to react to any change. The observer is implemented with the native `Proxy`
getters and setters. object. Note that this means that it will not work on older browsers.
Note that the `Observer` is used by the `useState` and `useContext` hooks. This
is the way most Owl applications will create observers. For the majority of
use cases, there is no need to directly instantiate an observer.
## Example
For example, this code will display `update` in the console: For example, this code will display `update` in the console:
@@ -16,5 +22,31 @@ const obj = observer.observe({ a: { b: 1 } });
obj.a.b = 2; obj.a.b = 2;
``` ```
The observer is implemented with the native `Proxy` object. Note that this This example shows that an observer can observe nested properties.
means that it will not work on older browsers.
## Reference
**observe** An observer can observe multiple values with the `observe` method.
This method takes an object or an array as its argument and will return a proxy
(which is mapped to the initial object/array). With this proxy, the observer
can detect whenever any internal value is changed.
**Registering a callback** Whenever an observer sees a state change, it will
call its `notifyCB` method. No additional information is given to the callback.
**deepRevNumber** Each observed value has an internal revision number, which
is incremented every time the value is observed. Sometimes, it can be useful
to obtain that number:
```js
const observer = new owl.Observer();
const obj = observer.observe({ a: { b: 1 } });
observer.deepRevNumber(obj.a); // 1
obj.a.b = 2;
observer.deepRevNumber(obj.a); // 2
```
The `deepRevNumber` can also return 0, which indicates that the value is not
observed.
-1
View File
@@ -44,7 +44,6 @@ owl
whenReady whenReady
``` ```
Note that for convenience, the `useState` hook is also exported at the root of the `owl` object. Note that for convenience, the `useState` hook is also exported at the root of the `owl` object.
## Reference ## Reference
+5 -7
View File
@@ -38,14 +38,14 @@ const actions = {
state.todos.push({ state.todos.push({
id: state.nextId++, id: state.nextId++,
message, message,
isCompleted: false, isCompleted: false
}); });
} }
}; };
const state = { const state = {
todos: [], todos: [],
nextId: 1, nextId: 1
}; };
const store = new owl.Store({ state, actions }); const store = new owl.Store({ state, actions });
@@ -113,6 +113,7 @@ const actions = {
``` ```
The first argument to an action method is an object with four keys: The first argument to an action method is an object with four keys:
- `state`: the current state of the store content, - `state`: the current state of the store content,
- `dispatch`: a function that can be used to dispatch other actions, - `dispatch`: a function that can be used to dispatch other actions,
- `getters`: an object containing all getters defined in the store, - `getters`: an object containing all getters defined in the store,
@@ -200,8 +201,7 @@ const post = store.getters.getPost(id);
Getters take _at most_ one argument. Getters take _at most_ one argument.
Note that getters are cached if they don't take any argument, or their argument Note that getters are not cached.
is a string or a number.
### Connecting a Component ### Connecting a Component
@@ -212,7 +212,6 @@ can be done with the help of the three store hooks:
- `useDispatch` to get a reference to a dispatch function, - `useDispatch` to get a reference to a dispatch function,
- `useGetters` to get a reference to the getters defined in the store. - `useGetters` to get a reference to the getters defined in the store.
Assume we have this store: Assume we have this store:
```javascript ```javascript
@@ -223,7 +222,7 @@ const actions = {
}; };
const state = { const state = {
counter: {value: 0} counter: { value: 0 }
}; };
const store = new owl.Store({ state, actions }); const store = new owl.Store({ state, actions });
``` ```
@@ -256,7 +255,6 @@ two arguments:
- optionally, an object with a `store` key (if we want to override the default - optionally, an object with a `store` key (if we want to override the default
store) and an equality function (if we want to specialize the comparison). store) and an equality function (if we want to specialize the comparison).
If the `useStore` callback selects a sub part of the store state, the component If the `useStore` callback selects a sub part of the store state, the component
will only be rerendered whenever this part of the state changes. Otherwise, it will only be rerendered whenever this part of the state changes. Otherwise, it
will perform a strict equality check and will update the component every time this will perform a strict equality check and will update the component every time this
+21 -21
View File
@@ -12,7 +12,6 @@ functions are all available in the `owl.utils` namespace.
- [`debounce`](#debounce): limiting rate of function calls - [`debounce`](#debounce): limiting rate of function calls
- [`shallowEqual`](#shallowequal): shallow object comparison - [`shallowEqual`](#shallowequal): shallow object comparison
## `whenReady` ## `whenReady`
The function `whenReady` returns a `Promise` resolved when the DOM is ready (if The function `whenReady` returns a `Promise` resolved when the DOM is ready (if
@@ -20,7 +19,7 @@ not ready yet, resolved directly otherwise). If called with a callback as
argument, it executes it as soon as the DOM ready (or directly). argument, it executes it as soon as the DOM ready (or directly).
```js ```js
Promise.all([loadFile('templates.xml'), owl.utils.whenReady()]).then(function([templates]) { Promise.all([loadFile("templates.xml"), owl.utils.whenReady()]).then(function([templates]) {
const qweb = new owl.QWeb(templates); const qweb = new owl.QWeb(templates);
const app = new App({ qweb }); const app = new App({ qweb });
app.mount(document.body); app.mount(document.body);
@@ -45,6 +44,7 @@ properly reacts when it is ready. Also, it is smart: it maintains a list of urls
previously loaded (or currently being loaded), and prevent doing twice the work. previously loaded (or currently being loaded), and prevent doing twice the work.
For example, it is useful for lazy loading external libraries: For example, it is useful for lazy loading external libraries:
```js ```js
class MyComponent extends owl.Component { class MyComponent extends owl.Component {
willStart() { willStart() {
@@ -55,7 +55,7 @@ class MyComponent extends owl.Component {
## `loadFile` ## `loadFile`
`loadFile` is a helper function to fetch a file. It simply `loadFile` is a helper function to fetch a file. It simply
performs a `GET` request and returns the resulting string in a promise. The performs a `GET` request and returns the resulting string in a promise. The
initial usecase for this function is to load a template file. For example: initial usecase for this function is to load a template file. For example:
@@ -73,38 +73,38 @@ string. It does not add a `script` tag or any other side effect.
## `escape` ## `escape`
Sometimes, we need to display dynamic data (for example user-generated data) in Sometimes, we need to display dynamic data (for example user-generated data) in
the user interface. If this is done by a `QWeb` template, it is not an issue: the user interface. If this is done by a `QWeb` template, it is not an issue:
```xml ```xml
<div><t t-esc="user.data"/></div> <div><t t-esc="user.data"/></div>
``` ```
The `QWeb` engine will create a `div` node and add the content of the `user.data` The `QWeb` engine will create a `div` node and add the content of the `user.data`
string as a text node, so the web browser will not parse it as html. However, string as a text node, so the web browser will not parse it as html. However,
it may be a problem if this is done with some javascript code like this: it may be a problem if this is done with some javascript code like this:
```js ```js
class BadComponent extends Component { class BadComponent extends Component {
// some template with a ref to a div // some template with a ref to a div
// some code ... // some code ...
mounted() { mounted() {
this.divRef.el.innerHTML = this.state.value; this.divRef.el.innerHTML = this.state.value;
} }
} }
``` ```
In this case, the content of the `div` will be parsed as html, which may inject In this case, the content of the `div` will be parsed as html, which may inject
unwanted behaviour. To fix this, the `escape` function will simply transform a unwanted behaviour. To fix this, the `escape` function will simply transform a
string into an escaped version of the same string, which will be properly displayed string into an escaped version of the same string, which will be properly displayed
by the browser, but which will not be parsed as html (for example, `"<ok>"` is by the browser, but which will not be parsed as html (for example, `"<ok>"` is
escaped to the string: `"&lt;ok&gt;"`). So, the bad example above can be fixed escaped to the string: `"&lt;ok&gt;"`). So, the bad example above can be fixed
with the following change: with the following change:
```js ```js
this.divRef.el.innerHTML = owl.utils.escape(this.state.value); this.divRef.el.innerHTML = owl.utils.escape(this.state.value);
``` ```
## `debounce` ## `debounce`
The `debounce` function is useful when we want to limit the number of times some The `debounce` function is useful when we want to limit the number of times some
@@ -112,31 +112,31 @@ function/action is perfomed. For example, this may be useful to prevent issue
with people double clicking on a button. with people double clicking on a button.
It takes three arguments: It takes three arguments:
- `func` (function): this is the function that will be rate limited - `func` (function): this is the function that will be rate limited
- `wait` (number): this is the number of milliseconds that we want to use to - `wait` (number): this is the number of milliseconds that we want to use to
rate limit the function `func` rate limit the function `func`
- `immediate` (optional, boolean, default=false): if `immediate` is true, the - `immediate` (optional, boolean, default=false): if `immediate` is true, the
function will be triggered immediately (leading edge of the interval). If false, function will be triggered immediately (leading edge of the interval). If false,
the function will be triggered at the end (trailing edge). the function will be triggered at the end (trailing edge).
It returns a function. For example: It returns a function. For example:
```js ```js
const debounce = owl.utils.debounce const debounce = owl.utils.debounce;
window.addEventListener('mousemove', debounce(doSomething, 100)); window.addEventListener("mousemove", debounce(doSomething, 100));
``` ```
As this example shows, it is usualy useful for event handlers which are triggered As this example shows, it is usualy useful for event handlers which are triggered
very quickly, such as `scroll` or `mousemove` events. very quickly, such as `scroll` or `mousemove` events.
## `shallowEqual` ## `shallowEqual`
This function checks if two objects have the same values assigned to each keys: This function checks if two objects have the same values assigned to each keys:
```js ```js
shallowEqual({a:1, b: 2}, {a:1, b:2}); // true shallowEqual({ a: 1, b: 2 }, { a: 1, b: 2 }); // true
shallowEqual({a:1, b: 2}, {a:1, b:3}); // false shallowEqual({ a: 1, b: 2 }, { a: 1, b: 3 }); // false
``` ```
However, for performance reasons, it assumes that the two objects have the same However, for performance reasons, it assumes that the two objects have the same
@@ -144,5 +144,5 @@ keys. If we are in a situation where this is not guaranteed, the following code
will work: will work:
```js ```js
const completeShallowEqual = (a,b) => shallowEqual(a,b) && shallowEqual(b,a); const completeShallowEqual = (a, b) => shallowEqual(a, b) && shallowEqual(b, a);
``` ```