From 06a6d890d71d19cff9e716452d6a583f13b8fb38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Sat, 30 Nov 2019 21:36:41 +0100 Subject: [PATCH] [DOC] move some component doc in sub pages closes #354 --- doc/learning/tutorial_todoapp.md | 2 +- doc/readme.md | 2 + doc/reference/component.md | 238 +---------------------------- doc/reference/concurrency_model.md | 183 ++++++++++++++++++++++ doc/reference/props_validation.md | 103 +++++++++++++ tests/component/component.test.ts | 8 +- 6 files changed, 294 insertions(+), 242 deletions(-) create mode 100644 doc/reference/concurrency_model.md create mode 100644 doc/reference/props_validation.md diff --git a/doc/learning/tutorial_todoapp.md b/doc/learning/tutorial_todoapp.md index 6e96c323..7840e0b7 100644 --- a/doc/learning/tutorial_todoapp.md +++ b/doc/learning/tutorial_todoapp.md @@ -297,7 +297,7 @@ A lot of stuff happened here: - the `Task` component has a `props` key: this is only useful for validation purpose. It says that each `Task` should be given exactly one prop, named `task`. If this is not the case, Owl will throw an - [error](../reference/component.md#props-validation). This is extremely + [error](../reference/props_validation.md). This is extremely useful when refactoring components - finally, to activate the props validation, we need to set Owl's [mode](../reference/config.md#mode) to `dev`. This is done in the `setup` diff --git a/doc/readme.md b/doc/readme.md index 43492b20..cbf253d9 100644 --- a/doc/readme.md +++ b/doc/readme.md @@ -4,6 +4,7 @@ - [Animations](reference/animations.md) - [Component](reference/component.md) +- [Concurrency Model](reference/concurrency_model.md) - [Configuration](reference/config.md) - [Context](reference/context.md) - [Environment](reference/environment.md) @@ -12,6 +13,7 @@ - [Miscellaneous Components](reference/misc.md) - [Observer](reference/observer.md) - [Props](reference/props.md) +- [Props Validation](reference/props_validation.md) - [QWeb Templating Language](reference/qweb_templating_language.md) - [QWeb Engine](reference/qweb_engine.md) - [Router](reference/router.md) diff --git a/doc/reference/component.md b/doc/reference/component.md index af850295..1862e086 100644 --- a/doc/reference/component.md +++ b/doc/reference/component.md @@ -14,12 +14,9 @@ - [Composition](#composition) - [Event Handling](#event-handling) - [Form Input Bindings](#form-input-bindings) - - [Semantics](#semantics) - - [Props Validation](#props-validation) - [References](#references) - [Slots](#slots) - [Dynamic sub components](#dynamic-sub-components) - - [Asynchronous Rendering](#asynchronous-rendering) - [Error Handling](#error-handling) - [Functional Components](#functional-components) - [SVG components](#svg-components) @@ -204,7 +201,7 @@ to be called in the constructor. * **`props`** (Object, optional): if given, this is an object that describes the type and shape of the (actual) props given to the component. If Owl mode is `dev`, this will be used to validate the props each time the component is - created/updated. See [Props Validation](#props-validation) for more information. + created/updated. See [Props Validation](props_validation.md) for more information. ```js class Counter extends owl.Component { @@ -745,196 +742,6 @@ update a number whenever the change is done. Note: the online playground has an example to show how it works. -### Semantics - -We give here an informal description of the way components are created/updated -in an application. Here, ordered lists describe actions that are executed -sequentially, bullet lists describe actions that are executed in parallel. - -**Scenario 1: initial rendering** Imagine we want to render the following component tree: - -``` - A - / \ - B C - / \ - D E -``` - -Here is what happen whenever we mount the root -component (with some code like `app.mount(document.body)`). - -1. `willStart` is called on `A` - -2. when it is done, template `A` is rendered. - - - component `B` is created - 1. `willStart` is called on `B` - 2. template `B` is rendered - - component `C` is created - 1. `willStart` is called on `C` - 2. template `C` is rendered - - component `D` is created - 1. `willStart` is called on `D` - 2. template `D` is rendered - - component `E` is created - 1. `willStart` is called on `E` - 2. template `E` is rendered - -3. each components are patched into a detached DOM element, in the following order: - `E`, `D`, `C`, `B`, `A`. (so the actual full DOM tree is created - in one pass) - -4. the component `A` root element is actually appended to `document.body` - -5. The method `mounted` is called recursively on all components in the following - order: `E`, `D`, `C`, `B`, `A`. - -**Scenario 2: rerendering a component**. Now, let's assume that the user clicked on some -button in `C`, and this results in a state update, which is supposed to: - -- update `D`, -- remove `E`, -- add new component `F`. - -So, the component tree should look like this: - -``` - A - / \ - B C - / \ - D F -``` - -Here is what Owl will do: - -1. because of a state change, the method `render` is called on `C` -2. template `C` is rendered again - - - component `D` is updated: - 1. hook `willUpdateProps` is called on `D` (async) - 2. template `D` is rerendered - - component `F` is created: - 1. hook `willStart` is called on `E` (async) - 2. template `F` is rendered - -3. `willPatch` hooks are called recursively on components `C`, `D` (not on `F`, - because it is not mounted yet) - -4. components `F`, `D` are patched in that order - -5. component `C` is patched, which will cause recursively: - - 1. `willUnmount` hook on `E` - 2. destruction of `E`, - -6. `mounted` hook is called on `F`, `patched` hooks are called on `D`, `C` - -### Props Validation - -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. -- 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 solves both issues, by describing the types and shapes -of the props. Here is how it works in Owl: - -- `props` key is a static key (so, different from `this.props` in a component instance) -- it is optional: it is ok for a component to not define a `props` key. -- props are validated whenever a component is created/updated -- props are only validated in `dev` mode (see [config page](config.md#mode)) -- if a key does not match the description, an error is thrown -- it validates keys defined in (static) `props`. Additional keys given by the - parent will cause an error. - -For example: - -```js -class ComponentA extends owl.Component { - static props = ['id', 'url']; - - ... -} - -class ComponentB extends owl.Component { - static props = { - count: {type: Number}, - messages: { - type: Array, - element: {type: Object, shape: {id: Boolean, text: 'string' } - }, - date: Date, - combinedVal: [Number, Boolean] - }; - - ... -} -``` - -- it is an object or a list of strings -- a list of strings is a simplified props definition, which only lists the name - of the props. Also, if the name ends with `?`, it is considered optional. -- all props are by default required, unless they are defined with `optional: true` - (in that case, validation is only done if there is a value) -- valid types are: `Number, String, Boolean, Object, Array, Date, Function`, and all - constructor functions (so, if you have a `Person` class, it can be used as a type) -- arrays are homogeneous (all elements have the same type/shape) - -For each key, a `prop` definition is either a boolean, a constructor, a list of constructors, or an object: - -- a boolean: indicate that the props exists, and is mandatory. -- a constructor: this should describe the type, for example: `id: Number` describe - the props `id` as a number -- a list of constructors. In that case, this means that we allow more than one - type. For example, `id: [Number, String]` means that `id` can be either a string - or a number. -- an object. This makes it possible to have more expressive definition. The following sub keys are then allowed (but not mandatory): - - `type`: the main type of the prop being validated - - `element`: if the type was `Array`, then the `element` key describes the type of each element in the array. If it is not set, then we only validate the array, not its elements, - - `shape`: if the type was `Object`, then the `shape` key describes the interface of the object. If it is not set, then we only validate the object, not its elements, - - `validate`: this is a function which should return a boolean to determine if - the value is valid or not. Useful for custom validation logic. - -Examples: - -```js - // only the existence of those 3 keys is documented - static props = ['message', 'id', 'date']; -``` - -```js - // size is optional - static props = ['message', 'size?']; -``` - -```js - static props = { - messageIds: {type: Array, element: Number}, // list of number - otherArr: {type: Array}, // just array. no validation is made on sub elements - otherArr2: Array, // same as otherArr - someObj: {type: Object}, // just an object, no internal validation - someObj2: { - type: Object, - shape: { - id: Number, - name: {type: String, optional: true}, - url: String - ]}, // object, with keys id (number), name (string, optional) and url (string) - someFlag: Boolean, // a boolean, mandatory (even if `false`) - someVal: [Boolean, Date], // either a boolean or a date - otherValue: true, // indicates that it is a prop - kindofsmallnumber: { - type: Number, - validate: n => (0 <= n && n <= 10) - }, - size: { - validate: e => ["small", "medium", "large"].includes(e) - }, - }; -``` - ### References The `useRef` hook is useful when we need a way to interact with some inside part @@ -1092,49 +899,6 @@ component class. Note that the `t-component` directive can only be used on `` nodes. -### Asynchronous Rendering - -Working with asynchronous code always adds a lot of complexity to a system. Whenever -different parts of a system are active at the same time, one needs to think -carefully about all possible interactions. Clearly, this is also true for Owl -components. - -There are two different common problems with Owl asynchronous rendering model: - -- any component can delay the rendering (initial and subsequent) of the whole - application -- for a given component, there are two independant situations that will trigger an - asynchronous rerendering: a change in the state, or a change in the props. - These changes may be done at different times, and Owl has no way of knowing - how to reconcile the resulting renderings. - -Here are a few tips on how to work with asynchronous components: - -1. Minimize the use of asynchronous components! -2. Maybe move the asynchronous logic in a store, which then triggers (mostly) - synchronous renderings -3. Lazy loading external libraries is a good use case for async rendering. This - is mostly fine, because we can assume that it will only takes a fraction of a - second, and only once (see [`owl.utils.loadJS`](utils.md#loadjs)) -4. For all the other cases, the [`AsyncRoot`](misc.md#asyncroot) component is there to help you. When - this component is met, a new rendering - sub tree is created, such that the rendering of that component (and its - children) is not tied to the rendering of the rest of the interface. It can - be used on an asynchronous component, to prevent it from delaying the - rendering of the whole interface, or on a synchronous one, such that its - rendering isn't delayed by other (asynchronous) components. Note that this - directive has no effect on the first rendering, but only on subsequent ones - (triggered by state or props changes). - - ```xml -
- - - - -
- ``` - ### Error Handling By default, whenever an error occurs in the rendering of an Owl application, we diff --git a/doc/reference/concurrency_model.md b/doc/reference/concurrency_model.md new file mode 100644 index 00000000..685f4683 --- /dev/null +++ b/doc/reference/concurrency_model.md @@ -0,0 +1,183 @@ +# 🦉 Concurrency Model 🦉 + +## Content + +- [Overview](#overview) +- [Rendering Components](#rendering-components) +- [Semantics](#semantics) +- [Asynchronous Rendering](#asynchronous-rendering) + +## Overview + +Owl was designed from the very beginning with asynchronous components. This comes +from the `willStart` and the `willUpdateProps` lifecycle hooks. With these +methods, it is possible to build complex highly concurrent applications. + +Owl concurrent mode has several benefits: it makes it possible to delay the +rendering until some asynchronous operation is complete, it makes it possible +to lazy load libraries, while keeping the previous screen completely functional. +It is also good for performance reasons: Owl uses it to only apply the result of +many different renderings only once in an animation frame. Owl can cancel +a rendering that is no longer relevant, restart it, reuse it in some cases. + +But even though using concurrency is quite simple (and is the default behaviour), +asynchrony is difficult, because it introduces an additional dimension that +vastly increase the complexity of an application. This section will explain +how Owl manages this complexity, how concuurent rendering works in a general way. + +## Rendering Components + +The word _rendering_ is a little vague, so, let us explain more precisely the +process by which Owl components are displayed on a screen. + +When a component is mounted or updated, a new rendering is started. It has +two phases: _virtual rendering_ and _patching_. + +### Virtual rendering + +This phase represent the process of rendering a template, in memory, which create a virtual representation of the desired component html). The output of this phase is a +virtual DOM. + +It is asynchronous: each subcomponents needs to either be created (so, `willStart` +will need to be called), or updated (which is done with the `willUpdateProps` +method). This is completely a recursive process: a component is the root of a +component tree, and each sub component needs to be (virtually) rendered. + +### Patching + +Once a rendering is complete, it will be applied on the next animation frame. +This is done synchronously: the whole component tree is patched to the real +DOM. + +## Semantics + +We give here an informal description of the way components are created/updated +in an application. Here, ordered lists describe actions that are executed +sequentially, bullet lists describe actions that are executed in parallel. + +**Scenario 1: initial rendering** Imagine we want to render the following component tree: + +``` + A + / \ + B C + / \ + D E +``` + +Here is what happen whenever we mount the root +component (with some code like `app.mount(document.body)`). + +1. `willStart` is called on `A` + +2. when it is done, template `A` is rendered. + + - component `B` is created + 1. `willStart` is called on `B` + 2. template `B` is rendered + - component `C` is created + 1. `willStart` is called on `C` + 2. template `C` is rendered + - component `D` is created + 1. `willStart` is called on `D` + 2. template `D` is rendered + - component `E` is created + 1. `willStart` is called on `E` + 2. template `E` is rendered + +3. each components are patched into a detached DOM element, in the following order: + `E`, `D`, `C`, `B`, `A`. (so the actual full DOM tree is created + in one pass) + +4. the component `A` root element is actually appended to `document.body` + +5. The method `mounted` is called recursively on all components in the following + order: `E`, `D`, `C`, `B`, `A`. + +**Scenario 2: rerendering a component**. Now, let's assume that the user clicked on some +button in `C`, and this results in a state update, which is supposed to: + +- update `D`, +- remove `E`, +- add new component `F`. + +So, the component tree should look like this: + +``` + A + / \ + B C + / \ + D F +``` + +Here is what Owl will do: + +1. because of a state change, the method `render` is called on `C` +2. template `C` is rendered again + + - component `D` is updated: + 1. hook `willUpdateProps` is called on `D` (async) + 2. template `D` is rerendered + - component `F` is created: + 1. hook `willStart` is called on `E` (async) + 2. template `F` is rendered + +3. `willPatch` hooks are called recursively on components `C`, `D` (not on `F`, + because it is not mounted yet) + +4. components `F`, `D` are patched in that order + +5. component `C` is patched, which will cause recursively: + + 1. `willUnmount` hook on `E` + 2. destruction of `E`, + +6. `mounted` hook is called on `F`, `patched` hooks are called on `D`, `C` + +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](../tooling.md#single-file-component). + +### Asynchronous Rendering + +Working with asynchronous code always adds a lot of complexity to a system. Whenever +different parts of a system are active at the same time, one needs to think +carefully about all possible interactions. Clearly, this is also true for Owl +components. + +There are two different common problems with Owl asynchronous rendering model: + +- any component can delay the rendering (initial and subsequent) of the whole + application +- for a given component, there are two independant situations that will trigger an + asynchronous rerendering: a change in the state, or a change in the props. + These changes may be done at different times, and Owl has no way of knowing + how to reconcile the resulting renderings. + +Here are a few tips on how to work with asynchronous components: + +1. Minimize the use of asynchronous components! +2. Maybe move the asynchronous logic in a store, which then triggers (mostly) + synchronous renderings +3. Lazy loading external libraries is a good use case for async rendering. This + is mostly fine, because we can assume that it will only takes a fraction of a + second, and only once (see [`owl.utils.loadJS`](utils.md#loadjs)) +4. For all the other cases, the [`AsyncRoot`](misc.md#asyncroot) component is there to help you. When + this component is met, a new rendering + sub tree is created, such that the rendering of that component (and its + children) is not tied to the rendering of the rest of the interface. It can + be used on an asynchronous component, to prevent it from delaying the + rendering of the whole interface, or on a synchronous one, such that its + rendering isn't delayed by other (asynchronous) components. Note that this + directive has no effect on the first rendering, but only on subsequent ones + (triggered by state or props changes). + + ```xml +
+ + + + +
+ ``` diff --git a/doc/reference/props_validation.md b/doc/reference/props_validation.md new file mode 100644 index 00000000..506b064c --- /dev/null +++ b/doc/reference/props_validation.md @@ -0,0 +1,103 @@ +# 🦉 Props Validation 🦉 + +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. +- 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 solves both issues, by describing the types and shapes +of the props. Here is how it works in Owl: + +- `props` key is a static key (so, different from `this.props` in a component instance) +- it is optional: it is ok for a component to not define a `props` key. +- props are validated whenever a component is created/updated +- props are only validated in `dev` mode (see [config page](config.md#mode)) +- if a key does not match the description, an error is thrown +- it validates keys defined in (static) `props`. Additional keys given by the + parent will cause an error. + +For example: + +```js +class ComponentA extends owl.Component { + static props = ['id', 'url']; + + ... +} + +class ComponentB extends owl.Component { + static props = { + count: {type: Number}, + messages: { + type: Array, + element: {type: Object, shape: {id: Boolean, text: 'string' } + }, + date: Date, + combinedVal: [Number, Boolean] + }; + + ... +} +``` + +- it is an object or a list of strings +- a list of strings is a simplified props definition, which only lists the name + of the props. Also, if the name ends with `?`, it is considered optional. +- all props are by default required, unless they are defined with `optional: true` + (in that case, validation is only done if there is a value) +- valid types are: `Number, String, Boolean, Object, Array, Date, Function`, and all + constructor functions (so, if you have a `Person` class, it can be used as a type) +- arrays are homogeneous (all elements have the same type/shape) + +For each key, a `prop` definition is either a boolean, a constructor, a list of constructors, or an object: + +- a boolean: indicate that the props exists, and is mandatory. +- a constructor: this should describe the type, for example: `id: Number` describe + the props `id` as a number +- a list of constructors. In that case, this means that we allow more than one + type. For example, `id: [Number, String]` means that `id` can be either a string + or a number. +- an object. This makes it possible to have more expressive definition. The following sub keys are then allowed (but not mandatory): + - `type`: the main type of the prop being validated + - `element`: if the type was `Array`, then the `element` key describes the type of each element in the array. If it is not set, then we only validate the array, not its elements, + - `shape`: if the type was `Object`, then the `shape` key describes the interface of the object. If it is not set, then we only validate the object, not its elements, + - `validate`: this is a function which should return a boolean to determine if + the value is valid or not. Useful for custom validation logic. + +Examples: + +```js + // only the existence of those 3 keys is documented + static props = ['message', 'id', 'date']; +``` + +```js + // size is optional + static props = ['message', 'size?']; +``` + +```js + static props = { + messageIds: {type: Array, element: Number}, // list of number + otherArr: {type: Array}, // just array. no validation is made on sub elements + otherArr2: Array, // same as otherArr + someObj: {type: Object}, // just an object, no internal validation + someObj2: { + type: Object, + shape: { + id: Number, + name: {type: String, optional: true}, + url: String + ]}, // object, with keys id (number), name (string, optional) and url (string) + someFlag: Boolean, // a boolean, mandatory (even if `false`) + someVal: [Boolean, Date], // either a boolean or a date + otherValue: true, // indicates that it is a prop + kindofsmallnumber: { + type: Number, + validate: n => (0 <= n && n <= 10) + }, + size: { + validate: e => ["small", "medium", "large"].includes(e) + }, + }; +``` diff --git a/tests/component/component.test.ts b/tests/component/component.test.ts index c2ace746..44d19eb0 100644 --- a/tests/component/component.test.ts +++ b/tests/component/component.test.ts @@ -2165,28 +2165,28 @@ describe("other directives with t-component", () => { grandChild = this; } _onEv() { - steps.push('GrandChild'); + steps.push("GrandChild"); } } class Child extends Component { static template = xml``; static components = { GrandChild }; _onEv() { - steps.push('Child'); + steps.push("Child"); } } class Parent extends Component { static template = xml``; static components = { Child }; _onEv() { - steps.push('Parent'); + steps.push("Parent"); } } const parent = new Parent(); await parent.mount(fixture); grandChild.trigger("ev"); - expect(steps).toEqual(['GrandChild', 'Child', 'Parent']); + expect(steps).toEqual(["GrandChild", "Child", "Parent"]); }); test("t-if works with t-component", async () => {