From 1f40f113aa650f872e23eb6af8861593994f92a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Thu, 17 Oct 2019 09:50:03 +0200 Subject: [PATCH] [DOC] update the documentation closes #334 closes #336 closes #338 closes #339 closes #341 closes #342 closes #343 closes #346 closes #348 --- doc/animations.md | 9 ++-- doc/component.md | 108 ++++++++++++++++++++++++++++++++++++--------- doc/observer.md | 4 +- doc/quick_start.md | 7 +-- doc/store.md | 34 +++++++------- doc/tags.md | 2 +- doc/utils.md | 83 +++++++++++++++++++++++++++++++++- 7 files changed, 195 insertions(+), 52 deletions(-) diff --git a/doc/animations.md b/doc/animations.md index eb06bc81..d8ba2c80 100644 --- a/doc/animations.md +++ b/doc/animations.md @@ -30,7 +30,7 @@ btn { } ``` -will produce a nice flash effect whenever the user click (or activate with the +will produce a nice flash effect whenever the user clicks (or activates with the keyboard) the button. ## CSS Transitions @@ -47,7 +47,7 @@ the lifetime of a node. Since this is not easy to do by hand, Owl `t-transition` directive is there to help. Whenever a node has a `t-transition` directive, with a `name` value, the following -will happen: +sequence of events will happen: At node insertion: @@ -91,6 +91,5 @@ The `t-transition` directive can be applied on a node element or on a component. Notes: -- more information on animations are available [here](animations.md). -- Owl does not support more than one transition on a single node, so the - `t-transition` expression must be a single value (i.e. no space allowed) +Owl does not support more than one transition on a single node, so the +`t-transition` expression must be a single value (i.e. no space allowed). diff --git a/doc/component.md b/doc/component.md index fb5230b3..c2966f9a 100644 --- a/doc/component.md +++ b/doc/component.md @@ -29,7 +29,7 @@ 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 +1. **declarative:** the user interface should be described in terms of the state of the application, not as a sequence of imperative steps. 2. **composable:** each component can seamlessly be created in a parent component by @@ -72,7 +72,7 @@ class ClickCounter extends owl.Component { 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 +This example shows 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, 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 @@ -80,33 +80,86 @@ a state object is defined, by using the `useState` hook. It is not mandatory to ## Reference -An Owl component is a small class which represent a component or some UI element. +An Owl component is a small class which represents a component 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. Be aware that the name of the component may be significant: if a component does not define a `template` key, then Owl will lookup in QWeb to -find a template with the component name (or one of its ancestor). +find a template with the component name (or one of its ancestors). ### Reactive system -OWL components can be made reactive by observing some part of their state. See the [hooks](hooks.md) section for more details. +OWL components are normal javascript classes. So, changing a component internal +state does nothing more: -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. +```js +class Counter extends Component { + static template = xml`
`; + state = {value: 0}; -```javascript -const { useState } = owl.hooks; - -class SomeComponent extends owl.Component { - state = useState({ a: 0, b: 1 }); + increment() { + this.state.value++; + } } ``` -Note that there is an important limitation: hooks need to be called in the -constructor. +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 +explicit call to `render` in `increment`: + +```js + increment() { + this.state.value++; + this.render(); + } +``` + +However, it may be simple in this case, but it quickly become cumbersome, as a +component get more complex, and its internal state is modified by more than one +method. + +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 +changes. The `useState` hook generates a proxy version of an object +(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: + +```js +const { useState } = owl.hooks; + +class Counter extends Component { + static template = xml`
`; + state = useState({value: 0}); + + increment() { + this.state.value++; + } +} +``` + +Obviously, we can call the `useState` hook more than once: +```js +const { useState } = owl.hooks; + +class Counter extends Component { + static template = xml` +
+ + +
`; + counter1 = useState({value: 0}); + counter2 = useState({value: 0}); + + increment(counter) { + counter.value++; + } +} +``` + +Note that hooks are subject to one important [rule](hooks.md#one-rule): they need +to be called in the constructor. ### Properties @@ -115,10 +168,23 @@ constructor. - **`env`** (Object): the component environment, which contains a QWeb instance. -- **`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!! +- **`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 component gives a `user` and a `color` value to the `ChildComponent`. + + ```xml +
+ +
+ ``` + + Note that `props` are owned by the parent, not by the component. + 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)!! + + The `props` can be modified dynamically by the parent. In that case, the + component will go through the following lifecycle methods: `willUpdateProps`, + `willPatch` and `patched`. ### Static Properties @@ -182,7 +248,7 @@ We explain here all the public methods of the `Component` class. we know that its state (or something in the environment, or ...) has changed. In that case, it should simply set to `true`. -- **`unmount()`**: in case a component need to be detached/removed from the DOM, this +- **`unmount()`**: in case a component needs 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. @@ -338,7 +404,7 @@ with the DOM (for example, through an external library) whenever the 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`). -Updating the compoent 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 turn will cause other calls to patched. So, we need to be particularly careful at avoiding endless cycles. diff --git a/doc/observer.md b/doc/observer.md index 15b4a735..34699573 100644 --- a/doc/observer.md +++ b/doc/observer.md @@ -1,9 +1,9 @@ # 🦉 Observer 🦉 -Owl need 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 an Observer class. Its job is to observe some object state, and react to any -change. To do that, it recursively replace all keys of the observed state by +change. To do that, it recursively replaces all keys of the observed state by getters and setters. For example, this code will display `update` in the console: diff --git a/doc/quick_start.md b/doc/quick_start.md index 315594a6..519f3948 100644 --- a/doc/quick_start.md +++ b/doc/quick_start.md @@ -55,7 +55,7 @@ To build an application (or a sub-part of an application), we need two things: - 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 + need. In practice, it could be used to contain some user session information, some configuration keys (for example, isMobile = true/false if we are in mobile mode). - a description of the user interface: there should be a root component, which can @@ -76,10 +76,7 @@ const useState = owl.hooks.useState; class ClickCounter extends owl.Component { static template = "clickcounter"; - constructor() { - super(...arguments); - this.state = useState({ value: 0 }); - } + state = useState({ value: 0 }); increment() { this.state.value++; diff --git a/doc/store.md b/doc/store.md index 7dcc96da..5f48064c 100644 --- a/doc/store.md +++ b/doc/store.md @@ -17,12 +17,12 @@ Managing the state in an application is not an easy task. In some cases, the state of an application can be part of the component tree, in a natural way. -However, there are situations where some part of the state need to be displayed +However, there are situations where some parts of the state need to be displayed in various parts of the user interface, and then, it is not obvious which component should own which part of the state. Owl's solution to this issue is a centralized store. It is a class that owns -some (or all) state, and let the developer update it in a structured way, with +some (or all) state, and lets the developer update it in a structured way, with `actions`. Owl components can then connect to the store, and will be updated if necessary. @@ -38,14 +38,14 @@ const actions = { state.todos.push({ id: state.nextId++, message, - isCompleted: false + isCompleted: false, }); } }; const state = { todos: [], - nextId: 1 + nextId: 1, }; const store = new owl.Store({ state, actions }); @@ -115,7 +115,7 @@ const actions = { The first argument to an action method is an object with four keys: - `state`: the current state of the store content, - `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, - `env`: the current environment. This is useful sometimes, in particular if an action needs to apply some side effects (such as performing an rpc), and the `rpc` method is located in the environment. @@ -132,7 +132,7 @@ call. Also, it is important to be aware that we need to be careful with asynchronous logic. Each state change will potentially trigger a rerendering, so we need to -make sure that we do not have a partial corrupted state. Here is an example that +make sure that we do not have a partially corrupted state. Here is an example that is likely not a good idea: ```javascript @@ -185,7 +185,7 @@ transform the data contained in the store. const getters = { getPost({ state }, id) { const post = state.posts.find(p => p.id === id); - const author = state.authors.find(a => (a.id = post.id)); + const author = state.authors.find(a => a.id === post.id); return { id, author, @@ -209,7 +209,7 @@ At some point, we need a way to interact with the store from a component. This can be done with the help of the three store hooks: - `useStore` to subscribe a component to some part of the store state, -- `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. @@ -252,14 +252,14 @@ two arguments: - a selector function, which takes the store state as first argument (and the component props as second argument) and returns - an object or an array (which will be then observed) + an object or an array (which will be then observed), - 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 will only be rerendered whenever this part of the state changes. Otherwise, it -will perform a strict equality check and updates the component every time this +will perform a strict equality check and will update the component every time this check fails. ### Semantics @@ -268,23 +268,23 @@ The `Store` class and the `useStore` hook try to be smart and to optimize as muc as possible the rendering and update process. What is important to know is: - components are always updated in the order of their creation (so, parent - before children) -- they are updated only if they are in the DOM + before children), +- they are updated only if they are in the DOM, - if a parent is asynchronous, the system will wait for it to complete its - update before updating other components. + update before updating other components, - in general, updates are not coordinated. This is not a problem for synchronous components, but if there are many asynchronous components, this could lead to - a situation where some part of the UI is updated and other parts of the UI is + a situation where some part of the UI is updated and some other part of the UI is not updated. ### Good Practices - avoid asynchronous components as much as possible. Asynchronous components - lead to situations where parts of the UI is not updated immediately. + lead to situations where parts of the UI is not updated immediately, - do not be afraid to connect many components, parent or children if needed. For example, a `MessageList` component could get a list of ids in its `useStore` call and a `Message` component could get the data of its own - message + message, - since the `useStore` function is called for each connected component, for each state update, it is important to make sure that these functions are as fast as possible. diff --git a/doc/tags.md b/doc/tags.md index ecf32c3a..e733c1d5 100644 --- a/doc/tags.md +++ b/doc/tags.md @@ -1,6 +1,6 @@ # 🦉 Tags 🦉 -Tags are very small helper to make it easy to write inline templates. There is +Tags are very small helpers to make it easy to write inline templates. There is only one currently available tag: `xml`, but we plan to add other tags later, such as a `css` tag, which will be used to write single file components. diff --git a/doc/utils.md b/doc/utils.md index c7ad213c..3292fde2 100644 --- a/doc/utils.md +++ b/doc/utils.md @@ -10,6 +10,8 @@ functions are all available in the `owl.utils` namespace. - [`loadTemplates`](#loadtemplates): loading xml files - [`escape`](#escape): sanitizing strings - [`debounce`](#debounce): limiting rate of function calls +- [`shallowEqual`](#shallowequal): shallow object comparison + ## `whenReady` @@ -25,6 +27,8 @@ Promise.all([loadTemplates(), owl.utils.whenReady()]).then(function([templates]) }); ``` +or alternatively: + ```js owl.utils.whenReady(function() { const qweb = new owl.QWeb(); @@ -36,7 +40,7 @@ owl.utils.whenReady(function() { ## `loadJS` `loadJS` takes a url (string) for a javascript resource, and loads it. It returns -a promise, so the caller can properly react when it is ready. Also, it is smart: +a promise, so the caller can 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. @@ -50,6 +54,10 @@ class MyComponent extends owl.Component { ## `loadTemplates` +`loadTemplates` is a helper function to fetch a template file. It simply +performs a `GET` request and return the resulting string in a promise. For +example: + ```js async function makeEnv() { const templates = await owl.utils.loadTemplates("templates.xml"); @@ -60,4 +68,77 @@ async function makeEnv() { ## `escape` +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: + +```xml +
+``` + +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, +it may be a problem if this is done with some javascript code like this: + +```js +class BadComponent extends Component { + // some template with a ref to a div + // some code ... + + mounted() { + this.divRef.el.innerHTML = this.state.value; + } +} +``` +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 +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, `""` is +escaped to the string: `"<ok>"`). So, the bad example above can be fixed +with the following change: + +```js + this.divRef.el.innerHTML = owl.utils.escape(this.state.value); +``` + + ## `debounce` + +The `debounce` function is useful when we want to limit the number of times some +function/action is perfomed. For example, this may be useful to prevent issue +with people double clicking on a button. + +It takes three arguments: +- `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 + rate limit the function `func` +- `immediate` (optional, boolean, default=false): if `immediate` is true, the + function will be triggered immediately (leading edge of the interval). If false, + the function will be triggered at the end (trailing edge). + +It returns a function. For example: + +```js +const debounce = owl.utils.debounce +window.addEventListener('mousemove', debounce(doSomething, 100)); +``` + +As this example shows, it is usualy useful for event handlers which are triggered +very quickly, such as `scroll` or `mousemove` events. + + +## `shallowEqual` + +This function checks if two objects have the same values assigned to each keys: + +```js +shallowEqual({a:1, b: 2}, {a:1, b:2}); // true +shallowEqual({a:1, b: 2}, {a:1, b:3}); // false +``` + +However, for performance reasons, it assumes that the two objects have the same +keys. If we are in a situation where this is not guaranteed, the following code +will work: + +```js +const completeShallowEqual = (a,b) => shallowEqual(a,b) && shallowEqual(b,a); +``` \ No newline at end of file