mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
181 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 42aa9d3ae1 | |||
| 85f26a0286 | |||
| ed4ab51c17 | |||
| 556a4644f0 | |||
| 821bd0b4b8 | |||
| 06a6d890d7 | |||
| c1269288f5 | |||
| 8c17bb0411 | |||
| 7f6782d009 | |||
| 6e5d6aa226 | |||
| 9400c0adad | |||
| 85d4393242 | |||
| 6c8b401092 | |||
| 12be815342 | |||
| 65344dbf1f | |||
| a6f9b26057 | |||
| 6c753bb49f | |||
| 1f079b883e | |||
| 8fb35ed969 | |||
| a3f2d07b40 | |||
| c993278c80 | |||
| 3aa586db43 | |||
| 14db513f3b | |||
| 7bb04185c7 | |||
| 48744cfa87 | |||
| ff76747a05 | |||
| 7e6b1a28a0 | |||
| 58f6724194 | |||
| 3c0f8ac76f | |||
| cb11c0118c | |||
| 27b4eece66 | |||
| 2e3e8cd603 | |||
| 28ee790b3e | |||
| 2922cee6ea | |||
| 7749fd3b96 | |||
| f44b9a38ae | |||
| c7773bfd2a | |||
| bd39797f17 | |||
| 94a595ef5b | |||
| f83846e054 | |||
| a97144366d | |||
| 79983de4ed | |||
| 54e1734f2f | |||
| 4aea093ed9 | |||
| 56087b95cd | |||
| e5940b4b6b | |||
| bbdc9d90d7 | |||
| f2b3ebd1ec | |||
| 55bb09ba1a | |||
| d249f50d09 | |||
| 749f0063ea | |||
| 263f31fea9 | |||
| b891ae7de4 | |||
| c5a80497ac | |||
| e47f604449 | |||
| 7e721a96b5 | |||
| af4f372506 | |||
| 5731358607 | |||
| 1226f015d7 | |||
| b211e75140 | |||
| 8d1dd06340 | |||
| bd3c1265d3 | |||
| 2413c98f50 | |||
| d74b5a03db | |||
| 83532db48f | |||
| 534152eff7 | |||
| 05a678c039 | |||
| 8fbf2172c5 | |||
| ea1376d0ca | |||
| ba483b6e2c | |||
| 2fc71cfb62 | |||
| 08cb83149e | |||
| 7d249d6f09 | |||
| 0addca63a0 | |||
| 5ba73cc09d | |||
| 9106c19066 | |||
| 9f93da4765 | |||
| 9e37b968e8 | |||
| fa6801b523 | |||
| 9edf29a3a1 | |||
| 6a434310ee | |||
| e7967d0779 | |||
| b0e2ef82f7 | |||
| c9c2b3fa6d | |||
| 62608cbb0d | |||
| 3035a9f009 | |||
| c2adb429bd | |||
| da9dda5eca | |||
| 5614c85b04 | |||
| 5d57a7bf13 | |||
| 4ebe419c56 | |||
| 9779cd196c | |||
| 9cee12d7b4 | |||
| 2c563ee380 | |||
| 2aa705f5c8 | |||
| 5911c8e3f6 | |||
| 2279dafbec | |||
| f07ec21a07 | |||
| b5b6b7342e | |||
| 97b914b9cd | |||
| 9dfcfda365 | |||
| e026f537ae | |||
| f0b5a55ad1 | |||
| 690d8edf67 | |||
| 3c38bbc076 | |||
| 9c5cad15c1 | |||
| 2bed1cfbd1 | |||
| caa9ac5cb2 | |||
| 67349eb0f0 | |||
| 1ca401811f | |||
| 2beb12678e | |||
| 0ea1091692 | |||
| 71827c3ba8 | |||
| c2f42d3b82 | |||
| a3317ab997 | |||
| 403935a41e | |||
| 39af9ec938 | |||
| 13128ed425 | |||
| be556a970e | |||
| da6c24bbca | |||
| f6d6da8393 | |||
| 918945c11e | |||
| 29c2c5b9c9 | |||
| 1bb4577ec1 | |||
| 0aeebd7b6e | |||
| 4bc49e7241 | |||
| 2b5783d9bd | |||
| 1f40f113aa | |||
| 9e65b0686f | |||
| 1d72164015 | |||
| 2825a5a55d | |||
| d40ff53f5a | |||
| 23246c42aa | |||
| 895fe7c60f | |||
| 19b10f7c1d | |||
| 0788d9fe75 | |||
| 4a346d0615 | |||
| 1337aa0107 | |||
| 3e8f60cefa | |||
| 41b0618c93 | |||
| 0928c189f4 | |||
| 2f27768be2 | |||
| aa1b0f502d | |||
| da1824f4dd | |||
| 07dac970f7 | |||
| 5f6f081ae2 | |||
| 1c3c2e5c51 | |||
| 17ce73e6a9 | |||
| cedb1f411f | |||
| e7516be95c | |||
| 683993dbc4 | |||
| 50d569411c | |||
| c3453d35b9 | |||
| b0dd0252a4 | |||
| e2b2e54e29 | |||
| 766c378ac9 | |||
| 022b29b6a0 | |||
| 367725d194 | |||
| e82ed7dc8c | |||
| 63c3d4f6b0 | |||
| b859fcb8f0 | |||
| 3005756d89 | |||
| f6820bb8ac | |||
| 090880f478 | |||
| 5cdaa7b473 | |||
| 91b9e78182 | |||
| 5335fb8fba | |||
| 4513e3f31c | |||
| ecf12f6eee | |||
| 82a6961b3a | |||
| 78a4550ebb | |||
| cc82b3ffcd | |||
| bbbaf95c8e | |||
| caa63628dc | |||
| 7a49b9d94e | |||
| 01eb338e69 | |||
| 3048c6f961 | |||
| 8edc637033 | |||
| c9910077a4 | |||
| 6bf70f016c | |||
| 905619f80f |
@@ -0,0 +1,14 @@
|
|||||||
|
|
||||||
|
Most of the files are
|
||||||
|
|
||||||
|
Copyright (c) 2004-2015 Odoo S.A.
|
||||||
|
|
||||||
|
Many files also contain contributions from third
|
||||||
|
parties. In this case the original copyright of
|
||||||
|
the contributions can be traced through the
|
||||||
|
history of the source version control system.
|
||||||
|
|
||||||
|
When that is not the case, the files contain a prominent
|
||||||
|
notice stating the original copyright and applicable
|
||||||
|
license, or come with their own dedicated COPYRIGHT
|
||||||
|
and/or LICENSE file.
|
||||||
@@ -1,38 +1,43 @@
|
|||||||
<h1 align="center">🦉 <a href="https://odoo.github.io/owl/">Odoo Web Library</a> 🦉</h1>
|
<h1 align="center">🦉 <a href="https://odoo.github.io/owl/">Odoo Web Library</a> 🦉</h1>
|
||||||
|
|
||||||
_A web framework for structured, dynamic and maintainable applications_
|
_Class based components with hooks, reactive state and concurrent mode_
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
The Odoo Web Library (OWL) is a small (~16kb gzipped) UI framework intended to be the basis for
|
The Odoo Web Library (OWL) is a smallish (~<20kb gzipped) UI framework intended to
|
||||||
the [Odoo](https://www.odoo.com/) Web Client, and hopefully many other Odoo
|
be the basis for the [Odoo](https://www.odoo.com/) Web Client. Owl is a modern
|
||||||
related projects. OWL's main features are:
|
framework, written in Typescript, taking the best ideas from React and Vue in a
|
||||||
|
simple and consistent way. Owl's main features are:
|
||||||
|
|
||||||
- a _declarative component system_, (template based, with asynchronous rendering and a virtual dom)
|
- a declarative component system,
|
||||||
- a store implementation (for state management)
|
- a reactivity system based on hooks,
|
||||||
|
- a store implementation (for state management),
|
||||||
- a small frontend router
|
- a small frontend router
|
||||||
|
|
||||||
**Try it online!** An online playground is available at [https://odoo.github.io/owl/playground](https://odoo.github.io/owl/playground) to let you experiment with the OWL framework.
|
Owl components are defined with ES6 classes, they use QWeb templates, an underlying
|
||||||
|
virtual dom, integrates beautifully with hooks, and the rendering is asynchronous.
|
||||||
|
|
||||||
|
**Try it online!** An online playground is available at [https://odoo.github.io/owl/playground](https://odoo.github.io/owl/playground) to let you experiment with the Owl framework. There
|
||||||
|
are some code examples to showcase some interesting features.
|
||||||
|
|
||||||
|
Owl is currently mostly stable. Possible future changes are explained in the
|
||||||
|
[roadmap](roadmap.md).
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
Here is a short example to illustrate interactive components:
|
Here is a short example to illustrate interactive components:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
import { Component, QWeb } from 'owl'
|
const { Component, useState } = owl;
|
||||||
import { xml } from 'owl/tags'
|
const { xml } = owl.tags;
|
||||||
|
|
||||||
class Counter extends Component {
|
class Counter extends Component {
|
||||||
static template = xml`
|
static template = xml`
|
||||||
<button t-on-click="increment">
|
<button t-on-click="state.value++">
|
||||||
Click Me! [<t t-esc="state.value"/>]
|
Click Me! [<t t-esc="state.value"/>]
|
||||||
</button>`;
|
</button>`;
|
||||||
|
|
||||||
state = { value: 0 };
|
state = useState({ value: 0 });
|
||||||
|
|
||||||
increment() {
|
|
||||||
this.state.value++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class App extends Component {
|
class App extends Component {
|
||||||
@@ -43,17 +48,20 @@ class App extends Component {
|
|||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
static components = { Counter };
|
static components = { Counter };
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const app = new App({ qweb: new QWeb() });
|
const app = new App();
|
||||||
app.mount(document.body);
|
app.mount(document.body);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Note that the counter component is made reactive with the [`useState` hook](doc/reference/hooks.md#usestate).
|
||||||
|
Also, all examples here uses the [`xml` helper](doc/reference/tags.md#xml-tag) to define inline templates.
|
||||||
|
But this is not mandatory, many applications will load templates separately.
|
||||||
|
|
||||||
More interesting examples can be found on the
|
More interesting examples can be found on the
|
||||||
[playground](https://odoo.github.io/owl/playground) application.
|
[playground](https://odoo.github.io/owl/playground) application.
|
||||||
|
|
||||||
## OWL's Design Principles
|
## Design Principles
|
||||||
|
|
||||||
OWL is designed to be used in highly dynamic applications where changing
|
OWL is designed to be used in highly dynamic applications where changing
|
||||||
requirements are common and code needs to be maintained by large teams.
|
requirements are common and code needs to be maintained by large teams.
|
||||||
@@ -78,11 +86,16 @@ find some more information [here](doc/comparison.md).
|
|||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
The complete documentation can be found [here](doc/readme.md). The most important sections are:
|
A complete documentation for Owl can be found here:
|
||||||
|
|
||||||
- [Quick Start](doc/quick_start.md)
|
- [Main documentation page](doc/readme.md).
|
||||||
- [Component](doc/component.md)
|
|
||||||
- [QWeb](doc/qweb.md)
|
The most important sections are:
|
||||||
|
|
||||||
|
- [Tutorial: TodoList application](doc/learning/tutorial_todoapp.md)
|
||||||
|
- [QWeb templating language](doc/reference/qweb_templating_language.md)
|
||||||
|
- [Component](doc/reference/component.md)
|
||||||
|
- [Hooks](doc/reference/hooks.md)
|
||||||
|
|
||||||
Found an issue in the documentation? A broken link? Some outdated information?
|
Found an issue in the documentation? A broken link? Some outdated information?
|
||||||
Submit a PR!
|
Submit a PR!
|
||||||
@@ -91,20 +104,148 @@ Submit a PR!
|
|||||||
|
|
||||||
If you want to use a simple `<script>` tag, the last release can be downloaded here:
|
If you want to use a simple `<script>` tag, the last release can be downloaded here:
|
||||||
|
|
||||||
- [owl-0.21.0.js](https://github.com/odoo/owl/releases/download/v0.21.0/owl.js)
|
- [owl-1.0.0-beta1.js](https://github.com/odoo/owl/releases/download/v1.0.0-beta1/owl.js)
|
||||||
- [owl-0.21.0.min.js](https://github.com/odoo/owl/releases/download/v0.21.0/owl.min.js)
|
- [owl-1.0.0-beta1.min.js](https://github.com/odoo/owl/releases/download/v1.0.0-beta1/owl.min.js)
|
||||||
|
|
||||||
Some npm scripts are available:
|
Some npm scripts are available:
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
| --------------------- | ---------------------------------------------------------------------------- |
|
| ---------------- | -------------------------------------------------- |
|
||||||
| `npm install` | install every dependency required for this project |
|
| `npm install` | install every dependency required for this project |
|
||||||
| `npm run build` | build a bundle of _owl_ in the _/dist/_ folder |
|
| `npm run build` | build a bundle of _owl_ in the _/dist/_ folder |
|
||||||
| `npm run minify` | minify the prebuilt owl.js file |
|
| `npm run minify` | minify the prebuilt owl.js file |
|
||||||
| `npm run test` | run all tests |
|
| `npm run test` | run all (owl) tests |
|
||||||
| `npm run test:watch` | run all tests, and keep a watcher |
|
|
||||||
| `npm run tools` | build tools applications, start a static server (see [here](doc/tooling.md)) |
|
## Quick Overview
|
||||||
| `npm run tools:watch` | same as `tools`, but with a watcher to rebuild owl |
|
|
||||||
|
Owl components in an application are used to define a (dynamic) tree of components.
|
||||||
|
|
||||||
|
```
|
||||||
|
Root
|
||||||
|
/ \
|
||||||
|
A B
|
||||||
|
/ \
|
||||||
|
C D
|
||||||
|
```
|
||||||
|
|
||||||
|
**State:** each component can manage its own local state. It is a simple ES6
|
||||||
|
class, there are no special rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class Counter extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<button t-on-click="increment">
|
||||||
|
Click Me! [<t t-esc="state.value"/>]
|
||||||
|
</button>`;
|
||||||
|
|
||||||
|
state = { value: 0 };
|
||||||
|
|
||||||
|
increment() {
|
||||||
|
this.state.value++;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The example above shows a component with a local state. Note that since there
|
||||||
|
is nothing magical to the `state` object, we need to manually call the `render`
|
||||||
|
function whenever we update it. This can quickly become annoying (and not
|
||||||
|
efficient if we do it too much). There is a better way: using the `useState`
|
||||||
|
hook, which transforms an object into a reactive version of itself:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { useState } = owl.hooks;
|
||||||
|
|
||||||
|
class Counter extends 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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that the `t-on-click` handler can even be replaced by an inline statement:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<button t-on-click="state.value++">
|
||||||
|
```
|
||||||
|
|
||||||
|
**Props:** sub components often needs some information from their parents. This
|
||||||
|
is done by adding the required information to the template. This will then be
|
||||||
|
accessible by the sub component in the `props` object. Note that there is an
|
||||||
|
important rule here: the information contained in the `props` object is not
|
||||||
|
owned by the sub component, and should never be modified.
|
||||||
|
|
||||||
|
```js
|
||||||
|
class Child extends Component {
|
||||||
|
static template = xml`<div>Hello <t t-esc="props.name"/></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<Child name="'Owl'" />
|
||||||
|
<Child name="'Framework'" />
|
||||||
|
</div>`;
|
||||||
|
static components = { Child };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Communication:** there are multiple ways to communicate information between
|
||||||
|
components. However, the two most important ways are the following:
|
||||||
|
|
||||||
|
- from parent to children: by using `props`,
|
||||||
|
- from a children to one of its parent: by triggering events.
|
||||||
|
|
||||||
|
The following example illustrate both mechanisms:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class OrderLine extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<div t-on-click="add">
|
||||||
|
<div><t t-esc="props.line.name"/></div>
|
||||||
|
<div>Quantity: <t t-esc="props.line.quantity"/></div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
add() {
|
||||||
|
this.trigger("add-to-order", { line: props.line });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<div t-on-add-to-order="addToOrder">
|
||||||
|
<OrderLine
|
||||||
|
t-foreach="orders"
|
||||||
|
t-as="line"
|
||||||
|
line="line" />
|
||||||
|
</div>`;
|
||||||
|
static components = { OrderLine };
|
||||||
|
orders = useState([{ id: 1, name: "Coffee", quantity: 0 }, { id: 2, name: "Tea", quantity: 0 }]);
|
||||||
|
|
||||||
|
addToOrder(event) {
|
||||||
|
const line = event.detail.line;
|
||||||
|
line.quantity++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example, the `OrderLine` component trigger a `add-to-order` event. This
|
||||||
|
will generate a DOM event which will bubble along the DOM tree. It will then be
|
||||||
|
intercepted by the parent component, which will then get the line (from the
|
||||||
|
`detail` key) and then increment its quantity. See the section on [event handling](doc/reference/component.md#event-handling)
|
||||||
|
for more details on how events work.
|
||||||
|
|
||||||
|
Note that this example would have also worked if the `OrderLine` component
|
||||||
|
directly modifies the `line` object. However, this is not a good practice: this
|
||||||
|
only works because the `props` object received by the child component is reactive,
|
||||||
|
so the child component is then coupled to the parents implementation.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# 🦉 Rendering Pipeline 🦉
|
||||||
|
|
||||||
|
We explain here how Owl is designed, from the perspective of its rendering
|
||||||
|
pipeline.
|
||||||
|
|
||||||
|
Warning: these notes are technical by nature, and intended for people working
|
||||||
|
on Owl (or interested in understanding its design).
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
A rendering occurs in two phases:
|
||||||
|
|
||||||
|
- virtual rendering: this generates the virtual dom in memory, asynchronously
|
||||||
|
- patch: applies a virtual tree to the screen (synchronously)
|
||||||
|
|
||||||
|
There are several classes involved in a rendering:
|
||||||
|
|
||||||
|
- components
|
||||||
|
- a scheduler
|
||||||
|
- fibers: small objects containing some metadata, associated with a rendering of
|
||||||
|
a specific component
|
||||||
|
|
||||||
|
Components are organized in a dynamic component tree, visible in the user
|
||||||
|
interface. Whenever a rendering is initiated in a component `C`:
|
||||||
|
|
||||||
|
- a fiber is created on `C` with the rendering props information
|
||||||
|
- the virtual rendering phase starts on C (will asynchronously render all the
|
||||||
|
child components)
|
||||||
|
- the fiber is added to the scheduler, which will poll continuously, every
|
||||||
|
animation frame, if the fiber is done
|
||||||
|
- once it is done, the scheduler will call the task callback, which will apply
|
||||||
|
the patch (if it was not cancelled in the meantime).
|
||||||
+130
-21
@@ -3,7 +3,7 @@
|
|||||||
OWL, React and Vue have the same main feature: they allow developers to build
|
OWL, React and Vue have the same main feature: they allow developers to build
|
||||||
declarative user interfaces. To do that, all these frameworks uses a virtual dom. However, there are still obviously many differences.
|
declarative user interfaces. To do that, all these frameworks uses a virtual dom. However, there are still obviously many differences.
|
||||||
|
|
||||||
In this page, we try to highlight some of these differences. Obviously, some
|
In this page, we try to highlight some of these differences. Obviously, a lot of
|
||||||
effort was done to be fair. However, if you disagree with some of the points
|
effort was done to be fair. However, if you disagree with some of the points
|
||||||
discussed, feel free to open an issue/submit a PR to correct this text.
|
discussed, feel free to open an issue/submit a PR to correct this text.
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@ discussed, feel free to open an issue/submit a PR to correct this text.
|
|||||||
- [Asynchronous rendering](#asynchronous-rendering)
|
- [Asynchronous rendering](#asynchronous-rendering)
|
||||||
- [Reactiveness](#reactiveness)
|
- [Reactiveness](#reactiveness)
|
||||||
- [State Management](#state-management)
|
- [State Management](#state-management)
|
||||||
|
- [Hooks](#hooks)
|
||||||
|
|
||||||
## Size
|
## Size
|
||||||
|
|
||||||
@@ -24,11 +25,16 @@ than React and Vue. Also, jQuery is not the same kind of framework, but it is in
|
|||||||
|
|
||||||
| Framework | Size (minified, gzipped) |
|
| Framework | Size (minified, gzipped) |
|
||||||
| ------------------------ | ------------------------ |
|
| ------------------------ | ------------------------ |
|
||||||
| OWL | 16kb |
|
| OWL | 18kb |
|
||||||
| Vue + VueX | 30kb |
|
| Vue + VueX | 30kb |
|
||||||
|
| Vue + VueX + Vue Router | 39kb |
|
||||||
| React + ReactDOM + Redux | 40kb |
|
| React + ReactDOM + Redux | 40kb |
|
||||||
| jQuery | 30kb |
|
| jQuery | 30kb |
|
||||||
|
|
||||||
|
Note that those comparisons are not entirely fair, because we do not compare
|
||||||
|
the same exact set of features. For example, VueX and Vue Router support more
|
||||||
|
advanced use cases.
|
||||||
|
|
||||||
## Class Based
|
## Class Based
|
||||||
|
|
||||||
Both React and Vue moved away from defining components with classes. They prefer
|
Both React and Vue moved away from defining components with classes. They prefer
|
||||||
@@ -40,6 +46,17 @@ contrast, Owl has only one mechanism: class-based components. We believe that Ow
|
|||||||
components are fast enough for all our usecases, and making it as simple as
|
components are fast enough for all our usecases, and making it as simple as
|
||||||
possible for developers is more valuable (for us).
|
possible for developers is more valuable (for us).
|
||||||
|
|
||||||
|
Also, functions or class based components are more than just syntax. Functions
|
||||||
|
comes with a mindset of composition and class are about inheritance. Clearly,
|
||||||
|
both of these are important mechanisms for reusing code. Also, one does not
|
||||||
|
exclude the other.
|
||||||
|
|
||||||
|
It certainly looks like the world of UI frameworks is moving toward composition,
|
||||||
|
for many very good reasons. Owl is still good at composition (for example,
|
||||||
|
Owl supports slots, which is the primary mechanism to make generic reusable
|
||||||
|
components). But it can also use inheritance (and this is very important since
|
||||||
|
templates can also be inherited with `xpaths` transformations).
|
||||||
|
|
||||||
## Tooling/Build step
|
## Tooling/Build step
|
||||||
|
|
||||||
OWL is designed to be easy to use in a standalone way. For various reasons,
|
OWL is designed to be easy to use in a standalone way. For various reasons,
|
||||||
@@ -50,13 +67,23 @@ be used by simply adding a script tag to a page.
|
|||||||
<script src="owl.min.js" />
|
<script src="owl.min.js" />
|
||||||
```
|
```
|
||||||
|
|
||||||
In comparison, React encourages using JSX,
|
In comparison, React encourages using JSX, which necessitate a build step, and
|
||||||
which necessitate a build step, and most Vue applications uses single file
|
most Vue applications uses single file components, which also necessitate a build step.
|
||||||
components, which also necessitate a build step.
|
|
||||||
|
|
||||||
On the flipside, external tooling may make it harder to use in some case, but it
|
On the flipside, external tooling may make it harder to use in some case, but it
|
||||||
also brings a lot of benefits. And React/Vue have both a large ecosystem.
|
also brings a lot of benefits. And React/Vue have both a large ecosystem.
|
||||||
|
|
||||||
|
Note that since Owl is not dependant on any external tool nor libraries, it is
|
||||||
|
very easy to integrate into any build toolchain. Also, since we cannot rely on
|
||||||
|
additional tools, we made a lot of effort to make the most of the web platform.
|
||||||
|
|
||||||
|
For example, Owl uses the standard `xml` parser that comes with every browser.
|
||||||
|
Because of that, Owl did not have to write its own template parser. Another
|
||||||
|
example is the [`xml`](reference/tags.md#xml-tag) tag helper function, which makes use of
|
||||||
|
native template literals to allow in a natural way to write `xml` templates
|
||||||
|
directly in the javascript code. This can be easily integrated with editor
|
||||||
|
plugins to have autocompletion inside the template.
|
||||||
|
|
||||||
## Templating
|
## Templating
|
||||||
|
|
||||||
OWL uses its own QWeb engine, which compiles templates on the
|
OWL uses its own QWeb engine, which compiles templates on the
|
||||||
@@ -79,7 +106,8 @@ into javascript functions. Note that Vue has a separate build which includes the
|
|||||||
template compiler.
|
template compiler.
|
||||||
|
|
||||||
In contrast, most React applications do not use a templating language, but write
|
In contrast, most React applications do not use a templating language, but write
|
||||||
some JSX code, which is precompiled into plain JavaScript by a build step.
|
some JSX code, which is precompiled into plain JavaScript by a build step. This
|
||||||
|
example is done with the (kind of outdated) React class system:
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
class Clock extends React.Component {
|
class Clock extends React.Component {
|
||||||
@@ -98,6 +126,20 @@ This has the advantage of having the full power of Javascript, but is less
|
|||||||
structured than a template language. Note that the tooling is quite impressive:
|
structured than a template language. Note that the tooling is quite impressive:
|
||||||
there is a syntax highlighter for jsx here on github!
|
there is a syntax highlighter for jsx here on github!
|
||||||
|
|
||||||
|
By comparison, here is the equivalent Owl component, written with the
|
||||||
|
[`xml`](reference/tags.md#xml-tag) tag helper:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class Clock extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<h1>Hello, world!</h1>
|
||||||
|
<h2>It is {props.date.toLocaleTimeString()}.</h2>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Asynchronous Rendering
|
## Asynchronous Rendering
|
||||||
|
|
||||||
This is actually a big difference between OWL and React/Vue: components in OWL
|
This is actually a big difference between OWL and React/Vue: components in OWL
|
||||||
@@ -127,12 +169,14 @@ This may be dangerous (to stop the rendering waiting for the network), but it is
|
|||||||
extremely powerful as well, as demonstrated by the Odoo Web Client.
|
extremely powerful as well, as demonstrated by the Odoo Web Client.
|
||||||
|
|
||||||
Lazy loading static libraries can obviously be done with React/Vue, but it is
|
Lazy loading static libraries can obviously be done with React/Vue, but it is
|
||||||
more convoluted.
|
more convoluted. For example, in Vue, you need to use a dynamic import keyword
|
||||||
|
that needs to be transpiled at build time in order for the component to be loaded
|
||||||
|
asynchronously (see [the documentation](https://vuejs.org/v2/guide/components-dynamic-async.html#Async-Components)).
|
||||||
|
|
||||||
## Reactiveness
|
## Reactiveness
|
||||||
|
|
||||||
React has a simple model: whenever the state changes, it is
|
React has a simple model: whenever the state changes, it is
|
||||||
replaced with a new state (via the setState method). Then, the DOM is patched.
|
replaced with a new state (via the `setState` method). Then, the DOM is patched.
|
||||||
This is simple, efficient, and a little bit awkward to write.
|
This is simple, efficient, and a little bit awkward to write.
|
||||||
|
|
||||||
Vue is a little bit different: it replace magically the properties in the state
|
Vue is a little bit different: it replace magically the properties in the state
|
||||||
@@ -207,30 +251,95 @@ keeps track of who get data, and retrigger a render when it was changed.
|
|||||||
Owl store is a little bit like a mix of redux and vuex: it has actions (but not
|
Owl store is a little bit like a mix of redux and vuex: it has actions (but not
|
||||||
mutations), and like VueX, it keeps track of the state changes. However, it does
|
mutations), and like VueX, it keeps track of the state changes. However, it does
|
||||||
not notify a component when the state changes. Instead, components need to connect
|
not notify a component when the state changes. Instead, components need to connect
|
||||||
to the store like in redux, by inheriting the `ConnectedComponent` class.
|
to the store like in redux, with the `useStore` hook (see the [store documentation](reference/store.md#connecting-a-component)).
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const actions = {
|
const actions = {
|
||||||
increment({ state }, val) {
|
increment({ state }, val) {
|
||||||
state.counter += val;
|
state.counter.value += val;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
counter: 0
|
counter: { value: 0 }
|
||||||
};
|
};
|
||||||
const store = new owl.Store({ state, actions });
|
const store = new owl.Store({ state, actions });
|
||||||
|
|
||||||
class Counter extends owl.ConnectedComponent {
|
class Counter extends Component {
|
||||||
static mapStoreToProps(state) {
|
static template = xml`
|
||||||
return {
|
<button t-name="Counter" t-on-click="dispatch('increment')">
|
||||||
value: state.counter
|
Click Me! [<t t-esc="counter.value"/>]
|
||||||
};
|
</button>`;
|
||||||
}
|
counter = useStore(state => state.counter);
|
||||||
increment() {
|
dispatch = useDispatch();
|
||||||
this.env.store.dispatch("increment");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const counter = new Counter({ store, qweb });
|
Counter.env.store = store;
|
||||||
|
const counter = new Counter();
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Hooks
|
||||||
|
|
||||||
|
[Hooks](https://reactjs.org/docs/hooks-intro.html#motivation) recently took over
|
||||||
|
the React world. They solve a lot of seemingly unconnected problems: attach
|
||||||
|
reusable behavior to a component, in a composable way, extract stateful logic
|
||||||
|
from a component or reuse stateful logic between component, without changing your
|
||||||
|
component hierarchy.
|
||||||
|
|
||||||
|
Here is an example of the React `useState` hook:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
function Example() {
|
||||||
|
// Declare a new state variable, which we'll call "count"
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>You clicked {count} times</p>
|
||||||
|
<button onClick={() => setCount(count + 1)}>Click me</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Because of the way React designed the hooks API, they only work for functional
|
||||||
|
components. But in that case, they really are powerful. Every major React library
|
||||||
|
is in the process of redesigning their API with hooks (for example,
|
||||||
|
[Redux](https://react-redux.js.org/next/api/hooks)).
|
||||||
|
|
||||||
|
Vue 2 does not have hooks, but the Vue project is working on its next version,
|
||||||
|
which will feature its new [composition API](https://vue-composition-api-rfc.netlify.com/).
|
||||||
|
This work is based on the new ideas introduced by React hooks.
|
||||||
|
|
||||||
|
From the way React and Vue introduce their hooks, it may look like hooks are not
|
||||||
|
compatible with class components. However, this is not the case, as shown by
|
||||||
|
Owl [hooks](reference/hooks.md). They are inspired by both React and Vue. For example,
|
||||||
|
the `useState` hook is named after React, but its API is closer to the `reactive`
|
||||||
|
Vue hook.
|
||||||
|
|
||||||
|
Here is what the `Counter` example above look like in Owl:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { Component, Owl } from "owl";
|
||||||
|
import { xml } from "owl/tags";
|
||||||
|
|
||||||
|
class Example extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<p>You clicked {count.value} times</p>
|
||||||
|
<button t-on-click="increment">Click me</button>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
count = useState({ value: 0 });
|
||||||
|
|
||||||
|
increment() {
|
||||||
|
this.state.value++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Since the Owl framework had hooks from early in its life, its main APIs
|
||||||
|
are designed to be interacted with hooks from the start. For example, the
|
||||||
|
`Context` and `Store` abstractions.
|
||||||
|
|||||||
@@ -2,8 +2,17 @@
|
|||||||
|
|
||||||
## Static Server
|
## Static Server
|
||||||
|
|
||||||
Let us assume that we have a static server running somewhere. We could then
|
Let us assume that we have a static server running somewhere. Let us start by
|
||||||
simply add an html page with a few extra files.
|
adding an html page with a few extra files:
|
||||||
|
|
||||||
|
```
|
||||||
|
my-app/
|
||||||
|
index.html
|
||||||
|
app.css
|
||||||
|
app.js
|
||||||
|
owl-X.Y.Z.js
|
||||||
|
templates.xml
|
||||||
|
```
|
||||||
|
|
||||||
### HTML and CSS
|
### HTML and CSS
|
||||||
|
|
||||||
@@ -55,7 +64,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
|
- 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
|
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).
|
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
|
- a description of the user interface: there should be a root component, which can
|
||||||
@@ -72,12 +81,11 @@ 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`:
|
Let us now add the javascript to make it work, in `app.js`:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
|
const useState = owl.hooks.useState;
|
||||||
|
|
||||||
class ClickCounter extends owl.Component {
|
class ClickCounter extends owl.Component {
|
||||||
static template = "clickcounter";
|
static template = "clickcounter";
|
||||||
constructor() {
|
state = useState({ value: 0 });
|
||||||
super(...arguments);
|
|
||||||
this.state = { value: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
increment() {
|
increment() {
|
||||||
this.state.value++;
|
this.state.value++;
|
||||||
@@ -88,11 +96,9 @@ class ClickCounter extends owl.Component {
|
|||||||
// Application initialization
|
// Application initialization
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
async function start() {
|
async function start() {
|
||||||
const templates = await owl.utils.loadTemplates("templates.xml");
|
const templates = await owl.utils.loadFile("templates.xml");
|
||||||
const env = {
|
ClickCounter.env = { qweb: new owl.QWeb({ templates }) };
|
||||||
qweb: new owl.QWeb(templates)
|
const counter = new ClickCounter();
|
||||||
};
|
|
||||||
const counter = new ClickCounter(env);
|
|
||||||
const target = document.getElementById("main");
|
const target = document.getElementById("main");
|
||||||
await counter.mount(target);
|
await counter.mount(target);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# 🦉 Testing Owl components 🦉
|
||||||
|
|
||||||
|
## Content
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Unit Tests](#unit-tests)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
It is a good practice to test applications and components to ensure that they
|
||||||
|
behave as expected. There are many ways to test a user interface: manual
|
||||||
|
testing, integration testing, unit testing, ...
|
||||||
|
|
||||||
|
In this section, we will discuss how to write unit tests for components, and
|
||||||
|
how to debug them if necessary.
|
||||||
|
|
||||||
|
## Unit Tests
|
||||||
|
|
||||||
|
Writing unit tests for Owl components really depends on the testing framework
|
||||||
|
used in a project. But usually, it involves the following steps:
|
||||||
|
|
||||||
|
- create a test file: for example `SomeComponent.test.js`,
|
||||||
|
- in that file, import the code for `SomeComponent`,
|
||||||
|
- add a test case:
|
||||||
|
- create a real DOM element to use as test fixture,
|
||||||
|
- create a test environment
|
||||||
|
- create an instance of `SomeComponent`, mount it to the fixture
|
||||||
|
- interact with the component and assert some properties.
|
||||||
|
|
||||||
|
To help with this, it is useful to have a `helper.js` file that contains some
|
||||||
|
common utility functions:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export function makeTestFixture() {
|
||||||
|
let fixture = document.createElement("div");
|
||||||
|
document.body.appendChild(fixture);
|
||||||
|
return fixture;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function nextTick() {
|
||||||
|
let requestAnimationFrame = owl.Component.scheduler.requestAnimationFrame;
|
||||||
|
return new Promise(function(resolve) {
|
||||||
|
setTimeout(() => requestAnimationFrame(() => resolve()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeTestEnv() {
|
||||||
|
// application specific. It needs a way to load actual templates
|
||||||
|
const templates = ...;
|
||||||
|
|
||||||
|
return {
|
||||||
|
qweb: new QWeb(templates),
|
||||||
|
..., // each service can be mocked here
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
With such a file, a typical test suite for Jest will look like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// in SomeComponent.test.js
|
||||||
|
import { SomeComponent } from "../../src/ui/SomeComponent";
|
||||||
|
import { nextTick, makeTestFixture, makeTestEnv} from '../helpers';
|
||||||
|
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Setup
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
let fixture: HTMLElement;
|
||||||
|
let env: Env;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = makeTestFixture();
|
||||||
|
env = makeTestEnv();
|
||||||
|
// we set here the default environment for each component created in the test
|
||||||
|
Component.env = env;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fixture.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Tests
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
describe("SomeComponent", () => {
|
||||||
|
test("component behaves as expected", async () => {
|
||||||
|
const props = {...}; // depends on the component
|
||||||
|
const comp = new SomeComponent(null, props);
|
||||||
|
await comp.mount(fixture);
|
||||||
|
|
||||||
|
// do some assertions
|
||||||
|
expect(...).toBe(...);
|
||||||
|
|
||||||
|
fixture.querySelector('button').click();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// some other assertions
|
||||||
|
expect(...).toBe(...);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that Owl does wait for the next animation frame to actually update the DOM.
|
||||||
|
This is why it is necessary to wait with the `nextTick` (or other methods) to
|
||||||
|
make sure that the DOM is up-to-date.
|
||||||
|
|
||||||
|
It is sometimes useful to wait until Owl is completely done updating components
|
||||||
|
(in particular, if we have a highly concurrent user interface). This next
|
||||||
|
helper simply polls every 20ms the internal Owl task queue and returns a promise
|
||||||
|
which resolves when it is empty:
|
||||||
|
|
||||||
|
```js
|
||||||
|
function afterUpdates() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let timer = setTimeout(poll, 20);
|
||||||
|
let counter = 0;
|
||||||
|
function poll() {
|
||||||
|
counter++;
|
||||||
|
if (owl.Component.scheduler.tasks.length) {
|
||||||
|
if (counter > 10) {
|
||||||
|
reject(new Error("timeout"));
|
||||||
|
} else {
|
||||||
|
timer = setTimeout(poll);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,50 +0,0 @@
|
|||||||
# 🦉 Observer 🦉
|
|
||||||
|
|
||||||
Owl need 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
|
|
||||||
getters and setters.
|
|
||||||
|
|
||||||
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 = { 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
|
|
||||||
```
|
|
||||||
+64
-40
@@ -1,53 +1,77 @@
|
|||||||
# 🦉 OWL Documentation 🦉
|
# 🦉 OWL Documentation 🦉
|
||||||
|
|
||||||
## Owl Content
|
|
||||||
|
|
||||||
Owl is a javascript library that contains some core classes and function to help
|
|
||||||
build applications. Here is a complete representation of its content:
|
|
||||||
|
|
||||||
```
|
|
||||||
owl
|
|
||||||
Component
|
|
||||||
QWeb
|
|
||||||
core
|
|
||||||
EventBus
|
|
||||||
Observer
|
|
||||||
router
|
|
||||||
Link
|
|
||||||
RouteComponent
|
|
||||||
Router
|
|
||||||
store
|
|
||||||
Store
|
|
||||||
ConnectedComponent
|
|
||||||
tags
|
|
||||||
xml
|
|
||||||
utils
|
|
||||||
debounce
|
|
||||||
escape
|
|
||||||
loadJS
|
|
||||||
loadTemplates
|
|
||||||
whenReady
|
|
||||||
```
|
|
||||||
|
|
||||||
## Reference
|
## Reference
|
||||||
|
|
||||||
- [Animations](animations.md)
|
- [Animations](reference/animations.md)
|
||||||
- [Component](component.md)
|
- [Component](reference/component.md)
|
||||||
- [Event Bus](event_bus.md)
|
- [Concurrency Model](reference/concurrency_model.md)
|
||||||
- [Observer](observer.md)
|
- [Configuration](reference/config.md)
|
||||||
- [QWeb](qweb.md)
|
- [Context](reference/context.md)
|
||||||
- [Router](router.md)
|
- [Environment](reference/environment.md)
|
||||||
- [Store](store.md)
|
- [Event Bus](reference/event_bus.md)
|
||||||
- [Tags](tags.md)
|
- [Hooks](reference/hooks.md)
|
||||||
- [Utils](utils.md)
|
- [Miscellaneous Components](reference/misc.md)
|
||||||
- [Virtual DOM](vdom.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)
|
||||||
|
- [Store](reference/store.md)
|
||||||
|
- [Tags](reference/tags.md)
|
||||||
|
- [Utils](reference/utils.md)
|
||||||
|
|
||||||
## Learning Resources
|
## Learning Resources
|
||||||
|
|
||||||
- [Quick Start](quick_start.md)
|
- [Quick Start: create an (almost) empty Owl application](learning/quick_start.md)
|
||||||
|
- [Tutorial: create a TodoList application](learning/tutorial_todoapp.md)
|
||||||
|
- [Testing Owl components](learning/testing_components.md)
|
||||||
|
|
||||||
## Miscellaneous
|
## Miscellaneous
|
||||||
|
|
||||||
- [Comparison with React/Vue](comparison.md)
|
- [Comparison with React/Vue](comparison.md)
|
||||||
- [Tooling](tooling.md)
|
- [Tooling](tooling.md)
|
||||||
- [Templates to start Owl applications (external link)](https://github.com/ged-odoo/owl-templates)
|
- [Templates to start Owl applications (external link)](https://github.com/ged-odoo/owl-templates)
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
This section explains in more detail the inner workings of Owl. It is targeted
|
||||||
|
for developers working on Owl itself.
|
||||||
|
|
||||||
|
- [Virtual DOM](architecture/vdom.md)
|
||||||
|
- [Rendering](architecture/rendering.md)
|
||||||
|
|
||||||
|
## Owl Content
|
||||||
|
|
||||||
|
Here is a complete visual representation of everything exported by the `owl`
|
||||||
|
global object (so, for example, `Component` is available at `owl.Component`,
|
||||||
|
and `EventBus` is exported as `owl.core.EventBus`):
|
||||||
|
|
||||||
|
```
|
||||||
|
Component misc
|
||||||
|
Context AsyncRoot
|
||||||
|
QWeb Portal
|
||||||
|
Store router
|
||||||
|
useState Link
|
||||||
|
config RouteComponent
|
||||||
|
mode Router
|
||||||
|
core tags
|
||||||
|
EventBus xml
|
||||||
|
Observer utils
|
||||||
|
hooks debounce
|
||||||
|
onWillStart escape
|
||||||
|
onMounted loadJS
|
||||||
|
onWillUpdateProps loadFile
|
||||||
|
onWillPatch shallowEqual
|
||||||
|
onPatched whenReady
|
||||||
|
onWillUnmount
|
||||||
|
useContext
|
||||||
|
useState
|
||||||
|
useRef
|
||||||
|
useSubEnv
|
||||||
|
useStore
|
||||||
|
useDispatch
|
||||||
|
useGetters
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that for convenience, the `useState` hook is also exported at the root of the `owl` object.
|
||||||
|
|||||||
@@ -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.
|
keyboard) the button.
|
||||||
|
|
||||||
## CSS Transitions
|
## 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.
|
directive is there to help.
|
||||||
|
|
||||||
Whenever a node has a `t-transition` directive, with a `name` value, the following
|
Whenever a node has a `t-transition` directive, with a `name` value, the following
|
||||||
will happen:
|
sequence of events will happen:
|
||||||
|
|
||||||
At node insertion:
|
At node insertion:
|
||||||
|
|
||||||
@@ -91,6 +91,5 @@ The `t-transition` directive can be applied on a node element or on a component.
|
|||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
- more information on animations are available [here](animations.md).
|
Owl does not support more than one transition on a single node, so the
|
||||||
- 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).
|
||||||
`t-transition` expression must be a single value (i.e. no space allowed)
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
|
<div t-name="ParentComponent">
|
||||||
|
<SyncChild />
|
||||||
|
<AsyncRoot>
|
||||||
|
<AsyncChild/>
|
||||||
|
</AsyncRoot>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# 🦉 Config 🦉
|
||||||
|
|
||||||
|
The Owl framework is designed to work in many situations. However, it is
|
||||||
|
sometimes necessary to customize some behaviour. This is done by using the
|
||||||
|
global `config` object. It currently has one key:
|
||||||
|
|
||||||
|
- [`mode`](#mode).
|
||||||
|
|
||||||
|
## Mode
|
||||||
|
|
||||||
|
By default, Owl is in _production_ mode, this means that it will try to do its
|
||||||
|
job fast, and skip some expensive operations. However, it is sometimes necessary
|
||||||
|
to have better information on what is going on, this is the purpose
|
||||||
|
of the `dev` mode.
|
||||||
|
|
||||||
|
Owl has a mode flag, in `owl.config.mode`. Its default value is `prod`, but
|
||||||
|
it can be set to `dev`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
owl.config.mode = "dev";
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that templates compiled with the `prod` settings will not be recompiled.
|
||||||
|
So, changing this setting is best done at startup.
|
||||||
|
|
||||||
|
An important job done by the `dev` mode is to validate props for each component
|
||||||
|
creation and update. Also, extra props will cause an error.
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
# 🦉 Context 🦉
|
||||||
|
|
||||||
|
## Content
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Example](#example)
|
||||||
|
- [Reference](#reference)
|
||||||
|
- [`Context`](#context)
|
||||||
|
- [`useContext`](#usecontext)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The `Context` object provides a way to share data between an arbitrary number
|
||||||
|
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
|
||||||
|
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.
|
||||||
|
|
||||||
|
With a `Context` object, each component can subscribe (with the `useContext` hook)
|
||||||
|
to its state, and will be updated whenever the context state is updated.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
Assume that we have an application with various components which needs to render
|
||||||
|
differently depending on the size of the device. Here is how we could proceed
|
||||||
|
to make sure that the information is properly shared. First, let us create a
|
||||||
|
context, and add it to the environment:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const deviceContext = new Context({ isMobile: true });
|
||||||
|
App.env.deviceContext = deviceContext;
|
||||||
|
```
|
||||||
|
|
||||||
|
If we want to make it completely responsive, we need to update its value whenever
|
||||||
|
the size of the screen is updated:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const isMobile = () => window.innerWidth <= 768;
|
||||||
|
window.addEventListener(
|
||||||
|
"resize",
|
||||||
|
owl.utils.debounce(() => {
|
||||||
|
const state = deviceContext.state;
|
||||||
|
if (state.isMobile !== isMobile()) {
|
||||||
|
state.isMobile = !state.isMobile;
|
||||||
|
}
|
||||||
|
}, 15)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, each component that want can subscribe and render differently depending on the
|
||||||
|
fact that we are in a mobile or desktop mode.
|
||||||
|
|
||||||
|
```js
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<t t-if=device.isMobile>
|
||||||
|
some simplified user interface
|
||||||
|
</t>
|
||||||
|
<t t-else="1">
|
||||||
|
a more advanced user interface
|
||||||
|
</t>
|
||||||
|
</div>`;
|
||||||
|
device = useContext(this.env.deviceContext);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
### `Context`
|
||||||
|
|
||||||
|
A `Context` object should be created with a state object:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const someContext = new Context({ some: "key" });
|
||||||
|
```
|
||||||
|
|
||||||
|
Its state is now available in the `state` key:
|
||||||
|
|
||||||
|
```js
|
||||||
|
someContext.state.some = "other key";
|
||||||
|
```
|
||||||
|
|
||||||
|
This is the way some global code (such as the responsive code above) should
|
||||||
|
read and update the context state. However, components should not ever read the
|
||||||
|
context state directly from the context, they should instead use the `useContext`
|
||||||
|
hook to properly register themselves to state changes.
|
||||||
|
|
||||||
|
Note that the `Context` hook is different from the React version. For example,
|
||||||
|
there is no concept of provider/consumer. So, the `Context` feature does not
|
||||||
|
by itself allow the use of a different context state depending on the component
|
||||||
|
place in the component tree. However, this functionality can be obtained, if
|
||||||
|
necessary, with the use of sub environment.
|
||||||
|
|
||||||
|
### `useContext`
|
||||||
|
|
||||||
|
The `useContext` hook is the normal way for a component to register themselve
|
||||||
|
to context state changes. The `useContext` method returns the context state:
|
||||||
|
|
||||||
|
```js
|
||||||
|
device = useContext(this.env.deviceContext);
|
||||||
|
```
|
||||||
|
|
||||||
|
It is a simple observed state (with an owl `Observer`), which contains the shared
|
||||||
|
information.
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
# 🦉 Environment 🦉
|
||||||
|
|
||||||
|
## Content
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Setting an Environment](#setting-an-environment)
|
||||||
|
- [Using a sub environment](#using-a-sub-environment)
|
||||||
|
- [Content of an Environment](#content-of-an-environment)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
An environment is an object which contains a [`QWeb` instance](qweb_engine.md). Whenever
|
||||||
|
a root component is created, it is assigned an environment (see
|
||||||
|
[below](#setting-an-environment) for more info on this). This environment is
|
||||||
|
then automatically given to each sub component (and accessible in the `this.env`
|
||||||
|
property).
|
||||||
|
|
||||||
|
```
|
||||||
|
Root
|
||||||
|
/ \
|
||||||
|
A B
|
||||||
|
```
|
||||||
|
|
||||||
|
This way, all components share the same `QWeb` instance. Owl internally requires
|
||||||
|
that the environment has a `qweb` key which maps to a
|
||||||
|
[`QWeb`](qweb_engine.md) instance. This is the QWeb instance that will be used to
|
||||||
|
render each templates in this specific component tree. Note that if no `QWeb`
|
||||||
|
instance is provided, Owl will simply generate it on the fly.
|
||||||
|
|
||||||
|
The environment is mostly static. Each application is free to add anything to
|
||||||
|
the environment, which is very useful, since this can be accessed by each sub
|
||||||
|
component.
|
||||||
|
|
||||||
|
## Setting an environment
|
||||||
|
|
||||||
|
An Owl application needs an [environment](environment.md) to be executed. The
|
||||||
|
environment has an important key: the [QWeb](qweb_engine.md) instance, which will render
|
||||||
|
all templates.
|
||||||
|
|
||||||
|
Whenever a root component `App` is mounted, Owl will setup a valid environment by
|
||||||
|
following the next steps:
|
||||||
|
|
||||||
|
- take the `env` object defined on `App.env` (if no `env` was explicitely setup,
|
||||||
|
this will be return the empty `env` object defined on `Component`)
|
||||||
|
- if `env.qweb` is not set, then Owl will create a `QWeb` instance.
|
||||||
|
|
||||||
|
The correct way to customize an environment is to simply set it up on the root
|
||||||
|
component class, before the first component is created:
|
||||||
|
|
||||||
|
```js
|
||||||
|
App.env = {
|
||||||
|
_t: myTranslateFunction,
|
||||||
|
user: {...},
|
||||||
|
services: {
|
||||||
|
...
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const app = new App();
|
||||||
|
app.mount(document.body);
|
||||||
|
```
|
||||||
|
|
||||||
|
It is also possible to simply share an environment between all root components,
|
||||||
|
by simply doing this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
Component.env = myEnv; // will be the default env for all components
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that this environment is the global owl environment for an application. The
|
||||||
|
next section explains how to extend an environment for a specific sub component
|
||||||
|
and its children.
|
||||||
|
|
||||||
|
## Using a sub environment
|
||||||
|
|
||||||
|
It is sometimes useful to add one (or more) specific keys to the environment,
|
||||||
|
from the perspective of a specific component and its children. In that case, the
|
||||||
|
solution presented above will not work, since it sets the global environment.
|
||||||
|
|
||||||
|
There is a hook for this situation: [`useSubEnv`](hooks.md#usesubenv).
|
||||||
|
|
||||||
|
```js
|
||||||
|
class FormComponent extends Component {
|
||||||
|
constructor(parent, props) {
|
||||||
|
super(parent, props);
|
||||||
|
useSubEnv({ myKey: someValue });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Content of an Environment
|
||||||
|
|
||||||
|
Some good use cases for additional keys in the environment are:
|
||||||
|
|
||||||
|
- some configuration keys,
|
||||||
|
- session information,
|
||||||
|
- generic services (such as doing rpcs, or accessing local storage).
|
||||||
|
|
||||||
|
Doing it this way means that components are easily testable: we can simply
|
||||||
|
create a test environment with mock services.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
async function myEnv() {
|
||||||
|
const templates = await loadTemplates();
|
||||||
|
const qweb = new QWeb({ templates });
|
||||||
|
const session = getSession();
|
||||||
|
|
||||||
|
return {
|
||||||
|
_t: myTranslateFunction,
|
||||||
|
session: session,
|
||||||
|
qweb: qweb,
|
||||||
|
services: {
|
||||||
|
localStorage: localStorage,
|
||||||
|
rpc: rpc
|
||||||
|
},
|
||||||
|
debug: false,
|
||||||
|
inMobileMode: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function start() {
|
||||||
|
App.env = await myEnv();
|
||||||
|
const app = new App();
|
||||||
|
await app.mount(document.body);
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -5,7 +5,7 @@ parts of the code. Owl has a very simple bus class, which manages subscriptions,
|
|||||||
triggering events, and callbacks.
|
triggering events, and callbacks.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const bus = new owl.EventBus();
|
const bus = new owl.core.EventBus();
|
||||||
|
|
||||||
bus.on("some-event", null, function(...args) {
|
bus.on("some-event", null, function(...args) {
|
||||||
console.log(...args);
|
console.log(...args);
|
||||||
@@ -0,0 +1,435 @@
|
|||||||
|
# 🦉 Hooks 🦉
|
||||||
|
|
||||||
|
## Content
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Example: Mouse Position](#example-mouse-position)
|
||||||
|
- [Example: Autofocus](#example-autofocus)
|
||||||
|
- [Reference](#reference)
|
||||||
|
- [One Rule](#one-rule)
|
||||||
|
- [`useState`](#usestate)
|
||||||
|
- [`onMounted`](#onmounted)
|
||||||
|
- [`onWillUnmount`](#onwillunmount)
|
||||||
|
- [`onWillPatch`](#onwillpatch)
|
||||||
|
- [`onPatched`](#onpatched)
|
||||||
|
- [`onWillStart`](#onwillstart)
|
||||||
|
- [`onWillUpdateProps`](#onwillupdateprops)
|
||||||
|
- [`useContext`](#usecontext)
|
||||||
|
- [`useRef`](#useref)
|
||||||
|
- [`useSubEnv`](#usesubenv)
|
||||||
|
- [`useStore`](#usestore)
|
||||||
|
- [`useDispatch`](#usedispatch)
|
||||||
|
- [`useGetters`](#usegetters)
|
||||||
|
- [Making customized hooks](#making-customized-hooks)
|
||||||
|
|
||||||
|
## 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 work beautifully with Owl components: they solve the problems mentioned
|
||||||
|
above, and in particular, they are the perfect way to make your component
|
||||||
|
reactive.
|
||||||
|
|
||||||
|
## Example: mouse position
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Example: autofocus
|
||||||
|
|
||||||
|
Hooks can be combined to create the desired effect. For example, the following
|
||||||
|
hook combines the `useRef` hook with the `onPatched` and `onMounted` functions
|
||||||
|
to create an easy way to focus an input whenever it appears in the DOM:
|
||||||
|
|
||||||
|
```js
|
||||||
|
function useAutofocus(name) {
|
||||||
|
let ref = useRef(name);
|
||||||
|
let isInDom = false;
|
||||||
|
function updateFocus() {
|
||||||
|
if (!isInDom && ref.el) {
|
||||||
|
isInDom = true;
|
||||||
|
ref.el.focus();
|
||||||
|
} else if (isInDom && !ref.el) {
|
||||||
|
isInDom = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onPatched(updateFocus);
|
||||||
|
onMounted(updateFocus);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This hook takes the name of a valid `t-ref` directive, which should be present
|
||||||
|
in the template. It then checks whenever the component is mounted or patched if
|
||||||
|
the reference is not valid, and in this case, it will focus the node element.
|
||||||
|
This hook can be used like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<input />
|
||||||
|
<input t-ref="myinput"/>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
useAutofocus("myinput");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
### One rule
|
||||||
|
|
||||||
|
There is only one rule: every hook for a component has to be called in the
|
||||||
|
constructor (or in class fields):
|
||||||
|
|
||||||
|
```js
|
||||||
|
// ok
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
state = useState({ value: 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// also ok
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this.state = useState({ value: 0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// not ok: this is executed after the constructor is called
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
async willStart() {
|
||||||
|
this.state = useState({ value: 0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
As you can see, the `useState` hook does not need to be given a reference to
|
||||||
|
the component. This is possible because there is a way to get a reference to the
|
||||||
|
current component: the `Component.current` static property is the reference to the
|
||||||
|
component instance that is currently being created.
|
||||||
|
|
||||||
|
Hooks need to be called in the constructor to ensure that this reference is
|
||||||
|
properly set. This is also a good thing for performance reasons (Owl can use
|
||||||
|
this to optimize its implementation), and for a clean architecture (this makes
|
||||||
|
it easier for developers to understand what is really happening in a component).
|
||||||
|
|
||||||
|
### `useState`
|
||||||
|
|
||||||
|
The `useState` hook is certainly the most important hook for Owl components:
|
||||||
|
this is what allows a 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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
It is important to remember that `useState` only works with objects or arrays. It
|
||||||
|
is necessary, since Owl needs to react to a change in state.
|
||||||
|
|
||||||
|
### `onMounted`
|
||||||
|
|
||||||
|
`onMounted` is not a user hook, but is a building block designed to help make useful
|
||||||
|
abstractions. `onMounted` registers a callback, which will be called when the component
|
||||||
|
is mounted (see example on top of this page).
|
||||||
|
|
||||||
|
### `onWillUnmount`
|
||||||
|
|
||||||
|
`onWillUnmount` is not a user hook, but is a building block designed to help make useful
|
||||||
|
abstractions. `onWillUnmount` registers a callback, which will be called when the component
|
||||||
|
is unmounted (see example on top of this page).
|
||||||
|
|
||||||
|
### `onWillPatch`
|
||||||
|
|
||||||
|
`onWillPatch` is not a user hook, but is a building block designed to help make useful
|
||||||
|
abstractions. `onWillPatch` registers a callback, which will be called just
|
||||||
|
before the component patched.
|
||||||
|
|
||||||
|
### `onPatched`
|
||||||
|
|
||||||
|
`onPatched` is not a user hook, but is a building block designed to help make useful
|
||||||
|
abstractions. `onPatched` registers a callback, which will be called just
|
||||||
|
after the component patched.
|
||||||
|
|
||||||
|
### `onWillStart`
|
||||||
|
|
||||||
|
`onWillStart` is an asynchronous hook. This means that the function registered
|
||||||
|
in the hook will be run just before the component is first rendered and can return a
|
||||||
|
promise, to express the fact that it is an asynchronous operation.
|
||||||
|
|
||||||
|
Note that if there are more than one `onWillStart` registered callback, then they
|
||||||
|
will all be run in parallel.
|
||||||
|
|
||||||
|
It can be used to load some initial data. For example, the following hook will
|
||||||
|
automatically load some data from the server, and return an object that will
|
||||||
|
be ready whenever the component is rendered:
|
||||||
|
|
||||||
|
```js
|
||||||
|
function useLoader() {
|
||||||
|
const component = Component.current;
|
||||||
|
const record = useState({});
|
||||||
|
onWillStart(async () => {
|
||||||
|
const recordId = component.props.id;
|
||||||
|
Object.assign(record, await fetchSomeRecord(recordId));
|
||||||
|
});
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that this example does not update the record value whenever props are
|
||||||
|
updated. For that situation, we need to use the `onWillUpdateProps` hook.
|
||||||
|
|
||||||
|
### `onWillUpdateProps`
|
||||||
|
|
||||||
|
Just like `onWillStart`, `onWillUpdateProps` is an asynchronous hook. It is
|
||||||
|
designed to be run whenever the component props are updated. This could be
|
||||||
|
useful to perform some asynchronous task such as fetching updated data.
|
||||||
|
|
||||||
|
```js
|
||||||
|
function useLoader() {
|
||||||
|
const component = Component.current;
|
||||||
|
const record = useState({});
|
||||||
|
|
||||||
|
async function updateRecord(id) {
|
||||||
|
Object.assign(record, await fetchSomeRecord(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
onWillStart(() => updateRecord(component.props.id));
|
||||||
|
onWillUpdateProps(nextProps => updateRecord(nextProps.id));
|
||||||
|
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that if there are more than one `onWillUpdateProps` registered callback,
|
||||||
|
then they will all be run in parallel.
|
||||||
|
|
||||||
|
### `useContext`
|
||||||
|
|
||||||
|
See [`useContext`](context.md#usecontext) for reference documentation.
|
||||||
|
|
||||||
|
### `useRef`
|
||||||
|
|
||||||
|
The `useRef` hook is useful when we need a way to interact with some inside part
|
||||||
|
of a component, rendered by Owl. It can work either on a DOM node, or on a component,
|
||||||
|
tagged by the `t-ref` directive:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div>
|
||||||
|
<div t-ref="someDiv"/>
|
||||||
|
<SubComponent t-ref="someComponent"/>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example, the component will be able to access the `div` and the component
|
||||||
|
`SubComponent` using the `useRef` hook:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class Parent extends Component {
|
||||||
|
subRef = useRef("someComponent");
|
||||||
|
divRef = useRef("someDiv");
|
||||||
|
|
||||||
|
someMethod() {
|
||||||
|
// here, if component is mounted, refs are active:
|
||||||
|
// - this.divRef.el is the div HTMLElement
|
||||||
|
// - this.subRef.comp is the instance of the sub component
|
||||||
|
// - this.subRef.el is the root HTML node of the sub component (i.e. this.subRef.comp.el)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
As shown by the example above, html elements are accessed by using the `el`
|
||||||
|
key, and components references are accessed with `comp`.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- if used on a component, the reference will be set in the `refs`
|
||||||
|
variable between `willPatch` and `patched`,
|
||||||
|
- on a component, accessing `ref.el` will get the root node of the component.
|
||||||
|
|
||||||
|
The `t-ref` directive also accepts dynamic values with string interpolation
|
||||||
|
(like the [`t-attf-`](qweb_templating_language.md#dynamic-attributes) and
|
||||||
|
`t-component` directives). For example,
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div t-ref="component_{{someCondition ? '1' : '2'}}"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Here, the references need to be set like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
this.ref1 = useRef("component_1");
|
||||||
|
this.ref2 = useRef("component_2");
|
||||||
|
```
|
||||||
|
|
||||||
|
References are only guaranteed to be active while the parent component is mounted.
|
||||||
|
If this is not the case, accessing `el` or `comp` on it will return `null`.
|
||||||
|
|
||||||
|
### `useSubEnv`
|
||||||
|
|
||||||
|
The environment is sometimes useful to share some common information between
|
||||||
|
all components. But sometimes, we want to _scope_ that knowledge to a subtree.
|
||||||
|
|
||||||
|
For example, if we have a form view component, maybe we would like to make some
|
||||||
|
`model` object available to all sub components, but not to the whole application.
|
||||||
|
This is where the `useSubEnv` hook may be useful: it lets a component add some
|
||||||
|
information to the environment in a way that only the component and its children
|
||||||
|
can access it:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class FormComponent extends Component {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
const model = makeModel();
|
||||||
|
useSubEnv({ model });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `useSubEnv` takes one argument: an object which contains some key/value that
|
||||||
|
will be added to the parent environment. Note that it will extend, not replace
|
||||||
|
the parent environment. And of course, the parent environment will not be
|
||||||
|
affected.
|
||||||
|
|
||||||
|
### `useStore`
|
||||||
|
|
||||||
|
The `useStore` hook is the entry point for a component to connect to the store.
|
||||||
|
See the [store documentation](store.md) for more information.
|
||||||
|
|
||||||
|
### `useDispatch`
|
||||||
|
|
||||||
|
The `useDispatch` hook is the way for components to get a reference to the store
|
||||||
|
`dispatch` function. See the [store documentation](store.md) for more information.
|
||||||
|
|
||||||
|
### `useGetters`
|
||||||
|
|
||||||
|
The `useGetters` hook is the way for components to get a reference to the store
|
||||||
|
getters. See the [store documentation](store.md) for more information.
|
||||||
|
|
||||||
|
### Making customized hooks
|
||||||
|
|
||||||
|
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
|
||||||
|
easily composed together.
|
||||||
|
|
||||||
|
But, like every good things in life, hooks should be used with moderation. They are
|
||||||
|
not the solution to every problem.
|
||||||
|
|
||||||
|
- they may be overkill: if your component needs to perform some action specific
|
||||||
|
to itself (so, the specific code does not need to be shared), there is nothing
|
||||||
|
wrong with a simple class method:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// maybe overkill
|
||||||
|
class A extends Component {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
useMySpecificHook();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ok
|
||||||
|
class B extends Component {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this.performSpecificTask();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that the second solution is easier to extend in sub components.
|
||||||
|
|
||||||
|
- 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
|
||||||
|
manipulation. For example, assume that we want to give a reference to a
|
||||||
|
router in a `useRouter` hook. We could do this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const router = new Router(...);
|
||||||
|
|
||||||
|
function useRouter() {
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
A better way would be to do something like this: get the reference from the
|
||||||
|
environment.
|
||||||
|
|
||||||
|
```js
|
||||||
|
function useRouter() {
|
||||||
|
return Component.current.env.router;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This means that we give control to the application developer to create the
|
||||||
|
router, which is good, so they can set it up, subclass it, ... And then, to
|
||||||
|
test our components, we can just add a mock router in the environment.
|
||||||
|
|
||||||
|
Note: the code above makes use of the `Component.current` property. This is the
|
||||||
|
way hooks are able to get a reference to the component currently being created.
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
# 🦉 Miscellaneous 🦉
|
||||||
|
|
||||||
|
## Content
|
||||||
|
|
||||||
|
- [Portal](#portal)
|
||||||
|
- [AsyncRoot](#asyncroot)
|
||||||
|
|
||||||
|
## `Portal`
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
|
||||||
|
The component `Portal` is meant to be used as a transparent way to 'teleport' a piece
|
||||||
|
of DOM to the node represented by its sole `target` props.
|
||||||
|
|
||||||
|
This component aims at helping the implementation of the needed infrastructure
|
||||||
|
for modals (as in `bootstrap-modal`).
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
The content it will teleport is defined within the `<Portal>` node and
|
||||||
|
internally uses the `default` [Slot](component.md#slots).
|
||||||
|
|
||||||
|
This slot must contain only **one** node, which in turn can have as many children as necessary.
|
||||||
|
|
||||||
|
The element under which the content will be teleported is represented as a selector
|
||||||
|
by the `target` props which only accepts a string as value.
|
||||||
|
|
||||||
|
The `target` props only supports static selector, and is not meant to be passed to `Portal`
|
||||||
|
as a variable. Namely, `<Portal target="'body'" />` is the intended use.
|
||||||
|
By contrast, `<Portal target="state.target" />` is not supported.
|
||||||
|
|
||||||
|
The component `Portal` has no particular state, rather it is meant to be a slave to its parent,
|
||||||
|
and ultimately just a way for the parent to teleport a piece of its own DOM elsewhere.
|
||||||
|
|
||||||
|
The `Portal`'s root node is always `<portal/>` and is placed where the teleported content
|
||||||
|
_would have_ been. It is this element that the [teleported events](#expected-behaviors) are re-directed on.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
The canonic use-case is to implement a Dialog, where a Component may choose to break the natural
|
||||||
|
workflow to help the user put in some data, which it could use later on.
|
||||||
|
|
||||||
|
JavaScript:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { Component } = owl;
|
||||||
|
const { Portal } = owl.misc;
|
||||||
|
|
||||||
|
class TeleportedComponent extends Component {}
|
||||||
|
class App extends Component {
|
||||||
|
static components = { Portal, TeleportedComponent };
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = new App();
|
||||||
|
app.mount(document.body);
|
||||||
|
```
|
||||||
|
|
||||||
|
XML:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<templates>
|
||||||
|
<div t-name="TeleportedComponent">
|
||||||
|
<span>I will move soon enough</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div t-name="App">
|
||||||
|
<span>I am like the rest of us</span>
|
||||||
|
<Portal target="'body'">
|
||||||
|
<TeleportedComponent />
|
||||||
|
</Portal>
|
||||||
|
</div>
|
||||||
|
</templates>
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example, the `Portal` component will teleport the `TeleportedComponent`'s `div` as a child of the `body`.
|
||||||
|
`TeleportedComponent` is acting as a Dialog here.
|
||||||
|
|
||||||
|
The resulting DOM will look like:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<span>I am like the rest of us</span>
|
||||||
|
<portal></portal>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>I will move soon enough</span>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Expected Behaviors
|
||||||
|
|
||||||
|
The teleported piece is updated as any other `Component`'s DOM and in the same sequence.
|
||||||
|
Namely the teleported piece will be updated in function of its parents components, and patched as
|
||||||
|
a normal child.
|
||||||
|
|
||||||
|
The [_business_ events](component.md#event-handling) triggered by a child component will be stopped
|
||||||
|
to not bubble outside of the `target`. They will, on the other hand, be re-directed onto the
|
||||||
|
`Portal`'s root node and bubble up the DOM as if it were triggered by a regular child component.
|
||||||
|
|
||||||
|
Beware that those re-directed events are copies of the original event.
|
||||||
|
They have:
|
||||||
|
|
||||||
|
- The same payload.
|
||||||
|
- The same `originalComponent` than their original counterpart,
|
||||||
|
that is the actual Component that triggered it.
|
||||||
|
- A **different** `target` property than their original counterpart.
|
||||||
|
The `target` of a re-directed event is necessarily the `Portal`'s root node.
|
||||||
|
|
||||||
|
Pure DOM events do not follow this pattern and are free to bubble their natural, unaltered way
|
||||||
|
up to the `body`.
|
||||||
|
|
||||||
|
## `AsyncRoot`
|
||||||
|
|
||||||
|
When this component is used, 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
|
||||||
|
<div t-name="ParentComponent">
|
||||||
|
<SyncChild />
|
||||||
|
<AsyncRoot>
|
||||||
|
<AsyncChild/>
|
||||||
|
</AsyncRoot>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
The `AsyncRoot` assumes that there is exactly one root node inside it. It can
|
||||||
|
be a dom node or a component.
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# 🦉 Observer 🦉
|
||||||
|
|
||||||
|
Owl needs to be able to react to state changes. For example, whenever the state
|
||||||
|
of a component is changed, Owl needs to rerender it. To help with that, there is
|
||||||
|
an Observer class. Its job is to observe the state of an object (or array), and
|
||||||
|
to react to any change. The observer is implemented with the native `Proxy`
|
||||||
|
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:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const observer = new owl.Observer();
|
||||||
|
observer.notifyCB = () => console.log("update");
|
||||||
|
const obj = observer.observe({ a: { b: 1 } });
|
||||||
|
|
||||||
|
obj.a.b = 2;
|
||||||
|
```
|
||||||
|
|
||||||
|
This example shows that an observer can observe nested properties.
|
||||||
|
|
||||||
|
## 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.
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# 🦉 Props 🦉
|
||||||
|
|
||||||
|
## Content
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Definition](#definition)
|
||||||
|
- [Good Practices](#good-practices)
|
||||||
|
- [Dynamic Props](#dynamic-props)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
In Owl, `props` (short for _properties_) is an object which contains every piece
|
||||||
|
of data given to a component by its parent.
|
||||||
|
|
||||||
|
```js
|
||||||
|
class Child extends Component {
|
||||||
|
static template = xml`<div><t t-esc="props.a"/><t t-esc="props.b"/></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Component {
|
||||||
|
static template = xml`<div><ComponentA a="state.a" b="'string'"/></div>`;
|
||||||
|
static components = { Child };
|
||||||
|
state = useState({ a: "fromparent" });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example, the `Child` component receives two props from its parent: `a`
|
||||||
|
and `b`. They are collected into a `props` object by Owl, with each value being
|
||||||
|
evaluated in the context of the parent. So, `props.a` is equal to `'fromparent'` and
|
||||||
|
`props.b` is equal to `'string'`.
|
||||||
|
|
||||||
|
Note that `props` is an object that only makes sense from the perspective of the
|
||||||
|
child component.
|
||||||
|
|
||||||
|
## Definition
|
||||||
|
|
||||||
|
The `props` object is made of every attributes defined on the template, with the
|
||||||
|
following exceptions:
|
||||||
|
|
||||||
|
- every attribute starting with `t-` are not props (they are QWeb directives),
|
||||||
|
- `style` and `class` attributes are excluded as well (they are applied by Owl on
|
||||||
|
the root element of the component).
|
||||||
|
|
||||||
|
In the following example:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div>
|
||||||
|
<ComponentA a="state.a" b="'string'"/>
|
||||||
|
<ComponentB t-if="state.flag" model="model"/>
|
||||||
|
<ComponentC style="color:red;" class="left-pane" />
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
the `props` object contains the following keys:
|
||||||
|
|
||||||
|
- for `ComponentA`: `a` and `b`,
|
||||||
|
- for `ComponentB`: `model`,
|
||||||
|
- for `ComponentC`: empty object
|
||||||
|
|
||||||
|
## Good Practices
|
||||||
|
|
||||||
|
A `props` object is a collection of values that come from the parent. As such,
|
||||||
|
they are owned by the parent, and should never be modified by the child:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class MyComponent extends Component {
|
||||||
|
constructor(parent, props) {
|
||||||
|
super(parent, props);
|
||||||
|
props.a.b = 43; // Never do that!!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Props should be considered readonly, from the perspective of the child component.
|
||||||
|
If there is a need to modify them, then the request to update them should be
|
||||||
|
sent to the parent (for example, with an event).
|
||||||
|
|
||||||
|
Any value can go in a props. Strings, objects, classes, or even callbacks could
|
||||||
|
be given to a child component (but then, in the case of callbacks, communicating
|
||||||
|
with events seems more appropriate).
|
||||||
|
|
||||||
|
## Dynamic Props
|
||||||
|
|
||||||
|
The `t-props` directive can be used to specify totally dynamic props:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div t-name="ParentComponent">
|
||||||
|
<Child t-props="some.obj"/>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
class ParentComponent {
|
||||||
|
static components = { Child };
|
||||||
|
some = { obj: { a: 1, b: 2 } };
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -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)
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
# 🦉 QWeb Engine 🦉
|
||||||
|
|
||||||
|
## Content
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Reference](#reference)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
[QWeb](https://www.odoo.com/documentation/13.0/reference/qweb.html) is the primary
|
||||||
|
templating engine used by Odoo. The QWeb class in the OWL project is an
|
||||||
|
implementation of that specification with a few interesting points:
|
||||||
|
|
||||||
|
- it compiles templates into functions that output a virtual DOM instead of a
|
||||||
|
string. This is necessary for the component system.
|
||||||
|
- it has a few extra directives: `t-component`, `t-on`, ...
|
||||||
|
|
||||||
|
We present in this section the engine, not the templating language.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
This section is about the javascript code that implements the `QWeb` specification.
|
||||||
|
Owl exports a `QWeb` class in `owl.QWeb`. To use it, it just needs to be
|
||||||
|
instantiated:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const qweb = new owl.QWeb();
|
||||||
|
```
|
||||||
|
|
||||||
|
Its API is quite simple:
|
||||||
|
|
||||||
|
- **`constructor(config)`**: constructor. Takes an optional configuration object
|
||||||
|
with an optional `templates` string to add initial
|
||||||
|
templates (see `addTemplates` for more information on format of the string)
|
||||||
|
and an optional `translateFn` translate function (see the section on
|
||||||
|
[translations](#translations)).
|
||||||
|
|
||||||
|
```js
|
||||||
|
const qweb = new owl.QWeb({ templates: TEMPLATES, translateFn: _t });
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`addTemplate(name, xmlStr, allowDuplicate)`**: add a specific template.
|
||||||
|
|
||||||
|
```js
|
||||||
|
qweb.addTemplate("mytemplate", "<div>hello</div>");
|
||||||
|
```
|
||||||
|
|
||||||
|
If the optional `allowDuplicate` is set to `true`, then `QWeb` will simply
|
||||||
|
ignore templates added for a second time. Otherwise, `QWeb` will crash.
|
||||||
|
|
||||||
|
- **`addTemplates(xmlStr)`**: add a list of templates (identified by `t-name`
|
||||||
|
attribute).
|
||||||
|
|
||||||
|
```js
|
||||||
|
const TEMPLATES = `
|
||||||
|
<templates>
|
||||||
|
<div t-name="App" class="main">main</div>
|
||||||
|
<div t-name="OtherComponent">other component</div>
|
||||||
|
</templates>`;
|
||||||
|
qweb.addTemplates(TEMPLATES);
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`render(name, context, extra)`**: renders a template. This returns a `vnode`,
|
||||||
|
which is a virtual representation of the DOM (see [vdom doc](../architecture/vdom.md)).
|
||||||
|
|
||||||
|
```js
|
||||||
|
const vnode = qweb.render("App", component);
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`renderToString(name, context)`**: renders a template, but returns an html
|
||||||
|
string.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const str = qweb.renderToString("someTemplate", somecontext);
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`registerTemplate(name, template)`**: static function to register a global
|
||||||
|
QWeb template. This is useful for commonly used components accross the
|
||||||
|
application, and for making a template available to an application without
|
||||||
|
having a reference to the actual QWeb instance.
|
||||||
|
|
||||||
|
```js
|
||||||
|
QWeb.registerTemplate("mytemplate", `<div>some template</div>`);
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`registerComponent(name, Component)`**: static function to register an OWL Component
|
||||||
|
to QWeb's global registry. Globally registered Components can be used in
|
||||||
|
templates (see the `t-component` directive). This is useful for commonly used
|
||||||
|
components accross the application.
|
||||||
|
|
||||||
|
```js
|
||||||
|
class Dialog extends owl.Component { ... }
|
||||||
|
QWeb.registerComponent("Dialog", Dialog);
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
class ParentComponent extends owl.Component { ... }
|
||||||
|
qweb.addTemplate("ParentComponent", "<div><Dialog/></div>");
|
||||||
|
```
|
||||||
|
|
||||||
|
In some way, a `QWeb` instance is the core of an Owl application. It is the only
|
||||||
|
mandatory element of an [environment](environment.md). As such, it
|
||||||
|
has an extra responsibility: it can act as an event bus for internal communication
|
||||||
|
between Owl classes. This is the reason why `QWeb` actually extends [EventBus](event_bus.md).
|
||||||
|
|
||||||
|
### Translations
|
||||||
|
|
||||||
|
If properly setup, Owl QWeb engine can translate all rendered templates. To do
|
||||||
|
so, it needs a translate function, which takes a string and returns a string.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const translations = {
|
||||||
|
hello: "bonjour",
|
||||||
|
yes: "oui",
|
||||||
|
no: "non"
|
||||||
|
};
|
||||||
|
const translateFn = str => translations[str] || str;
|
||||||
|
|
||||||
|
const qweb = new QWeb({ translateFn });
|
||||||
|
```
|
||||||
|
|
||||||
|
Once setup, all rendered templates will be translated using `translateFn`:
|
||||||
|
|
||||||
|
- each text node will be replaced with its translation,
|
||||||
|
- each of the following attribute values will be translated as well: `title`,
|
||||||
|
`placeholder`, `label` and `alt`,
|
||||||
|
- translating text nodes can be disabled with the special attribute `t-translation`,
|
||||||
|
if its value is `off`.
|
||||||
|
|
||||||
|
So, with the above `translateFn`, the following templates:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div>hello</div>
|
||||||
|
<div t-translation="off">hello</div>
|
||||||
|
<div>Are you sure?</div>
|
||||||
|
<input placeholder="hello" other="yes"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
will be rendered as:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div>bonjour</div>
|
||||||
|
<div>hello</div>
|
||||||
|
<div>Are you sure?</div>
|
||||||
|
<input placeholder="bonjour" other="yes"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that the translation is done during the compilation of the template, not
|
||||||
|
when it is rendered.
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
# 🦉 QWeb 🦉
|
# 🦉 QWeb Templating Language🦉
|
||||||
|
|
||||||
## Content
|
## Content
|
||||||
|
|
||||||
- [Overview](#overview)
|
- [Overview](#overview)
|
||||||
- [Directives](#directives)
|
- [Directives](#directives)
|
||||||
- [QWeb Engine](#qweb-engine)
|
|
||||||
- [Reference](#reference)
|
- [Reference](#reference)
|
||||||
- [White Spaces](#white-spaces)
|
- [White Spaces](#white-spaces)
|
||||||
- [Root Nodes](#root-nodes)
|
- [Root Nodes](#root-nodes)
|
||||||
@@ -16,42 +15,43 @@
|
|||||||
- [Dynamic Attributes](#dynamic-attributes)
|
- [Dynamic Attributes](#dynamic-attributes)
|
||||||
- [Loops](#loops)
|
- [Loops](#loops)
|
||||||
- [Rendering Sub Templates](#rendering-sub-templates)
|
- [Rendering Sub Templates](#rendering-sub-templates)
|
||||||
|
- [Translations](#translations)
|
||||||
- [Debugging](#debugging)
|
- [Debugging](#debugging)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
[QWeb](https://www.odoo.com/documentation/12.0/reference/qweb.html) is the primary templating engine used by Odoo. It is based on the XML format, and used
|
[QWeb](https://www.odoo.com/documentation/13.0/reference/qweb.html) is the primary
|
||||||
|
templating engine used by Odoo. It is based on the XML format, and used
|
||||||
mostly to generate HTML. In OWL, QWeb templates are compiled into functions that
|
mostly to generate HTML. In OWL, QWeb templates are compiled into functions that
|
||||||
generate a virtual dom representation of the HTML.
|
generate a virtual dom representation of the HTML.
|
||||||
|
|
||||||
Template directives are specified as XML attributes prefixed with `t-`, for instance `t-if` for conditionals, with elements and other attributes being rendered directly.
|
|
||||||
|
|
||||||
To avoid element rendering, a placeholder element `<t>` is also available, which executes its directive but doesn’t generate any output in and of itself.
|
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<div>
|
<div>
|
||||||
<span t-if="somecondition">Some string</span>
|
<span t-if="somecondition">Some string</span>
|
||||||
<ul t-else="1">
|
<ul t-else="1">
|
||||||
<li t-foreach="messages" t-as="message">
|
<li t-foreach="messages" t-as="message">
|
||||||
<t t-esc="message">
|
<t t-esc="message"/>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
The QWeb class in the OWL project is an implementation of that specification
|
Template directives are specified as XML attributes prefixed with `t-`, for
|
||||||
with a few interesting points:
|
instance `t-if` for conditionals, with elements and other attributes being
|
||||||
|
rendered directly.
|
||||||
|
|
||||||
- it compiles templates into functions that output a virtual DOM instead of a
|
To avoid element rendering, a placeholder element `<t>` is also available, which
|
||||||
string. This is necessary for the component system.
|
executes its directive but doesn’t generate any output in and of itself.
|
||||||
- it has a few extra directives: `t-component`, `t-on`, ...
|
|
||||||
|
We present in this section the templating language, including its Owl specific
|
||||||
|
extensions.
|
||||||
|
|
||||||
## Directives
|
## Directives
|
||||||
|
|
||||||
We present here a list of all standard QWeb directives:
|
For reference, here is a list of all standard QWeb directives:
|
||||||
|
|
||||||
| Name | Description |
|
| Name | Description |
|
||||||
| ------------------------------ | ------------------------------------------------------------ |
|
| ------------------------------ | -------------------------------------------------------------- |
|
||||||
| `t-esc` | [Outputting safely a value](#outputting-data) |
|
| `t-esc` | [Outputting safely a value](#outputting-data) |
|
||||||
| `t-raw` | [Outputting value, without escaping](#outputting-data) |
|
| `t-raw` | [Outputting value, without escaping](#outputting-data) |
|
||||||
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
|
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
|
||||||
@@ -60,113 +60,27 @@ We present here a list of all standard QWeb directives:
|
|||||||
| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
|
| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
|
||||||
| `t-call` | [Rendering sub templates](#rendering-sub-templates) |
|
| `t-call` | [Rendering sub templates](#rendering-sub-templates) |
|
||||||
| `t-debug`, `t-log` | [Debugging](#debugging) |
|
| `t-debug`, `t-log` | [Debugging](#debugging) |
|
||||||
| `t-name` | [Defining a template (not really a directive)](#qweb-engine) |
|
| `t-translation` | [Disabling the translation of a node](#translations) |
|
||||||
|
| `t-name` | [Defining a template (not really a directive)](qweb_engine.md) |
|
||||||
|
|
||||||
The component system in Owl requires additional directives, to express various
|
The component system in Owl requires additional directives, to express various
|
||||||
needs. Here is a list of all Owl specific directives:
|
needs. Here is a list of all Owl specific directives:
|
||||||
|
|
||||||
| Name | Description |
|
| Name | Description |
|
||||||
| ------------------------------------------------------ | ----------------------------------------------------------------------------------- |
|
| ------------------------ | ------------------------------------------------------------------------------- |
|
||||||
| `t-component`, `t-props`, `t-keepalive`, `t-asyncroot` | [Defining a sub component](component.md#composition) |
|
| `t-component`, `t-props` | [Defining a sub component](component.md#composition) |
|
||||||
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#references) |
|
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#references) |
|
||||||
| `t-key` | [Defining a key (to help virtual dom reconciliation)](component.md#t-key-directive) |
|
| `t-key` | [Defining a key (to help virtual dom reconciliation)](#loops) |
|
||||||
| `t-on-*` | [Event handling](component.md#event-handling) |
|
| `t-on-*` | [Event handling](component.md#event-handling) |
|
||||||
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
|
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
|
||||||
| `t-mounted` | [Callback when a node or component is mounted](component.md#t-mounted-directive) |
|
|
||||||
| `t-slot` | [Rendering a slot](component.md#slots) |
|
| `t-slot` | [Rendering a slot](component.md#slots) |
|
||||||
| `t-model` | [Form input bindings](component.md#form-input-bindings) |
|
| `t-model` | [Form input bindings](component.md#form-input-bindings) |
|
||||||
|
|
||||||
## QWeb Engine
|
|
||||||
|
|
||||||
This section is about the javascript code that implements the `QWeb` specification.
|
|
||||||
Owl exports a `QWeb` class in `owl.QWeb`. To use it, it just needs to be
|
|
||||||
instantiated:
|
|
||||||
|
|
||||||
```js
|
|
||||||
const qweb = new owl.QWeb();
|
|
||||||
```
|
|
||||||
|
|
||||||
It's API is quite simple:
|
|
||||||
|
|
||||||
- **`constructor(data)`**: constructor. Takes an optional string to add initial
|
|
||||||
templates (see `addTemplates` for more information on format of the string).
|
|
||||||
|
|
||||||
```js
|
|
||||||
const qweb = new owl.QWeb(TEMPLATES);
|
|
||||||
```
|
|
||||||
|
|
||||||
- **`addTemplate(name, xmlStr, allowDuplicate)`**: add a specific template.
|
|
||||||
|
|
||||||
```js
|
|
||||||
qweb.addTemplate("mytemplate", "<div>hello</div>");
|
|
||||||
```
|
|
||||||
|
|
||||||
If the optional `allowDuplicate` is set to `true`, then `QWeb` will simply return whenever a template is added for a second time. Otherwise, `QWeb` will crash.
|
|
||||||
|
|
||||||
- **`addTemplates(xmlStr)`**: add a list of templates (identified by `t-name`
|
|
||||||
attribute).
|
|
||||||
|
|
||||||
```js
|
|
||||||
const TEMPLATES = `
|
|
||||||
<templates>
|
|
||||||
<div t-name="App" class="main">main</div>
|
|
||||||
<div t-name="OtherComponent">other component</div>
|
|
||||||
</templates>`;
|
|
||||||
qweb.addTemplates(TEMPLATES);
|
|
||||||
```
|
|
||||||
|
|
||||||
- **`render(name, context, extra)`**: renders a template. This returns a `vnode`,
|
|
||||||
which is a virtual representation of the DOM (see [vdom doc](vdom.md)).
|
|
||||||
|
|
||||||
```js
|
|
||||||
const vnode = qweb.render("App", component);
|
|
||||||
```
|
|
||||||
|
|
||||||
- **`renderToString(name, context)`**: renders a template, but returns an html
|
|
||||||
string.
|
|
||||||
|
|
||||||
```js
|
|
||||||
const str = qweb.renderToString("someTemplate", somecontext);
|
|
||||||
```
|
|
||||||
|
|
||||||
- **`registerTemplate(name, template)`**: static function to register an global
|
|
||||||
QWeb template. This is useful for commonly used components accross the
|
|
||||||
application, and for making a template available to an application without
|
|
||||||
having a reference to the actual QWeb instance.
|
|
||||||
|
|
||||||
```js
|
|
||||||
QWeb.registerTemplate("mytemplate", `<div>some template`);
|
|
||||||
```
|
|
||||||
|
|
||||||
- **`registerComponent(name, Component)`**: static function to register an OWL Component
|
|
||||||
to QWeb's global registry. Globally registered Components can be used in
|
|
||||||
templates (see the `t-component` directive). This is useful for commonly used
|
|
||||||
components accross the application.
|
|
||||||
|
|
||||||
```js
|
|
||||||
class Dialog extends owl.Component { ... }
|
|
||||||
QWeb.registerComponent("Dialog", Dialog);
|
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
class ParentComponent extends owl.Component { ... }
|
|
||||||
qweb.addTemplate("ParentComponent", "<div><Dialog/></div>");
|
|
||||||
```
|
|
||||||
|
|
||||||
In some way, a `QWeb` instance is the core of an Owl application. It is the only
|
|
||||||
mandatory element of an [environment](component.md#environment). As such, it
|
|
||||||
has an extra responsability: it can act as an event bus for internal communication
|
|
||||||
between Owl classes. This is the reason why `QWeb` actually extends [EventBus](event_bus.md).
|
|
||||||
|
|
||||||
## Reference
|
## Reference
|
||||||
|
|
||||||
We define in this section the specification of how `QWeb` templates should be
|
|
||||||
rendered. Note that we only document here the standard QWeb specification. Owl
|
|
||||||
specific extensions are documented in various other parts of the documentation.
|
|
||||||
|
|
||||||
### White Spaces
|
### White Spaces
|
||||||
|
|
||||||
White spaces in a templates are handled in a special way:
|
White spaces in a template are handled in a special way:
|
||||||
|
|
||||||
- consecutive whitespaces are always condensed to a single whitespace
|
- consecutive whitespaces are always condensed to a single whitespace
|
||||||
- if a whitespace-only text node contains a linebreak, it is ignored
|
- if a whitespace-only text node contains a linebreak, it is ignored
|
||||||
@@ -201,14 +115,14 @@ root nodes.
|
|||||||
### Expression Evaluation
|
### Expression Evaluation
|
||||||
|
|
||||||
QWeb expressions are strings that will be processed at compile time. Each variable in
|
QWeb expressions are strings that will be processed at compile time. Each variable in
|
||||||
the javascript expression will be replaced by a lookup in the context (so, the
|
the javascript expression will be replaced with a lookup in the context (so, the
|
||||||
component). For example, `a + b.c(d)` will be converted into:
|
component). For example, `a + b.c(d)` will be converted into:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
context["a"] + context["b"].c(context["d"]);
|
context["a"] + context["b"].c(context["d"]);
|
||||||
```
|
```
|
||||||
|
|
||||||
It is useful to explain the various rules that applies on these expressions:
|
It is useful to explain the various rules that apply on these expressions:
|
||||||
|
|
||||||
1. it should be a simple expression which returns a value. It cannot be a statement.
|
1. it should be a simple expression which returns a value. It cannot be a statement.
|
||||||
|
|
||||||
@@ -234,8 +148,8 @@ It is useful to explain the various rules that applies on these expressions:
|
|||||||
3. it can use a few special operators to avoid using symbols such as `<`, `>`,
|
3. it can use a few special operators to avoid using symbols such as `<`, `>`,
|
||||||
`&` or `|`. This is useful to make sure that we still write valid XML.
|
`&` or `|`. This is useful to make sure that we still write valid XML.
|
||||||
|
|
||||||
| Word | will be replaced by |
|
| Word | replaced with |
|
||||||
| ----- | ------------------- |
|
| ----- | ------------- |
|
||||||
| `and` | `&&` |
|
| `and` | `&&` |
|
||||||
| `or` | `\|\|` |
|
| `or` | `\|\|` |
|
||||||
| `gt` | `>` |
|
| `gt` | `>` |
|
||||||
@@ -286,6 +200,11 @@ rendered with the value `value` set to `<span>foo</span>` in the rendering conte
|
|||||||
<p><span>foo</span></p>
|
<p><span>foo</span></p>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Note that since the content of the expression is not known beforehand, the `t-raw`
|
||||||
|
directive has to parse the html (and convert it to a virtual dom structure) for
|
||||||
|
each rendering. So, it will be much slower than a regular template. It is
|
||||||
|
therefore advised to limit the use of `t-raw` whenever possible.
|
||||||
|
|
||||||
### Setting Variables
|
### Setting Variables
|
||||||
|
|
||||||
QWeb allows creating variables from within the template, to memoize a computation (to use it multiple times), give a piece of data a clearer name, ...
|
QWeb allows creating variables from within the template, to memoize a computation (to use it multiple times), give a piece of data a clearer name, ...
|
||||||
@@ -385,14 +304,25 @@ If an expression evaluates to a falsy value, it will not be set at all:
|
|||||||
<div t-att-foo="false"/> <!-- result: <div></div> -->
|
<div t-att-foo="false"/> <!-- result: <div></div> -->
|
||||||
```
|
```
|
||||||
|
|
||||||
There is another way to format a string attribute: the `t-attf-` directive. With
|
It is sometimes convenient to format an attribute with string interpolation. In
|
||||||
it, you get string interpolation:
|
that case, the `t-attf-` directive can be used. It is useful when we need to mix
|
||||||
|
literal and dynamic elements, such as css classes.
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<div t-attf-foo="a {{value1}} is {{value2}} of {{value3}} ]"/>
|
<div t-attf-foo="a {{value1}} is {{value2}} of {{value3}} ]"/>
|
||||||
<!-- result if values are set to 1,2 and 3: <div foo="a 0 is 1 of 2 ]"></div> -->
|
<!-- result if values are set to 1,2 and 3: <div foo="a 0 is 1 of 2 ]"></div> -->
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If we need completely dynamic attribute names, then there is an additional
|
||||||
|
directive: `t-att`, which takes either an object (with keys mapping to their
|
||||||
|
values) or a pair `[key, value]`. For example:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div t-att="{'a': 1, 'b': 2}"/> <!-- result: <div a="1" b="2"></div> -->
|
||||||
|
|
||||||
|
<div t-att="['a', 'b']"/> <!-- <div a="b"></div> -->
|
||||||
|
```
|
||||||
|
|
||||||
### Loops
|
### Loops
|
||||||
|
|
||||||
QWeb has an iteration directive `t-foreach` which take an expression returning the
|
QWeb has an iteration directive `t-foreach` which take an expression returning the
|
||||||
@@ -427,7 +357,7 @@ is equivalent to the previous example.
|
|||||||
or an object (the current item will be the current key).
|
or an object (the current item will be the current key).
|
||||||
|
|
||||||
In addition to the name passed via t-as, `t-foreach` provides a few other
|
In addition to the name passed via t-as, `t-foreach` provides a few other
|
||||||
variables for various data points (note: `$as` will be replaced by the name
|
variables for various data points (note: `$as` will be replaced with the name
|
||||||
passed to `t-as`):
|
passed to `t-as`):
|
||||||
|
|
||||||
- `$as_value`: the current iteration value, identical to `$as` for lists and
|
- `$as_value`: the current iteration value, identical to `$as` for lists and
|
||||||
@@ -445,19 +375,83 @@ the context of the `t-foreach`, the value is copied at the end of the foreach
|
|||||||
into the global context.
|
into the global context.
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<t t-set="existing_variable" t-value="False"/>
|
<t t-set="existing_variable" t-value="false"/>
|
||||||
<!-- existing_variable now False -->
|
<!-- existing_variable now False -->
|
||||||
|
|
||||||
<p t-foreach="Array(3)" t-as="i">
|
<p t-foreach="Array(3)" t-as="i">
|
||||||
<t t-set="existing_variable" t-value="True"/>
|
<t t-set="existing_variable" t-value="true"/>
|
||||||
<t t-set="new_variable" t-value="True"/>
|
<t t-set="new_variable" t-value="true"/>
|
||||||
<!-- existing_variable and new_variable now True -->
|
<!-- existing_variable and new_variable now true -->
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- existing_variable always True -->
|
<!-- existing_variable always true -->
|
||||||
<!-- new_variable undefined -->
|
<!-- new_variable undefined -->
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Even though Owl tries to be as declarative as possible, the DOM does not fully
|
||||||
|
expose its state declaratively in the DOM tree. For example, the scrolling state,
|
||||||
|
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, in some situations, this is not enough, and we need to help Owl decide
|
||||||
|
if an element is actually the same, or is a different element with the same
|
||||||
|
properties.
|
||||||
|
|
||||||
|
Consider the following situation: we have a list of two items `[{text: "a"}, {text: "b"}]`
|
||||||
|
and we render them in this template:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<p t-foreach="items" t-as="item"><t t-esc="item.text"/></p>
|
||||||
|
```
|
||||||
|
|
||||||
|
The result will be two `<p>` tags with text `a` and `b`. Now, if we swap them,
|
||||||
|
and rerender the template, Owl needs to know what the intent is:
|
||||||
|
|
||||||
|
- should Owl actually swap the DOM nodes,
|
||||||
|
- or should it keep the DOM nodes, but with an updated text content?
|
||||||
|
|
||||||
|
This might look trivial, but it actually matters. These two possibilities lead
|
||||||
|
to different results in some cases. For example, if the user selected the text
|
||||||
|
of the first `p`, swapping them will keep the selection while updating the
|
||||||
|
text content will not.
|
||||||
|
|
||||||
|
There are many other cases where this is important: `input` tags with their
|
||||||
|
value, css classes and animations, scroll position...
|
||||||
|
|
||||||
|
So, the `t-key` directive is used to give an identity to an element. It allows
|
||||||
|
Owl to understand if different elements of a list are actually different or not.
|
||||||
|
|
||||||
|
The above example could be modified by adding an ID: `[{id: 1, text: "a"}, {id: 2, text: "b"}]`.
|
||||||
|
Then, the template could look like this:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<p t-foreach="items" t-as="item" t-key="item.id"><t t-esc="item.text"/></p>
|
||||||
|
```
|
||||||
|
|
||||||
|
The `t-key` directive is useful for lists (`t-foreach`). A key should be
|
||||||
|
a unique number or string (objects will not work: they will be cast to the
|
||||||
|
`"[object Object]"` string, which is obviously not unique).
|
||||||
|
|
||||||
|
Also, the key can be set on a `t` tag or on its children. The following variations
|
||||||
|
are all equivalent:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<p t-foreach="items" t-as="item" t-key="item.id">
|
||||||
|
<t t-esc="item.text"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<t t-foreach="items" t-as="item" t-key="item.id">
|
||||||
|
<p t-esc="item.text"/>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<t t-foreach="items" t-as="item">
|
||||||
|
<p t-key="item.id" t-esc="item.text"/>
|
||||||
|
</t>
|
||||||
|
```
|
||||||
|
|
||||||
|
If there is no `t-key` directive, Owl will use the index as a default key.
|
||||||
|
|
||||||
### Rendering Sub Templates
|
### Rendering Sub Templates
|
||||||
|
|
||||||
QWeb templates can be used for top level rendering, but they can also be used
|
QWeb templates can be used for top level rendering, but they can also be used
|
||||||
@@ -506,6 +500,21 @@ will result in :
|
|||||||
</div>
|
</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Translations
|
||||||
|
|
||||||
|
By default, QWeb specify that templates should be translated. If this behaviour
|
||||||
|
is not wanted, there is a `t-translation` directive which can turn off
|
||||||
|
translations (if it is set to the `off` value), with the following rules:
|
||||||
|
|
||||||
|
- each text node will be replaced with its translation,
|
||||||
|
- each of the following attribute values will be translated as well: `title`,
|
||||||
|
`placeholder`, `label` and `alt`,
|
||||||
|
- translating text nodes can be disabled with the special attribute `t-translation`,
|
||||||
|
if its value is `off`.
|
||||||
|
|
||||||
|
See [here](qweb_engine.md#translations) for more information on how to setup a
|
||||||
|
translate function in Owl QWeb.
|
||||||
|
|
||||||
### Debugging
|
### Debugging
|
||||||
|
|
||||||
The javascript QWeb implementation provides two useful debugging directives:
|
The javascript QWeb implementation provides two useful debugging directives:
|
||||||
@@ -527,4 +536,4 @@ will stop execution if the browser dev tools are open.
|
|||||||
<t t-log="foo"/>
|
<t t-log="foo"/>
|
||||||
```
|
```
|
||||||
|
|
||||||
will print 42 to the console
|
will print 42 to the console.
|
||||||
@@ -67,6 +67,9 @@ function makeEnvironment() {
|
|||||||
await env.router.start();
|
await env.router.start();
|
||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
App.env = makeEnvironment();
|
||||||
|
// create root component here
|
||||||
```
|
```
|
||||||
|
|
||||||
Notice that the router needs to be started. This is an asynchronous operation
|
Notice that the router needs to be started. This is an asynchronous operation
|
||||||
@@ -97,6 +100,13 @@ The `Router` constructor takes three arguments:
|
|||||||
- an optional object (with the only key `mode` which can be `history` (default
|
- an optional object (with the only key `mode` which can be `history` (default
|
||||||
value) or `hash`).
|
value) or `hash`).
|
||||||
|
|
||||||
|
`history` will use the browser [History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API) as the mechanism to manage URL.\
|
||||||
|
Example: `https://yourdomain.tld/my_custom_route`.\
|
||||||
|
For this mechanism to work, you need a way to configure your web server accordingly.
|
||||||
|
|
||||||
|
`hash` will manipulate the hash of the URL.\
|
||||||
|
Example: `https://yourdomain.tld/index.html#/my_custom_route`.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const ROUTES = [...];
|
const ROUTES = [...];
|
||||||
const router = new owl.router.Router(env, ROUTES, {mode: 'history'});
|
const router = new owl.router.Router(env, ROUTES, {mode: 'history'});
|
||||||
@@ -9,6 +9,9 @@
|
|||||||
- [Actions](#actions)
|
- [Actions](#actions)
|
||||||
- [Getters](#getters)
|
- [Getters](#getters)
|
||||||
- [Connecting a Component](#connecting-a-component)
|
- [Connecting a Component](#connecting-a-component)
|
||||||
|
- [`useStore`](#usestore)
|
||||||
|
- [`useDispatch`](#usedispatch)
|
||||||
|
- [`useGetters`](#usegetters)
|
||||||
- [Semantics](#semantics)
|
- [Semantics](#semantics)
|
||||||
- [Good Practices](#good-practices)
|
- [Good Practices](#good-practices)
|
||||||
|
|
||||||
@@ -16,13 +19,14 @@
|
|||||||
|
|
||||||
Managing the state in an application is not an easy task. In some cases, the
|
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.
|
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
|
in various parts of the user interface, and then, it is not obvious which
|
||||||
component should own which part of the state.
|
component should own which part of the state.
|
||||||
|
|
||||||
Owl's solution to this issue is a centralized store. It is a class that owns
|
Owl's solution to this issue is a centralized store. It is a class that owns
|
||||||
some state, and let the developer update it in a structured way, with `actions`.
|
some (or all) state, and lets the developer update it in a structured way, with
|
||||||
Owl components can then connect to the store, and will be updated if necessary.
|
`actions`. Owl components can then connect to the store, and will be updated if
|
||||||
|
necessary.
|
||||||
|
|
||||||
Note: Owl store is inspired by React Redux and VueX.
|
Note: Owl store is inspired by React Redux and VueX.
|
||||||
|
|
||||||
@@ -47,7 +51,7 @@ const state = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const store = new owl.Store({ state, actions });
|
const store = new owl.Store({ state, actions });
|
||||||
store.on("update", () => console.log(store.state));
|
store.on("update", null, () => console.log(store.state));
|
||||||
|
|
||||||
// updating the state
|
// updating the state
|
||||||
store.dispatch("addTodo", "fix all bugs");
|
store.dispatch("addTodo", "fix all bugs");
|
||||||
@@ -110,6 +114,15 @@ 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,
|
||||||
|
- `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.
|
||||||
|
|
||||||
Actions are called with the `dispatch` method on the store, and can receive an
|
Actions are called with the `dispatch` method on the store, and can receive an
|
||||||
arbitrary number of arguments.
|
arbitrary number of arguments.
|
||||||
|
|
||||||
@@ -122,7 +135,7 @@ call.
|
|||||||
|
|
||||||
Also, it is important to be aware that we need to be careful with asynchronous
|
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
|
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:
|
is likely not a good idea:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
@@ -175,7 +188,7 @@ transform the data contained in the store.
|
|||||||
const getters = {
|
const getters = {
|
||||||
getPost({ state }, id) {
|
getPost({ state }, id) {
|
||||||
const post = state.posts.find(p => p.id === 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 {
|
return {
|
||||||
id,
|
id,
|
||||||
author,
|
author,
|
||||||
@@ -190,100 +203,135 @@ 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
|
||||||
|
|
||||||
At some point, we need a way to access the state in the store from a component.
|
At some point, we need a way to interact with the store from a component. This
|
||||||
By default, an Owl `Component` is not connected to any store. To do that, we
|
can be done with the help of the three store hooks:
|
||||||
need to create a component inheriting from `OwlComponent`:
|
|
||||||
|
- [`useStore`](#usestore) to subscribe a component to some part of the store state,
|
||||||
|
- [`useDispatch`](#usedispatch) to get a reference to a dispatch function,
|
||||||
|
- [`useGetters`](#usegetters) to get a reference to the getters defined in the store.
|
||||||
|
|
||||||
|
Assume we have this store:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const actions = {
|
const actions = {
|
||||||
increment({ state }, val) {
|
increment({ state }, val) {
|
||||||
state.counter += val;
|
state.counter.value += val;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
counter: 0
|
counter: { value: 0 }
|
||||||
};
|
};
|
||||||
const store = new owl.Store({ state, actions });
|
const store = new owl.Store({ state, actions });
|
||||||
|
```
|
||||||
|
|
||||||
class Counter extends owl.ConnectedComponent {
|
A counter component can then select this value and dispatch an action like this:
|
||||||
static mapStoreToProps(state) {
|
|
||||||
return {
|
```js
|
||||||
value: state.counter
|
class Counter extends Component {
|
||||||
};
|
counter = useStore(state => state.counter);
|
||||||
}
|
dispatch = useDispatch();
|
||||||
increment() {
|
|
||||||
this.env.store.dispatch("increment");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const counter = new Counter({ store, qweb });
|
const counter = new Counter({ store, qweb });
|
||||||
```
|
```
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<button t-name="Counter" t-on-click="increment">
|
<button t-name="Counter" t-on-click="dispatch('increment')">
|
||||||
Click Me! [<t t-esc="props.value"/>]
|
Click Me! [<t t-esc="counter.value"/>]
|
||||||
</button>
|
</button>
|
||||||
```
|
```
|
||||||
|
|
||||||
The `ConnectedComponent` class can be configured with the following fields:
|
### `useStore`
|
||||||
|
|
||||||
- `mapStoreToProps`: a function that extracts the `props` of the Component
|
The `useStore` hook is used to select some part of the store state. It accepts
|
||||||
from the `state` of the `Store` and returns them as a dict.
|
two arguments:
|
||||||
- `getStore`: a function that takes the `env` in arguments and returns an
|
|
||||||
instance of `Store` to connect to (if not given, connects to `env.store`)
|
|
||||||
- `hashFunction`: the function to use to detect changes in the state (if not
|
|
||||||
given, generates a function that uses revision numbers, incremented at
|
|
||||||
each state change)
|
|
||||||
- `deep` (boolean): [only useful if no hashFunction is given] if `false`, only watch
|
|
||||||
for top level state changes (`true` by default)
|
|
||||||
|
|
||||||
Note that the class `ConnectedComponent` has a `dispatch` method. This means
|
- a selector function, which takes the store state as first argument (and the
|
||||||
that the previous example could be simplified like this:
|
component props as second argument) and which must return the part of the
|
||||||
|
store state that will be made available and observed for changes,
|
||||||
|
- optionally, an object which can have the following optional keys:
|
||||||
|
- a `store` key containing a store object if we want to use another store than
|
||||||
|
the default store,
|
||||||
|
- an `isEqual` key containing an equality function if we want to specialize
|
||||||
|
the comparison (the function must accept two arguments: the previous result
|
||||||
|
and the new result, and must return whether they are equal),
|
||||||
|
- and an `onUpdate` key containing an update function if we want to execute an
|
||||||
|
arbitrary code every time the selected state changes (the function will
|
||||||
|
receive one argument, the new result, and can execute arbitrary code).
|
||||||
|
|
||||||
```javascript
|
If the `useStore` selector returns a sub part of the store state, the component
|
||||||
class Counter extends owl.ConnectedComponent {
|
will only be rerendered whenever this part of the state changes. Otherwise, it
|
||||||
static mapStoreToProps(state) {
|
will perform a strict equality check (unless the `isEqual` option is defined,
|
||||||
return {
|
then it will call it) and will update the component every time this check fails.
|
||||||
value: state.counter
|
|
||||||
};
|
Note that if the selector function returns a primitive type, the result of
|
||||||
}
|
`useStore` will be immutable and it will not react to changes. In this case, it
|
||||||
|
is important to define the `onUpdate` option to properly update the value
|
||||||
|
manually when it changes.
|
||||||
|
|
||||||
|
Also, the return value from `useStore` is not supposed to be modified. The store
|
||||||
|
state should only be updated with actions.
|
||||||
|
|
||||||
|
### `useDispatch`
|
||||||
|
|
||||||
|
The `useDispatch` hook is useful when a component needs to be able to dispatch
|
||||||
|
actions. It takes an optional argument, which is a store. If not given, it will
|
||||||
|
use the store in the environment.
|
||||||
|
|
||||||
|
Note that a component does not need to be connected in any other way to the store.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class DoSomethingButton extends Component {
|
||||||
|
static template = xml`<button t-on-click="dispatch('something')">Click</button>`;
|
||||||
|
dispatch = useDispatch();
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
```xml
|
### `useGetters`
|
||||||
<button t-name="Counter" t-on-click="dispatch('increment')">
|
|
||||||
Click Me! [<t t-esc="props.value"/>]
|
The `useGetters` hook is useful when a component needs to be able to use the
|
||||||
</button>
|
getters defined in a store. It takes an optional argument, which is a store. If
|
||||||
|
not given, it will use the store in the environment.
|
||||||
|
|
||||||
|
Note that a component does not need to be connected in any other way to the store.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class InfoButton extends Component {
|
||||||
|
static template = xml`<span><t t-esc="getters.somevalue()"></span>`;
|
||||||
|
getters = useGetters();
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Semantics
|
### Semantics
|
||||||
|
|
||||||
The `Store` and the `ConnectedComponent` try to be smart and to optimize as much
|
The `Store` class and the `useStore` hook try to be smart and to optimize as much
|
||||||
as possible the rendering and update process. What is important to know is:
|
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
|
- components are always updated in the order of their creation (so, parent
|
||||||
before children)
|
before children),
|
||||||
- they are updated only if they are in the DOM
|
- they are updated only if they are in the DOM,
|
||||||
- if a parent is asynchronous, the system will wait for it to complete its
|
- 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
|
- 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
|
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.
|
not updated.
|
||||||
|
|
||||||
### Good Practices
|
### Good Practices
|
||||||
|
|
||||||
- avoid asynchronous components as much as possible. Asynchronous components
|
- 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
|
- 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 `mapStoreToProps` and a `Message` component could get the data of its own
|
example, a `MessageList` component could get a list of ids in its `useStore`
|
||||||
message
|
call and a `Message` component could get the data of its own
|
||||||
- since the `mapStoreToProps` function is called for each connected component,
|
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
|
for each state update, it is important to make sure that these functions are
|
||||||
as fast as possible.
|
as fast as possible.
|
||||||
@@ -1,8 +1,15 @@
|
|||||||
# 🦉 Tags 🦉
|
# 🦉 Tags 🦉
|
||||||
|
|
||||||
Tags are very small helper to make it easy to write inline templates. There is
|
## Content
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [`xml` tag](#xml-tag)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
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,
|
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.
|
such as a `css` tag, which will be used to write [single file components](../tooling.md#single-file-component).
|
||||||
|
|
||||||
## XML tag
|
## XML tag
|
||||||
|
|
||||||
@@ -31,8 +38,8 @@ With tags, this process is slightly simplified. The name is uniquely generated,
|
|||||||
and the template is automatically registered:
|
and the template is automatically registered:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import { Component } from 'owl'
|
const { Component } = owl;
|
||||||
import { xml } from 'owl/tags'
|
const { xml } = owl.tags;
|
||||||
|
|
||||||
class MyComponent extends Component {
|
class MyComponent extends Component {
|
||||||
static template = xml`
|
static template = xml`
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
# 🦉 Utils 🦉
|
||||||
|
|
||||||
|
Owl export a few useful utility functions, to help with common issues. Those
|
||||||
|
functions are all available in the `owl.utils` namespace.
|
||||||
|
|
||||||
|
## Content
|
||||||
|
|
||||||
|
- [`whenReady`](#whenready): executing code when DOM is ready
|
||||||
|
- [`loadJS`](#loadjs): loading script files
|
||||||
|
- [`loadFile`](#loadfile): loading a file (useful for templates)
|
||||||
|
- [`escape`](#escape): sanitizing strings
|
||||||
|
- [`debounce`](#debounce): limiting rate of function calls
|
||||||
|
- [`shallowEqual`](#shallowequal): shallow object comparison
|
||||||
|
|
||||||
|
## `whenReady`
|
||||||
|
|
||||||
|
The function `whenReady` returns a `Promise` resolved when the DOM is ready (if
|
||||||
|
not ready yet, resolved directly otherwise). If called with a callback as
|
||||||
|
argument, it executes it as soon as the DOM ready (or directly).
|
||||||
|
|
||||||
|
```js
|
||||||
|
Promise.all([loadFile("templates.xml"), owl.utils.whenReady()]).then(function([templates]) {
|
||||||
|
const qweb = new owl.QWeb({ templates });
|
||||||
|
const app = new App({ qweb });
|
||||||
|
app.mount(document.body);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
or alternatively:
|
||||||
|
|
||||||
|
```js
|
||||||
|
owl.utils.whenReady(function() {
|
||||||
|
const qweb = new owl.QWeb();
|
||||||
|
const app = new App({ qweb });
|
||||||
|
app.mount(document.body);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## `loadJS`
|
||||||
|
|
||||||
|
`loadJS` takes a url (string) for a javascript resource, and loads it (by adding
|
||||||
|
a script tag in the document head). It returns 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.
|
||||||
|
|
||||||
|
For example, it is useful for lazy loading external libraries:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class MyComponent extends owl.Component {
|
||||||
|
willStart() {
|
||||||
|
return owl.utils.loadJS("/static/libs/someLib.js");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## `loadFile`
|
||||||
|
|
||||||
|
`loadFile` is a helper function to fetch a file. It simply
|
||||||
|
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:
|
||||||
|
|
||||||
|
```js
|
||||||
|
async function makeEnv() {
|
||||||
|
const templates = await owl.utils.loadFile("templates.xml");
|
||||||
|
const qweb = new owl.QWeb({ templates });
|
||||||
|
return { qweb };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that unlike `loadJS`, this function returns the content of the file as a
|
||||||
|
string. It does not add a `script` tag or any other side effect.
|
||||||
|
|
||||||
|
## `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
|
||||||
|
<div><t t-esc="user.data"/></div>
|
||||||
|
```
|
||||||
|
|
||||||
|
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, `"<ok>"` 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);
|
||||||
|
```
|
||||||
+69
-21
@@ -3,9 +3,10 @@
|
|||||||
## Content
|
## Content
|
||||||
|
|
||||||
- [Overview](#overview)
|
- [Overview](#overview)
|
||||||
- [Development Mode](#development-mode)
|
|
||||||
- [Playground](#playground)
|
- [Playground](#playground)
|
||||||
- [Benchmarks](#benchmarks)
|
- [Benchmarks](#benchmarks)
|
||||||
|
- [Single File Component](#single-file-component)
|
||||||
|
- [Debugging Script](#debugging-script)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -20,26 +21,6 @@ by using a static http server. A simple python
|
|||||||
server is available in `server.py`. There is also a npm script to start it:
|
server is available in `server.py`. There is also a npm script to start it:
|
||||||
`npm run tools` (and its version with a watcher: `npm run tools:watch`).
|
`npm run tools` (and its version with a watcher: `npm run tools:watch`).
|
||||||
|
|
||||||
## Development Mode
|
|
||||||
|
|
||||||
By default, Owl is in _production_ mode, this means that it will try to do its
|
|
||||||
job fast, and skip some expensive operations. However, in some cases, it is
|
|
||||||
convenient to have better information on what is going on, this is the purpose
|
|
||||||
of the dev mode.
|
|
||||||
|
|
||||||
Owl has a mode flag, in `owl.__info__.mode`. Its default value is `prod`, but
|
|
||||||
it can be set to `dev`:
|
|
||||||
|
|
||||||
```js
|
|
||||||
owl.__info__.mode = "dev";
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that templates compiled with the `prod` settings will not be recompiled.
|
|
||||||
So, changing this setting is best done at startup.
|
|
||||||
|
|
||||||
An important job done by the `dev` mode is to validate props for each component
|
|
||||||
creation and update. Also, extra props will cause an error.
|
|
||||||
|
|
||||||
## Playground
|
## Playground
|
||||||
|
|
||||||
The playground is an important application designed to help learning and
|
The playground is an important application designed to help learning and
|
||||||
@@ -56,3 +37,70 @@ Note: This is more an internal tool, useful for people working on Owl.
|
|||||||
The benchmarks application is a very small application, implemented in different
|
The benchmarks application is a very small application, implemented in different
|
||||||
frameworks, and in different versions of Owl. This is a simple internal tool,
|
frameworks, and in different versions of Owl. This is a simple internal tool,
|
||||||
useful to compare various performance metrics on some tasks.
|
useful to compare various performance metrics on some tasks.
|
||||||
|
|
||||||
|
## Single File Component
|
||||||
|
|
||||||
|
It is very useful to group code by feature instead of by type of file. It makes
|
||||||
|
it easier to scale application to larger size.
|
||||||
|
|
||||||
|
To do so, Owl currently has a small helper that makes it easy to define a
|
||||||
|
template inside a javascript (or typescript) file: the [`xml`](reference/tags.md#xml-tag)
|
||||||
|
helper. With this, a template is automatically registered to [QWeb](reference/qweb_engine.md).
|
||||||
|
|
||||||
|
This means that the template and the javascript code can be defined in the same
|
||||||
|
file. It is not currently possible to add css to the same file, but Owl may
|
||||||
|
get a `css` tag helper later.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { Component } = owl;
|
||||||
|
const { xml } = owl.tags;
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// TEMPLATE
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
const TEMPLATE = xml/* xml */ `
|
||||||
|
<div class="main two-columns">
|
||||||
|
<Sidebar/>
|
||||||
|
<Content />
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// CODE
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
class MyComponent extends Component {
|
||||||
|
static template = TEMPLATE;
|
||||||
|
static components = { Sidebar, Content };
|
||||||
|
|
||||||
|
// rest of component...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that the above example has an inline xml comment, just after the `xml` call.
|
||||||
|
This is useful for some editor plugins, such as the VS Code addon
|
||||||
|
`Comment tagged template`, which, if installed, add syntax highlighting to the
|
||||||
|
content of the template string.
|
||||||
|
|
||||||
|
## Debugging Script
|
||||||
|
|
||||||
|
## Debugging
|
||||||
|
|
||||||
|
Non trivial applications become quickly more difficult to understand. It is then
|
||||||
|
useful to have a solid understanding of what is going on. To help with that,
|
||||||
|
logging useful information is extremely valuable. There is a [javascript file](../tools/debug.js) which can be evaluated in an application.
|
||||||
|
|
||||||
|
Once it is executed, it will log a lot of information on each component main hooks. The following code is a minified version to make it easier to copy/paste:
|
||||||
|
|
||||||
|
```
|
||||||
|
function debugOwl(o,t){let e,n="[OWL_DEBUG]";function l(o){let t=JSON.stringify(o||{});return t.length>200&&(t=t.slice(0,200)+"..."),t}if(Object.defineProperty(o.Component,"current",{get:()=>e,set(s){e=s;const c=s.constructor.name;if(t.componentBlackList&&t.componentBlackList.test(c))return;if(t.componentWhiteList&&!t.componentWhiteList.test(c))return;let r;Object.defineProperty(e,"__owl__",{get:()=>r,set(e){!function(e,s,c){let r=`${s}<id=${c}>`,i=o=>(!t.methodBlackList||!t.methodBlackList.includes(o))&&!(t.methodWhiteList&&!t.methodWhiteList.includes(o));i("constructor")&&console.log(`${n} ${r} constructor, props=${l(e.props)}`);i("willStart")&&o.hooks.onWillStart(()=>{console.log(`${n} ${r} willStart`)});i("mounted")&&o.hooks.onMounted(()=>{console.log(`${n} ${r} mounted`)});i("willUpdateProps")&&o.hooks.onWillUpdateProps(o=>{console.log(`${n} ${r} willUpdateProps, nextprops=${l(o)}`)});i("willPatch")&&o.hooks.onWillPatch(()=>{console.log(`${n} ${r} willPatch`)});i("patched")&&o.hooks.onPatched(()=>{console.log(`${n} ${r} patched`)});i("willUnmount")&&o.hooks.onWillUnmount(()=>{console.log(`${n} ${r} willUnmount`)});const u=e.__render.bind(e);e.__render=function(...o){console.log(`${n} ${r} rendering template`),u(...o)};const d=e.render.bind(e);e.render=function(...o){return console.log(`${n} ${r} render`),d(...o)};const p=e.mount.bind(e);e.mount=function(...o){return console.log(`${n} ${r} mount`),p(...o)}}(s,c,(r=e).id)}})}}),t.logScheduler){let t=o.Component.scheduler.start,e=o.Component.scheduler.stop;o.Component.scheduler.start=function(){console.log(`${n} scheduler: start running tasks queue`),t.call(this)},o.Component.scheduler.stop=function(){console.log(`${n} scheduler: stop running tasks queue`),e.call(this)}}if(t.logStore){let t=o.Store.prototype.dispatch;o.Store.prototype.dispatch=function(o,...e){return console.log(`${n} store: action '${o}' dispatched. Payload: '${l(e)}'`),t.call(this,o,...e)}}}
|
||||||
|
debugOwl({
|
||||||
|
// componentBlackList: /App/, // regexp
|
||||||
|
// componentWhiteList: /SomeComponent/, // regexp
|
||||||
|
// methodBlackList: ["mounted"], // list of method names
|
||||||
|
// methodWhiteList: ["willStart"], // list of method names
|
||||||
|
logScheduler: false, // display/mute scheduler logs
|
||||||
|
logStore: true, // display/mute store logs
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that it is certainly useful to run this code at some point in an application,
|
||||||
|
just to get a feel of what each user action implies, for the framework.
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
# 🦉 Utils 🦉
|
|
||||||
|
|
||||||
Owl export a few useful utility functions, to help with common issues. Those
|
|
||||||
functions are all available in the `owl.utils` namespace.
|
|
||||||
|
|
||||||
## Content
|
|
||||||
|
|
||||||
- [`whenReady`](#whenready): executing code when DOM is ready
|
|
||||||
- [`loadJS`](#loadjs): loading script files
|
|
||||||
- [`loadTemplates`](#loadtemplates): loading xml files
|
|
||||||
- [`escape`](#escape): sanitizing strings
|
|
||||||
- [`debounce`](#debounce): limiting rate of function calls
|
|
||||||
|
|
||||||
## `whenReady`
|
|
||||||
|
|
||||||
The function `whenReady` returns a `Promise` resolved when the DOM is ready (if
|
|
||||||
not ready yet, resolved directly otherwise). If called with a callback as
|
|
||||||
argument, it executes it as soon as the DOM ready (or directly).
|
|
||||||
|
|
||||||
```js
|
|
||||||
Promise.all([loadTemplates(), owl.utils.whenReady()]).then(function([templates]) {
|
|
||||||
const qweb = new owl.QWeb(templates);
|
|
||||||
const app = new App({ qweb });
|
|
||||||
app.mount(document.body);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
```js
|
|
||||||
owl.utils.whenReady(function() {
|
|
||||||
const qweb = new owl.QWeb();
|
|
||||||
const app = new App({ qweb });
|
|
||||||
app.mount(document.body);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## `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:
|
|
||||||
it maintains a list of urls previously loaded (or currently being loaded), and
|
|
||||||
prevent doing twice the work.
|
|
||||||
|
|
||||||
```js
|
|
||||||
class MyComponent extends owl.Component {
|
|
||||||
willStart() {
|
|
||||||
return owl.utils.loadJS("/static/libs/someLib.js");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## `loadTemplates`
|
|
||||||
|
|
||||||
```js
|
|
||||||
async function makeEnv() {
|
|
||||||
const templates = await owl.utils.loadTemplates("templates.xml");
|
|
||||||
const qweb = new owl.QWeb(templates);
|
|
||||||
return { qweb };
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## `escape`
|
|
||||||
|
|
||||||
## `debounce`
|
|
||||||
+8
-3
@@ -1,8 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "owl-framework",
|
"name": "owl-framework",
|
||||||
"version": "0.21.0",
|
"version": "1.0.0-beta1",
|
||||||
"description": "Odoo Web Library (OWL)",
|
"description": "Odoo Web Library (OWL)",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.15.3"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:js": "tsc --target esnext --module es6 --outDir dist/owl",
|
"build:js": "tsc --target esnext --module es6 --outDir dist/owl",
|
||||||
"build:bundle": "rollup -c",
|
"build:bundle": "rollup -c",
|
||||||
@@ -14,7 +17,8 @@
|
|||||||
"tools:serve": "python3 tools/server.py || python tools/server.py",
|
"tools:serve": "python3 tools/server.py || python tools/server.py",
|
||||||
"tools": "npm run build && npm run tools:serve",
|
"tools": "npm run build && npm run tools:serve",
|
||||||
"pretools:watch": "npm run build",
|
"pretools:watch": "npm run build",
|
||||||
"tools:watch": "npm-run-all --parallel tools:serve \"build:* -- --watch\""
|
"tools:watch": "npm-run-all --parallel tools:serve \"build:* -- --watch\"",
|
||||||
|
"prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --write"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -35,12 +39,13 @@
|
|||||||
"jest-environment-jsdom": "^24.7.1",
|
"jest-environment-jsdom": "^24.7.1",
|
||||||
"live-server": "^1.2.1",
|
"live-server": "^1.2.1",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
|
"prettier": "^1.19.1",
|
||||||
"rollup": "^1.6.0",
|
"rollup": "^1.6.0",
|
||||||
"rollup-plugin-typescript2": "^0.20.1",
|
"rollup-plugin-typescript2": "^0.20.1",
|
||||||
"sass": "^1.16.1",
|
"sass": "^1.16.1",
|
||||||
"source-map-support": "^0.5.10",
|
"source-map-support": "^0.5.10",
|
||||||
"ts-jest": "^23.10.5",
|
"ts-jest": "^23.10.5",
|
||||||
"typescript": "^3.2.2",
|
"typescript": "^3.7.2",
|
||||||
"uglify-es": "^3.3.9"
|
"uglify-es": "^3.3.9"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
|
|||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
# 🦉 OWL Roadmap 🦉
|
||||||
|
|
||||||
|
- Current version: 1.0.0-beta1
|
||||||
|
- Status: mostly stable
|
||||||
|
|
||||||
|
This roadmap is only an attempt at predicting Owl's future. Everything may
|
||||||
|
change!
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### December 2019
|
||||||
|
|
||||||
|
If all goes well, Owl is upgraded to beta status. From now on, no API change,
|
||||||
|
even small, is expected (but it still could happen).
|
||||||
|
|
||||||
|
### End of 2019
|
||||||
|
|
||||||
|
Release v1.0
|
||||||
|
|
||||||
|
- API should be stable,
|
||||||
|
- we will use semantic versioning,
|
||||||
|
- we will maintain a changelog and an upgrade guide.
|
||||||
|
|
||||||
|
### 1.x
|
||||||
|
|
||||||
|
- add chrome and firefox devtools,
|
||||||
|
- add support for single file components,
|
||||||
|
- fix every bugs,
|
||||||
|
- improve documentation,
|
||||||
|
- small backward compatible improvements.
|
||||||
|
|
||||||
|
### 2.x (2021? 2022?)
|
||||||
|
|
||||||
|
Maybe:
|
||||||
|
|
||||||
|
- reimplement vdom to use *block* system, like Vue 3, which should make Owl
|
||||||
|
much faster
|
||||||
|
- refactor `QWeb` to use an intermediate representation (some kind of AST) to
|
||||||
|
allow additional optimisations.
|
||||||
|
|
||||||
|
|
||||||
+254
-307
@@ -1,8 +1,11 @@
|
|||||||
import { Observer } from "../core/observer";
|
import { Observer } from "../core/observer";
|
||||||
|
import { OwlEvent } from "../core/owl_event";
|
||||||
import { CompiledTemplate, QWeb } from "../qweb/index";
|
import { CompiledTemplate, QWeb } from "../qweb/index";
|
||||||
import { h, patch, VNode } from "../vdom/index";
|
import { patch, VNode } from "../vdom/index";
|
||||||
import "./directive";
|
import "./directive";
|
||||||
|
import { Fiber } from "./fiber";
|
||||||
import "./props_validation";
|
import "./props_validation";
|
||||||
|
import { Scheduler, scheduler } from "./scheduler";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Owl Component System
|
* Owl Component System
|
||||||
@@ -11,7 +14,7 @@ import "./props_validation";
|
|||||||
* contains:
|
* contains:
|
||||||
*
|
*
|
||||||
* - the Env interface (generic type for the environment)
|
* - the Env interface (generic type for the environment)
|
||||||
* - the Meta interface (the owl specific metadata attached to a component)
|
* - the Internal interface (the owl specific metadata attached to a component)
|
||||||
* - the Component class
|
* - the Component class
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -42,43 +45,63 @@ interface Internal<T extends Env, Props> {
|
|||||||
// each component has a unique id, useful mostly to handle parent/child
|
// each component has a unique id, useful mostly to handle parent/child
|
||||||
// relationships
|
// relationships
|
||||||
readonly id: number;
|
readonly id: number;
|
||||||
|
depth: number;
|
||||||
vnode: VNode | null;
|
vnode: VNode | null;
|
||||||
|
pvnode: VNode | null;
|
||||||
isMounted: boolean;
|
isMounted: boolean;
|
||||||
isDestroyed: boolean;
|
isDestroyed: boolean;
|
||||||
|
|
||||||
// parent and children keys are obviously useful to setup the parent-children
|
// parent and children keys are obviously useful to setup the parent-children
|
||||||
// relationship.
|
// relationship.
|
||||||
parent: Component<T, any, any> | null;
|
parent: Component<T, any> | null;
|
||||||
children: { [key: number]: Component<T, any, any> };
|
children: { [key: number]: Component<T, any> };
|
||||||
// children mapping: from templateID to componentID. templateID identifies a
|
// children mapping: from templateID to componentID. templateID identifies a
|
||||||
// place in a template. The t-component directive needs it to be able to get
|
// place in a template. The t-component directive needs it to be able to get
|
||||||
// the component instance back whenever the template is rerendered.
|
// the component instance back whenever the template is rerendered.
|
||||||
cmap: { [key: number]: number };
|
cmap: { [key: number]: number };
|
||||||
|
|
||||||
renderId: number;
|
currentFiber: Fiber | null;
|
||||||
|
// parentLastFiberId is there to help the parent component to detect, among
|
||||||
|
// its children, those that are not used anymore and thus can be destroyed
|
||||||
|
parentLastFiberId: number;
|
||||||
|
|
||||||
// the renderProps and renderPromise keys are only useful for the "prepare"
|
// when a rendering is initiated by a parent, it may set variables in 'scope'
|
||||||
// step of the lifecycle of a component. Once a component has been rendered
|
// and 'vars' (typically when the component is rendered in a slot). We need to
|
||||||
// and patched, it is no longer useful.
|
// store that information in case the component would be re-rendered later on.
|
||||||
renderProps: Props | null;
|
scope: any;
|
||||||
renderPromise: Promise<VNode> | null;
|
vars: any;
|
||||||
|
|
||||||
boundHandlers: { [key: number]: any };
|
boundHandlers: { [key: number]: any };
|
||||||
observer: Observer | null;
|
observer: Observer | null;
|
||||||
render: CompiledTemplate | null;
|
renderFn: CompiledTemplate;
|
||||||
mountedHandlers: { [key: number]: Function };
|
mountedCB: Function | null;
|
||||||
|
willUnmountCB: Function | null;
|
||||||
|
willPatchCB: Function | null;
|
||||||
|
patchedCB: Function | null;
|
||||||
|
willStartCB: Function | null;
|
||||||
|
willUpdatePropsCB: Function | null;
|
||||||
classObj: { [key: string]: boolean } | null;
|
classObj: { [key: string]: boolean } | null;
|
||||||
|
refs: { [key: string]: Component<T, any> | HTMLElement | undefined } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const portalSymbol = Symbol("portal"); // FIXME
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Component
|
// Component
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
let nextId = 1;
|
let nextId = 1;
|
||||||
|
|
||||||
export class Component<T extends Env, Props extends {}, State extends {}> {
|
export class Component<T extends Env, Props extends {}> {
|
||||||
readonly __owl__: Internal<Env, Props>;
|
readonly __owl__: Internal<Env, Props>;
|
||||||
static template?: string | null = null;
|
static template?: string | null = null;
|
||||||
static _template?: string | null = null;
|
static _template?: string | null = null;
|
||||||
|
static current: Component<any, any> | null = null;
|
||||||
|
static components = {};
|
||||||
|
static props?: any;
|
||||||
|
static defaultProps?: any;
|
||||||
|
static env: any = {};
|
||||||
|
// expose scheduler s.t. it can be mocked for testing purposes
|
||||||
|
static scheduler: Scheduler = scheduler;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `el` is the root element of the component. Note that it could be null:
|
* The `el` is the root element of the component. Note that it could be null:
|
||||||
@@ -87,20 +110,10 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
get el(): HTMLElement | null {
|
get el(): HTMLElement | null {
|
||||||
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
|
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
|
||||||
}
|
}
|
||||||
static components = {};
|
|
||||||
|
|
||||||
env: T;
|
env: T;
|
||||||
state?: State;
|
|
||||||
props: Props;
|
props: Props;
|
||||||
|
|
||||||
// type of props is not easily representable in typescript...
|
|
||||||
static props?: any;
|
|
||||||
static defaultProps?: any;
|
|
||||||
|
|
||||||
refs: {
|
|
||||||
[key: string]: Component<T, any, any> | HTMLElement | undefined;
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
@@ -108,44 +121,36 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
/**
|
/**
|
||||||
* Creates an instance of Component.
|
* Creates an instance of Component.
|
||||||
*
|
*
|
||||||
* The root component of a component tree needs an environment:
|
|
||||||
*
|
|
||||||
* ```javascript
|
|
||||||
* const root = new RootComponent(env, props);
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* Every other component simply needs a reference to its parent:
|
|
||||||
*
|
|
||||||
* ```javascript
|
|
||||||
* const child = new SomeComponent(parent, props);
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* Note that most of the time, only the root component needs to be created by
|
* Note that most of the time, only the root component needs to be created by
|
||||||
* hand. Other components should be created automatically by the framework (with
|
* hand. Other components should be created automatically by the framework (with
|
||||||
* the t-component directive in a template)
|
* the t-component directive in a template)
|
||||||
*/
|
*/
|
||||||
constructor(parent: Component<T, any, any> | T, props?: Props) {
|
constructor(parent?: Component<T, any> | null, props?: Props) {
|
||||||
const defaultProps = (<any>this.constructor).defaultProps;
|
Component.current = this;
|
||||||
|
|
||||||
|
let constr = this.constructor as any;
|
||||||
|
const defaultProps = constr.defaultProps;
|
||||||
if (defaultProps) {
|
if (defaultProps) {
|
||||||
props = this.__applyDefaultProps(props, defaultProps);
|
props = props || ({} as Props);
|
||||||
|
this.__applyDefaultProps(props, defaultProps);
|
||||||
}
|
}
|
||||||
// is this a good idea?
|
this.props = <Props>props;
|
||||||
// Pro: if props is empty, we can create easily a component
|
|
||||||
// Con: this is not really safe
|
|
||||||
// Pro: but creating component (by a template) is always unsafe anyway
|
|
||||||
this.props = <Props>props || <Props>{};
|
|
||||||
let id: number = nextId++;
|
|
||||||
let p: Component<T, any, any> | null = null;
|
|
||||||
if (parent instanceof Component) {
|
|
||||||
p = parent;
|
|
||||||
this.env = parent.env;
|
|
||||||
parent.__owl__.children[id] = this;
|
|
||||||
} else {
|
|
||||||
this.env = parent;
|
|
||||||
if (QWeb.dev) {
|
if (QWeb.dev) {
|
||||||
// we only validate props for root widgets here. "Regular" widget
|
QWeb.utils.validateProps(constr, this.props);
|
||||||
// props are validated by the t-component directive
|
}
|
||||||
QWeb.utils.validateProps(this.constructor, this.props);
|
|
||||||
|
const id: number = nextId++;
|
||||||
|
let depth;
|
||||||
|
if (parent) {
|
||||||
|
this.env = parent.env;
|
||||||
|
const __powl__ = parent.__owl__;
|
||||||
|
__powl__.children[id] = this;
|
||||||
|
depth = __powl__.depth + 1;
|
||||||
|
} else {
|
||||||
|
// we are the root component
|
||||||
|
this.env = (this.constructor as any).env;
|
||||||
|
if (!this.env.qweb) {
|
||||||
|
this.env.qweb = new QWeb();
|
||||||
}
|
}
|
||||||
this.env.qweb.on("update", this, () => {
|
this.env.qweb.on("update", this, () => {
|
||||||
if (this.__owl__.isMounted) {
|
if (this.__owl__.isMounted) {
|
||||||
@@ -160,23 +165,36 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
this.env.qweb.off("update", this);
|
this.env.qweb.off("update", this);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
depth = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const qweb = this.env.qweb;
|
||||||
|
const template = constr.template || this.__getTemplate(qweb);
|
||||||
this.__owl__ = {
|
this.__owl__ = {
|
||||||
id: id,
|
id: id,
|
||||||
|
depth: depth,
|
||||||
vnode: null,
|
vnode: null,
|
||||||
|
pvnode: null,
|
||||||
isMounted: false,
|
isMounted: false,
|
||||||
isDestroyed: false,
|
isDestroyed: false,
|
||||||
parent: p,
|
parent: parent || null,
|
||||||
children: {},
|
children: {},
|
||||||
cmap: {},
|
cmap: {},
|
||||||
renderId: 1,
|
currentFiber: null,
|
||||||
renderPromise: null,
|
parentLastFiberId: 0,
|
||||||
renderProps: props || null,
|
|
||||||
boundHandlers: {},
|
boundHandlers: {},
|
||||||
mountedHandlers: {},
|
mountedCB: null,
|
||||||
|
willUnmountCB: null,
|
||||||
|
willPatchCB: null,
|
||||||
|
patchedCB: null,
|
||||||
|
willStartCB: null,
|
||||||
|
willUpdatePropsCB: null,
|
||||||
observer: null,
|
observer: null,
|
||||||
render: null,
|
renderFn: qweb.render.bind(qweb, template),
|
||||||
classObj: null
|
classObj: null,
|
||||||
|
refs: null,
|
||||||
|
scope: null,
|
||||||
|
vars: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,8 +241,6 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
* It is not called on the initial render. This is useful to get some
|
* 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
|
* information which are in the DOM. For example, the current position of the
|
||||||
* scrollbar
|
* scrollbar
|
||||||
*
|
|
||||||
* The return value of willPatch will be given to the patched function.
|
|
||||||
*/
|
*/
|
||||||
willPatch(): any {}
|
willPatch(): any {}
|
||||||
|
|
||||||
@@ -240,10 +256,8 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
* One need to be careful, because updates here will cause rerender, which in
|
* One need to be careful, because updates here will cause rerender, which in
|
||||||
* turn will cause other calls to updated. So, we need to be particularly
|
* turn will cause other calls to updated. So, we need to be particularly
|
||||||
* careful at avoiding endless cycles.
|
* careful at avoiding endless cycles.
|
||||||
*
|
|
||||||
* The snapshot parameter is the result of the call to willPatch.
|
|
||||||
*/
|
*/
|
||||||
patched(snapshot: any) {}
|
patched() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* willUnmount is a hook that is called each time just before a component is
|
* willUnmount is a hook that is called each time just before a component is
|
||||||
@@ -259,8 +273,11 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
/**
|
/**
|
||||||
* catchError is a method called whenever some error happens in the rendering or
|
* catchError is a method called whenever some error happens in the rendering or
|
||||||
* lifecycle hooks of a child.
|
* lifecycle hooks of a child.
|
||||||
|
*
|
||||||
|
* It needs to be implemented by a component that is designed to handle the
|
||||||
|
* error properly.
|
||||||
*/
|
*/
|
||||||
catchError(error: Error): void {}
|
catchError?(error?: Error): void;
|
||||||
|
|
||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
// Public
|
// Public
|
||||||
@@ -274,28 +291,24 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
*
|
*
|
||||||
* Note that a component can be mounted an unmounted several times
|
* Note that a component can be mounted an unmounted several times
|
||||||
*/
|
*/
|
||||||
async mount(target: HTMLElement, renderBeforeRemount: boolean = false): Promise<void> {
|
async mount(target: HTMLElement | DocumentFragment): Promise<void> {
|
||||||
const __owl__ = this.__owl__;
|
const __owl__ = this.__owl__;
|
||||||
if (__owl__.isMounted) {
|
if (__owl__.isMounted) {
|
||||||
return;
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
if (!(target instanceof HTMLElement || target instanceof DocumentFragment)) {
|
||||||
|
let message = `Component '${this.constructor.name}' cannot be mounted: the target is not a valid DOM node.`;
|
||||||
|
message += `\nMaybe the DOM is not ready yet? (in that case, you can use owl.utils.whenReady)`;
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
const fiber = new Fiber(null, this, false, target);
|
||||||
|
fiber.shouldPatch = false;
|
||||||
if (!__owl__.vnode) {
|
if (!__owl__.vnode) {
|
||||||
const vnode = await this.__prepare();
|
this.__prepareAndRender(fiber, () => {});
|
||||||
if (__owl__.isDestroyed) {
|
} else {
|
||||||
// component was destroyed before we get here...
|
this.__render(fiber);
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.__patch(vnode);
|
|
||||||
} else if (renderBeforeRemount) {
|
|
||||||
const patchQueue = [];
|
|
||||||
await this.__render(false, patchQueue, undefined, undefined);
|
|
||||||
this.__applyPatchQueue(<any[]>patchQueue);
|
|
||||||
}
|
|
||||||
target.appendChild(this.el!);
|
|
||||||
|
|
||||||
if (document.body.contains(target)) {
|
|
||||||
this.__callMounted();
|
|
||||||
}
|
}
|
||||||
|
return scheduler.addFiber(fiber);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -320,19 +333,34 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
*/
|
*/
|
||||||
async render(force: boolean = false): Promise<void> {
|
async render(force: boolean = false): Promise<void> {
|
||||||
const __owl__ = this.__owl__;
|
const __owl__ = this.__owl__;
|
||||||
if (!__owl__.isMounted) {
|
if (!__owl__.isMounted && !__owl__.currentFiber) {
|
||||||
|
// if we get here, this means that the component was either never mounted,
|
||||||
|
// or was unmounted and some state change triggered a render. Either way,
|
||||||
|
// we do not want to actually render anything in this case.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const patchQueue = [];
|
if (__owl__.currentFiber && !__owl__.currentFiber.isRendered) {
|
||||||
|
return scheduler.addFiber(__owl__.currentFiber.root);
|
||||||
const renderId = ++__owl__.renderId;
|
|
||||||
await this.__render(force, patchQueue, undefined, undefined);
|
|
||||||
|
|
||||||
if (__owl__.isMounted && renderId === __owl__.renderId) {
|
|
||||||
// we only update the vnode and the actual DOM if no other rendering
|
|
||||||
// occurred between now and when the render method was initially called.
|
|
||||||
this.__applyPatchQueue(<any[]>patchQueue);
|
|
||||||
}
|
}
|
||||||
|
// if we aren't mounted at this point, it implies that there is a
|
||||||
|
// currentFiber that is already rendered (isRendered is true), so we are
|
||||||
|
// about to be mounted
|
||||||
|
const isMounted = __owl__.isMounted;
|
||||||
|
const fiber = new Fiber(null, this, force, null);
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
if (__owl__.isMounted || !isMounted) {
|
||||||
|
// we are mounted (__owl__.isMounted), or if we are currently being
|
||||||
|
// mounted (!isMounted), so we call __render
|
||||||
|
this.__render(fiber);
|
||||||
|
} else {
|
||||||
|
// we were mounted when render was called, but we aren't anymore, so we
|
||||||
|
// were actually about to be unmounted ; we can thus forget about this
|
||||||
|
// fiber
|
||||||
|
fiber.isCompleted = true;
|
||||||
|
__owl__.currentFiber = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return scheduler.addFiber(fiber);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -364,26 +392,6 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This method is the correct way to update the environment of a component. Doing
|
|
||||||
* this will cause a full rerender of the component and its children, so this is
|
|
||||||
* an operation that should not be done frequently.
|
|
||||||
*
|
|
||||||
* A good usecase for updating the environment would be to update some mostly
|
|
||||||
* static config keys, such as a boolean to determine if we are in mobile
|
|
||||||
* mode or not.
|
|
||||||
*/
|
|
||||||
async updateEnv(nextEnv: Partial<T>): Promise<void> {
|
|
||||||
const __owl__ = this.__owl__;
|
|
||||||
if (__owl__.parent && __owl__.parent.env === this.env) {
|
|
||||||
this.env = Object.create(this.env);
|
|
||||||
}
|
|
||||||
Object.assign(this.env, nextEnv);
|
|
||||||
if (__owl__.isMounted) {
|
|
||||||
await this.render(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emit a custom event of type 'eventType' with the given 'payload' on the
|
* Emit a custom event of type 'eventType' with the given 'payload' on the
|
||||||
* component's el, if it exists. However, note that the event will only bubble
|
* component's el, if it exists. However, note that the event will only bubble
|
||||||
@@ -391,14 +399,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
* willUnmount().
|
* willUnmount().
|
||||||
*/
|
*/
|
||||||
trigger(eventType: string, payload?: any) {
|
trigger(eventType: string, payload?: any) {
|
||||||
if (this.el) {
|
this.__trigger(this, eventType, payload);
|
||||||
const ev = new CustomEvent(eventType, {
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
detail: payload
|
|
||||||
});
|
|
||||||
this.el.dispatchEvent(ev);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
@@ -416,10 +417,13 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
* Note that it does not call the __callWillUnmount method to avoid visiting
|
* Note that it does not call the __callWillUnmount method to avoid visiting
|
||||||
* all children many times.
|
* all children many times.
|
||||||
*/
|
*/
|
||||||
__destroy(parent: Component<any, any, any> | null) {
|
__destroy(parent: Component<any, any> | null) {
|
||||||
const __owl__ = this.__owl__;
|
const __owl__ = this.__owl__;
|
||||||
const isMounted = __owl__.isMounted;
|
const isMounted = __owl__.isMounted;
|
||||||
if (isMounted) {
|
if (isMounted) {
|
||||||
|
if (__owl__.willUnmountCB) {
|
||||||
|
__owl__.willUnmountCB();
|
||||||
|
}
|
||||||
this.willUnmount();
|
this.willUnmount();
|
||||||
__owl__.isMounted = false;
|
__owl__.isMounted = false;
|
||||||
}
|
}
|
||||||
@@ -434,33 +438,33 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
}
|
}
|
||||||
__owl__.isDestroyed = true;
|
__owl__.isDestroyed = true;
|
||||||
delete __owl__.vnode;
|
delete __owl__.vnode;
|
||||||
|
if (__owl__.currentFiber) {
|
||||||
|
__owl__.currentFiber.isCompleted = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
__callMounted() {
|
__callMounted() {
|
||||||
const __owl__ = this.__owl__;
|
const __owl__ = this.__owl__;
|
||||||
const children = __owl__.children;
|
|
||||||
for (let id in children) {
|
|
||||||
const comp = children[id];
|
|
||||||
if (!comp.__owl__.isMounted && this.el!.contains(comp.el)) {
|
|
||||||
comp.__callMounted();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
__owl__.isMounted = true;
|
__owl__.isMounted = true;
|
||||||
const handlers = __owl__.mountedHandlers;
|
__owl__.currentFiber = null;
|
||||||
for (let key in handlers) {
|
|
||||||
handlers[key]();
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
this.mounted();
|
this.mounted();
|
||||||
} catch (e) {
|
if (__owl__.mountedCB) {
|
||||||
errorHandler(e, this);
|
__owl__.mountedCB();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
__callWillUnmount() {
|
__callWillUnmount() {
|
||||||
this.willUnmount();
|
|
||||||
const __owl__ = this.__owl__;
|
const __owl__ = this.__owl__;
|
||||||
|
if (__owl__.willUnmountCB) {
|
||||||
|
__owl__.willUnmountCB();
|
||||||
|
}
|
||||||
|
this.willUnmount();
|
||||||
__owl__.isMounted = false;
|
__owl__.isMounted = false;
|
||||||
|
if (this.__owl__.currentFiber) {
|
||||||
|
this.__owl__.currentFiber.isCompleted = true;
|
||||||
|
this.__owl__.currentFiber.root.counter = 0;
|
||||||
|
}
|
||||||
const children = __owl__.children;
|
const children = __owl__.children;
|
||||||
for (let id in children) {
|
for (let id in children) {
|
||||||
const comp = children[id];
|
const comp = children[id];
|
||||||
@@ -469,27 +473,59 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Private trigger method, allows to choose the component which triggered
|
||||||
|
* the event in the first place
|
||||||
|
*/
|
||||||
|
__trigger(component: Component<any, any>, eventType: string, payload?: any) {
|
||||||
|
if (this.el) {
|
||||||
|
const ev = new OwlEvent(component, eventType, {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
detail: payload
|
||||||
|
});
|
||||||
|
const triggerHook = this.env[portalSymbol as any];
|
||||||
|
if (triggerHook) {
|
||||||
|
triggerHook(ev);
|
||||||
|
}
|
||||||
|
this.el.dispatchEvent(ev);
|
||||||
|
}
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* The __updateProps method is called by the t-component directive whenever
|
* The __updateProps method is called by the t-component directive whenever
|
||||||
* it updates a component (so, when the parent template is rerendered).
|
* it updates a component (so, when the parent template is rerendered).
|
||||||
*/
|
*/
|
||||||
async __updateProps(
|
async __updateProps(nextProps: Props, parentFiber: Fiber, scope: any, vars: any): Promise<void> {
|
||||||
nextProps: Props,
|
this.__owl__.scope = scope;
|
||||||
forceUpdate: boolean = false,
|
this.__owl__.vars = vars;
|
||||||
patchQueue?: any[],
|
const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps);
|
||||||
scope?: any,
|
|
||||||
vars?: any
|
|
||||||
): Promise<void> {
|
|
||||||
const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
|
|
||||||
if (shouldUpdate) {
|
if (shouldUpdate) {
|
||||||
|
const __owl__ = this.__owl__;
|
||||||
|
const fiber = new Fiber(parentFiber, this, parentFiber.force, null);
|
||||||
|
if (!parentFiber.child) {
|
||||||
|
parentFiber.child = fiber;
|
||||||
|
} else {
|
||||||
|
parentFiber.lastChild!.sibling = fiber;
|
||||||
|
}
|
||||||
|
parentFiber.lastChild = fiber;
|
||||||
|
|
||||||
const defaultProps = (<any>this.constructor).defaultProps;
|
const defaultProps = (<any>this.constructor).defaultProps;
|
||||||
if (defaultProps) {
|
if (defaultProps) {
|
||||||
nextProps = this.__applyDefaultProps(nextProps, defaultProps);
|
this.__applyDefaultProps(nextProps, defaultProps);
|
||||||
|
}
|
||||||
|
if (QWeb.dev) {
|
||||||
|
QWeb.utils.validateProps(this.constructor, nextProps);
|
||||||
|
}
|
||||||
|
await Promise.all([
|
||||||
|
this.willUpdateProps(nextProps),
|
||||||
|
__owl__.willUpdatePropsCB && __owl__.willUpdatePropsCB(nextProps)
|
||||||
|
]);
|
||||||
|
if (fiber.isCompleted) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
await this.willUpdateProps(nextProps);
|
|
||||||
this.props = nextProps;
|
this.props = nextProps;
|
||||||
await this.__render(forceUpdate, patchQueue, scope, vars);
|
|
||||||
|
this.__render(fiber);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -497,37 +533,35 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
* Main patching method. We call the virtual dom patch method here to convert
|
* Main patching method. We call the virtual dom patch method here to convert
|
||||||
* a virtual dom vnode into some actual dom.
|
* a virtual dom vnode into some actual dom.
|
||||||
*/
|
*/
|
||||||
__patch(vnode) {
|
__patch(vnode: VNode) {
|
||||||
const __owl__ = this.__owl__;
|
const __owl__ = this.__owl__;
|
||||||
const target = __owl__.vnode || document.createElement(vnode.sel!);
|
const target = __owl__.vnode || document.createElement(vnode.sel!);
|
||||||
__owl__.vnode = patch(target, vnode);
|
__owl__.vnode = patch(target, vnode);
|
||||||
}
|
}
|
||||||
|
|
||||||
__prepare(scope?: Object, vars?: any): Promise<VNode> {
|
/**
|
||||||
const __owl__ = this.__owl__;
|
* The __prepare method is only called by the t-component directive, when a
|
||||||
__owl__.renderProps = this.props;
|
* subcomponent is created. It gets its scope and vars, if any, from the
|
||||||
__owl__.renderPromise = this.__prepareAndRender(scope, vars);
|
* parent template.
|
||||||
return __owl__.renderPromise;
|
*/
|
||||||
|
__prepare(parentFiber: Fiber, scope: any, vars: any, cb: CallableFunction): Fiber {
|
||||||
|
this.__owl__.scope = scope;
|
||||||
|
this.__owl__.vars = vars;
|
||||||
|
const fiber = new Fiber(parentFiber, this, parentFiber.force, null);
|
||||||
|
fiber.shouldPatch = false;
|
||||||
|
if (!parentFiber.child) {
|
||||||
|
parentFiber.child = fiber;
|
||||||
|
} else {
|
||||||
|
parentFiber.lastChild!.sibling = fiber;
|
||||||
|
}
|
||||||
|
parentFiber.lastChild = fiber;
|
||||||
|
this.__prepareAndRender(fiber, cb);
|
||||||
|
return fiber;
|
||||||
}
|
}
|
||||||
|
|
||||||
async __prepareAndRender(scope?: Object, vars?: any): Promise<VNode> {
|
__getTemplate(qweb: QWeb): string {
|
||||||
try {
|
|
||||||
await this.willStart();
|
|
||||||
} catch (e) {
|
|
||||||
errorHandler(e, this);
|
|
||||||
return Promise.resolve(h("div"));
|
|
||||||
}
|
|
||||||
const __owl__ = this.__owl__;
|
|
||||||
if (__owl__.isDestroyed) {
|
|
||||||
return Promise.resolve(h("div"));
|
|
||||||
}
|
|
||||||
const qweb = this.env.qweb;
|
|
||||||
let p = (<any>this).constructor;
|
let p = (<any>this).constructor;
|
||||||
// console.warn(p, p.template, p._template, 'template' in p, p.hasOwnProperty('template'))
|
|
||||||
if (!p.hasOwnProperty("_template")) {
|
if (!p.hasOwnProperty("_template")) {
|
||||||
if (p.template) {
|
|
||||||
p._template = p.template;
|
|
||||||
} else {
|
|
||||||
// here, the component and none of its superclasses defines a static `template`
|
// here, the component and none of its superclasses defines a static `template`
|
||||||
// key. So we fall back on looking for a template matching its name (or
|
// key. So we fall back on looking for a template matching its name (or
|
||||||
// one of its subclass).
|
// one of its subclass).
|
||||||
@@ -542,76 +576,68 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
p._template = template;
|
p._template = template;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return p._template;
|
||||||
|
}
|
||||||
|
async __prepareAndRender(fiber: Fiber, cb: CallableFunction) {
|
||||||
|
try {
|
||||||
|
await Promise.all([this.willStart(), this.__owl__.willStartCB && this.__owl__.willStartCB()]);
|
||||||
|
} catch (e) {
|
||||||
|
fiber.handleError(e);
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
if (this.__owl__.isDestroyed) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
if (!fiber.isCompleted) {
|
||||||
|
this.__render(fiber);
|
||||||
|
cb();
|
||||||
}
|
}
|
||||||
__owl__.render = qweb.render.bind(qweb, p._template);
|
|
||||||
this.__observeState();
|
|
||||||
|
|
||||||
return this.__render(false, [], scope, vars);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
__render(
|
__render(fiber: Fiber) {
|
||||||
force: boolean = false,
|
|
||||||
patchQueue: any[] = [],
|
|
||||||
scope?: Object,
|
|
||||||
vars?: any
|
|
||||||
): Promise<VNode> {
|
|
||||||
const __owl__ = this.__owl__;
|
const __owl__ = this.__owl__;
|
||||||
const promises: Promise<void>[] = [];
|
|
||||||
const patch: any[] = [this];
|
|
||||||
patchQueue.push(patch);
|
|
||||||
if (__owl__.observer) {
|
if (__owl__.observer) {
|
||||||
__owl__.observer.allowMutations = false;
|
__owl__.observer.allowMutations = false;
|
||||||
}
|
}
|
||||||
let vnode;
|
let error;
|
||||||
try {
|
try {
|
||||||
vnode = __owl__.render!(this, {
|
let vnode = __owl__.renderFn!(this, {
|
||||||
promises,
|
|
||||||
handlers: __owl__.boundHandlers,
|
handlers: __owl__.boundHandlers,
|
||||||
mountedHandlers: __owl__.mountedHandlers,
|
fiber: fiber
|
||||||
forceUpdate: force,
|
|
||||||
patchQueue,
|
|
||||||
scope,
|
|
||||||
vars
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
// we iterate over the children to detect those that no longer belong to the
|
||||||
vnode = __owl__.vnode || h("div");
|
// current rendering: those ones, if not mounted yet, can (and have to) be
|
||||||
errorHandler(e, this);
|
// destroyed right now, because they are not in the DOM, and thus we won't
|
||||||
|
// be notified later on (when patching), that they are removed from the DOM
|
||||||
|
for (let childKey in __owl__.children) {
|
||||||
|
let child = __owl__.children[childKey];
|
||||||
|
if (!child.__owl__.isMounted && child.__owl__.parentLastFiberId < fiber.id) {
|
||||||
|
child.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!vnode) {
|
||||||
|
throw new Error(`Rendering '${this.constructor.name}' did not return anything`);
|
||||||
|
}
|
||||||
|
fiber.vnode = vnode;
|
||||||
|
// we apply here the class information described on the component by the
|
||||||
|
// template (so, something like <MyComponent class="..."/>) to the actual
|
||||||
|
// root vnode
|
||||||
|
if (__owl__.classObj) {
|
||||||
|
const data = vnode.data!;
|
||||||
|
data.class = Object.assign(data.class || {}, __owl__.classObj);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
}
|
}
|
||||||
patch.push(vnode);
|
|
||||||
if (__owl__.observer) {
|
if (__owl__.observer) {
|
||||||
__owl__.observer.allowMutations = true;
|
__owl__.observer.allowMutations = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// this part is critical for the patching process to be done correctly. The
|
fiber.root.counter--;
|
||||||
// tricky part is that a child component can be rerendered on its own, which
|
fiber.isRendered = true;
|
||||||
// will update its own vnode representation without the knowledge of the
|
if (error) {
|
||||||
// parent component. With this, we make sure that the parent component will be
|
fiber.handleError(error);
|
||||||
// able to patch itself properly after
|
|
||||||
vnode.key = __owl__.id;
|
|
||||||
|
|
||||||
// we applly here the class information described on the component by the
|
|
||||||
// template (so, something like <MyComponent class="..."/>) to the actual
|
|
||||||
// root vnode
|
|
||||||
if (__owl__.classObj) {
|
|
||||||
vnode.data.class = Object.assign(vnode.data.class || {}, __owl__.classObj);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.all(promises).then(() => vnode);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Only called by qweb t-component directive
|
|
||||||
*/
|
|
||||||
__mount(vnode: VNode, elm: HTMLElement): VNode {
|
|
||||||
const __owl__ = this.__owl__;
|
|
||||||
if (__owl__.classObj) {
|
|
||||||
(<any>vnode).data.class = Object.assign((<any>vnode).data.class || {}, __owl__.classObj);
|
|
||||||
}
|
|
||||||
__owl__.vnode = patch(elm, vnode);
|
|
||||||
if (__owl__.parent!.__owl__.isMounted && !__owl__.isMounted) {
|
|
||||||
this.__callMounted();
|
|
||||||
}
|
|
||||||
return __owl__.vnode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -625,95 +651,16 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Enable the observe feature on the state. We only create an observer if
|
|
||||||
* there is some state to be observed.
|
|
||||||
*/
|
|
||||||
__observeState() {
|
|
||||||
if (this.state) {
|
|
||||||
const __owl__ = this.__owl__;
|
|
||||||
__owl__.observer = new Observer();
|
|
||||||
this.state = __owl__.observer.observe(this.state);
|
|
||||||
__owl__.observer.notifyCB = this.render.bind(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply default props (only top level).
|
* Apply default props (only top level).
|
||||||
*
|
*
|
||||||
* Note that this method does not modify in place the props, it returns a new
|
* Note that this method does modify in place the props
|
||||||
* prop object
|
|
||||||
*/
|
*/
|
||||||
__applyDefaultProps(props: Object | undefined, defaultProps: Object): Props {
|
__applyDefaultProps(props: Object, defaultProps: Object) {
|
||||||
props = props ? Object.assign({}, props) : {};
|
|
||||||
for (let propName in defaultProps) {
|
for (let propName in defaultProps) {
|
||||||
if (props![propName] === undefined) {
|
if (props![propName] === undefined) {
|
||||||
props![propName] = defaultProps[propName];
|
props![propName] = defaultProps[propName];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return <Props>props;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply the given patch queue. A patch is a pair [c, vn], where c is a
|
|
||||||
* Component instance and vn a VNode.
|
|
||||||
* 1) Call 'willPatch' on the component of each patch
|
|
||||||
* 2) Call '__patch' on the component of each patch
|
|
||||||
* 3) Call 'patched' on the component of each patch, in inverse order
|
|
||||||
*/
|
|
||||||
__applyPatchQueue(patchQueue: any[]) {
|
|
||||||
let component = this;
|
|
||||||
try {
|
|
||||||
const patchLen = patchQueue.length;
|
|
||||||
for (let i = 0; i < patchLen; i++) {
|
|
||||||
const patch = patchQueue[i];
|
|
||||||
component = patch[0];
|
|
||||||
patch.push(patch[0].willPatch());
|
|
||||||
}
|
|
||||||
for (let i = 0; i < patchLen; i++) {
|
|
||||||
const patch = patchQueue[i];
|
|
||||||
patch[0].__patch(patch[1]);
|
|
||||||
}
|
|
||||||
for (let i = patchLen - 1; i >= 0; i--) {
|
|
||||||
const patch = patchQueue[i];
|
|
||||||
component = patch[0];
|
|
||||||
patch[0].patched(patch[2]);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
errorHandler(e, component);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Error handling
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is the global error handler for errors occurring in Owl main lifecycle
|
|
||||||
* methods. Caught errors are triggered on the QWeb instance, and are
|
|
||||||
* potentially given to some parent component which implements `catchError`.
|
|
||||||
*
|
|
||||||
* If there are no such component, we destroy everything. This is better than
|
|
||||||
* being in a corrupted state.
|
|
||||||
*/
|
|
||||||
function errorHandler(error: Error, component: Component<any, any, any>) {
|
|
||||||
let canCatch = false;
|
|
||||||
let qweb = component.env.qweb;
|
|
||||||
let root = component;
|
|
||||||
while (component && !(canCatch = component.catchError !== Component.prototype.catchError)) {
|
|
||||||
root = component;
|
|
||||||
component = component.__owl__.parent!;
|
|
||||||
}
|
|
||||||
console.error(error);
|
|
||||||
// we trigger error on QWeb so it can be logged/handled
|
|
||||||
qweb.trigger("error", error);
|
|
||||||
|
|
||||||
if (canCatch) {
|
|
||||||
setTimeout(() => {
|
|
||||||
component.catchError(error);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
root.destroy();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+94
-134
@@ -1,4 +1,5 @@
|
|||||||
import { QWeb } from "../qweb/index";
|
import { QWeb } from "../qweb/index";
|
||||||
|
import { INTERP_REGEXP } from "../qweb/compilation_context";
|
||||||
import { MODS_CODE } from "../qweb/extensions";
|
import { MODS_CODE } from "../qweb/extensions";
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
@@ -185,7 +186,7 @@ QWeb.utils.defineProxy = function defineProxy(target, source) {
|
|||||||
|
|
||||||
QWeb.addDirective({
|
QWeb.addDirective({
|
||||||
name: "component",
|
name: "component",
|
||||||
extraNames: ["props", "keepalive", "asyncroot"],
|
extraNames: ["props"],
|
||||||
priority: 100,
|
priority: 100,
|
||||||
atNodeEncounter({ ctx, value, node, qweb }): boolean {
|
atNodeEncounter({ ctx, value, node, qweb }): boolean {
|
||||||
ctx.addLine("//COMPONENT");
|
ctx.addLine("//COMPONENT");
|
||||||
@@ -193,9 +194,7 @@ QWeb.addDirective({
|
|||||||
ctx.rootContext.shouldDefineQWeb = true;
|
ctx.rootContext.shouldDefineQWeb = true;
|
||||||
ctx.rootContext.shouldDefineParent = true;
|
ctx.rootContext.shouldDefineParent = true;
|
||||||
ctx.rootContext.shouldDefineUtils = true;
|
ctx.rootContext.shouldDefineUtils = true;
|
||||||
let keepAlive = node.getAttribute("t-keepalive") ? true : false;
|
|
||||||
let hasDynamicProps = node.getAttribute("t-props") ? true : false;
|
let hasDynamicProps = node.getAttribute("t-props") ? true : false;
|
||||||
let async = node.getAttribute("t-asyncroot") ? true : false;
|
|
||||||
|
|
||||||
// t-on- events and t-transition
|
// t-on- events and t-transition
|
||||||
const events: [string, string[], string, string][] = [];
|
const events: [string, string[], string, string][] = [];
|
||||||
@@ -208,11 +207,11 @@ QWeb.addDirective({
|
|||||||
if (name.startsWith("t-on-")) {
|
if (name.startsWith("t-on-")) {
|
||||||
const [eventName, ...mods] = name.slice(5).split(".");
|
const [eventName, ...mods] = name.slice(5).split(".");
|
||||||
let extraArgs;
|
let extraArgs;
|
||||||
let handlerName = value.replace(/\(.*\)/, function(args) {
|
let handlerValue = value.replace(/\(.*\)/, function(args) {
|
||||||
extraArgs = args.slice(1, -1);
|
extraArgs = args.slice(1, -1);
|
||||||
return "";
|
return "";
|
||||||
});
|
});
|
||||||
events.push([eventName, mods, handlerName, extraArgs]);
|
events.push([eventName, mods, handlerValue, extraArgs]);
|
||||||
} else if (name === "t-transition") {
|
} else if (name === "t-transition") {
|
||||||
transition = value;
|
transition = value;
|
||||||
} else if (!name.startsWith("t-")) {
|
} else if (!name.startsWith("t-")) {
|
||||||
@@ -223,61 +222,31 @@ QWeb.addDirective({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let key = node.getAttribute("t-key");
|
|
||||||
if (key) {
|
|
||||||
key = ctx.formatExpression(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
// computing the props string representing the props object
|
// computing the props string representing the props object
|
||||||
let propStr = Object.keys(props)
|
let propStr = Object.keys(props)
|
||||||
.map(k => k + ":" + props[k])
|
.map(k => k + ":" + props[k])
|
||||||
.join(",");
|
.join(",");
|
||||||
let dummyID = ctx.generateID();
|
|
||||||
let defID = ctx.generateID();
|
|
||||||
let componentID = ctx.generateID();
|
let componentID = ctx.generateID();
|
||||||
let keyID = key && ctx.generateID();
|
|
||||||
if (key) {
|
|
||||||
// we bind a variable to the key (could be a complex expression, so we
|
|
||||||
// want to evaluate it only once)
|
|
||||||
ctx.addLine(`let key${keyID} = 'key' + ${key};`);
|
|
||||||
}
|
|
||||||
ctx.addLine(`let def${defID};`);
|
|
||||||
let templateID = key
|
|
||||||
? `key${keyID}`
|
|
||||||
: ctx.inLoop
|
|
||||||
? ctx.currentKey
|
|
||||||
? `String(${ctx.currentKey} + '_k_' + i + '_c_' + ${componentID} )`
|
|
||||||
: `String(-${componentID} - i)`
|
|
||||||
: String(componentID);
|
|
||||||
if (ctx.allowMultipleRoots) {
|
|
||||||
templateID = `"_slot_${templateID}"`;
|
|
||||||
}
|
|
||||||
if (key || ctx.inLoop) {
|
|
||||||
let id = ctx.generateID();
|
|
||||||
ctx.addLine(`let templateId${id} = ${templateID};`);
|
|
||||||
templateID = `templateId${id}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const templateKey = ctx.generateTemplateKey();
|
||||||
let ref = node.getAttribute("t-ref");
|
let ref = node.getAttribute("t-ref");
|
||||||
let refExpr = "";
|
let refExpr = "";
|
||||||
let refKey: string = "";
|
let refKey: string = "";
|
||||||
if (ref) {
|
if (ref) {
|
||||||
|
ctx.rootContext.shouldDefineRefs = true;
|
||||||
refKey = `ref${ctx.generateID()}`;
|
refKey = `ref${ctx.generateID()}`;
|
||||||
ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`);
|
ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`);
|
||||||
refExpr = `context.refs[${refKey}] = w${componentID};`;
|
refExpr = `context.__owl__.refs[${refKey}] = w${componentID};`;
|
||||||
}
|
}
|
||||||
let transitionsInsertCode = "";
|
let finalizeComponentCode = `w${componentID}.destroy();`;
|
||||||
if (transition) {
|
if (ref) {
|
||||||
transitionsInsertCode = `utils.transitionInsert(vn, '${transition}');`;
|
finalizeComponentCode += `delete context.__owl__.refs[${refKey}];`;
|
||||||
}
|
|
||||||
let finalizeComponentCode = `w${componentID}.${keepAlive ? "unmount" : "destroy"}();`;
|
|
||||||
if (ref && !keepAlive) {
|
|
||||||
finalizeComponentCode += `delete context.refs[${refKey}];`;
|
|
||||||
}
|
}
|
||||||
if (transition) {
|
if (transition) {
|
||||||
finalizeComponentCode = `let finalize = () => {
|
finalizeComponentCode = `let finalize = () => {
|
||||||
${finalizeComponentCode}
|
${finalizeComponentCode}
|
||||||
};
|
};
|
||||||
|
delete w${componentID}.__owl__.transitionInserted;
|
||||||
utils.transitionRemove(vn, '${transition}', finalize);`;
|
utils.transitionRemove(vn, '${transition}', finalize);`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,10 +284,10 @@ QWeb.addDirective({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let eventsCode = events
|
let eventsCode = events
|
||||||
.map(function([eventName, mods, handlerName, extraArgs]) {
|
.map(function([eventName, mods, handlerValue, extraArgs]) {
|
||||||
let params = "owner";
|
let params = "owner";
|
||||||
if (extraArgs) {
|
if (extraArgs) {
|
||||||
if (ctx.inLoop) {
|
if (ctx.loopNumber) {
|
||||||
let argId = ctx.generateID();
|
let argId = ctx.generateID();
|
||||||
// we need to evaluate the arguments now, because the handler will
|
// we need to evaluate the arguments now, because the handler will
|
||||||
// be set asynchronously later when the widget is ready, and the
|
// be set asynchronously later when the widget is ready, and the
|
||||||
@@ -329,18 +298,17 @@ QWeb.addDirective({
|
|||||||
params = `owner, ${ctx.formatExpression(extraArgs)}`;
|
params = `owner, ${ctx.formatExpression(extraArgs)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let handler;
|
let handler = `function (e) {`;
|
||||||
if (mods.length > 0) {
|
|
||||||
handler = `function (e) {`;
|
|
||||||
handler += mods
|
handler += mods
|
||||||
.map(function(mod) {
|
.map(function(mod) {
|
||||||
return T_COMPONENT_MODS_CODE[mod];
|
return T_COMPONENT_MODS_CODE[mod];
|
||||||
})
|
})
|
||||||
.join("");
|
.join("");
|
||||||
handler += `owner['${handlerName}'].call(${params}, e);}`;
|
if (handlerValue) {
|
||||||
} else {
|
handler += `const fn = owner['${handlerValue}'];`;
|
||||||
handler = `owner['${handlerName}'].bind(${params})`;
|
handler += `if (fn) { fn.call(${params}, e); } else { owner.${handlerValue}; }`;
|
||||||
}
|
}
|
||||||
|
handler += `}`;
|
||||||
return `vn.elm.addEventListener('${eventName}', ${handler});`;
|
return `vn.elm.addEventListener('${eventName}', ${handler});`;
|
||||||
})
|
})
|
||||||
.join("");
|
.join("");
|
||||||
@@ -350,21 +318,10 @@ QWeb.addDirective({
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx.addLine(
|
ctx.addLine(
|
||||||
`let w${componentID} = ${templateID} in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[${templateID}]] : false;`
|
`let w${componentID} = ${templateKey} in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[${templateKey}]] : false;`
|
||||||
);
|
);
|
||||||
if (ctx.parentNode) {
|
let shouldProxy = !ctx.parentNode;
|
||||||
ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`);
|
if (shouldProxy) {
|
||||||
}
|
|
||||||
let shouldProxy = false;
|
|
||||||
if (async) {
|
|
||||||
ctx.addLine(`const patchQueue${componentID} = [];`);
|
|
||||||
ctx.addLine(
|
|
||||||
`c${ctx.parentNode}.push(w${componentID} && w${componentID}.__owl__.pvnode || null);`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
if (ctx.parentNode) {
|
|
||||||
ctx.addLine(`c${ctx.parentNode}.push(null);`);
|
|
||||||
} else {
|
|
||||||
let id = ctx.generateID();
|
let id = ctx.generateID();
|
||||||
ctx.rootContext.rootNode = id;
|
ctx.rootContext.rootNode = id;
|
||||||
shouldProxy = true;
|
shouldProxy = true;
|
||||||
@@ -372,7 +329,6 @@ QWeb.addDirective({
|
|||||||
ctx.addLine(`let vn${id} = {};`);
|
ctx.addLine(`let vn${id} = {};`);
|
||||||
ctx.addLine(`result = vn${id};`);
|
ctx.addLine(`result = vn${id};`);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (hasDynamicProps) {
|
if (hasDynamicProps) {
|
||||||
const dynamicProp = ctx.formatExpression(node.getAttribute("t-props")!);
|
const dynamicProp = ctx.formatExpression(node.getAttribute("t-props")!);
|
||||||
ctx.addLine(`let props${componentID} = Object.assign({${propStr}}, ${dynamicProp});`);
|
ctx.addLine(`let props${componentID} = Object.assign({${propStr}}, ${dynamicProp});`);
|
||||||
@@ -380,31 +336,16 @@ QWeb.addDirective({
|
|||||||
ctx.addLine(`let props${componentID} = {${propStr}};`);
|
ctx.addLine(`let props${componentID} = {${propStr}};`);
|
||||||
}
|
}
|
||||||
ctx.addIf(
|
ctx.addIf(
|
||||||
`w${componentID} && w${componentID}.__owl__.renderPromise && !w${componentID}.__owl__.vnode`
|
`w${componentID} && w${componentID}.__owl__.currentFiber && !w${componentID}.__owl__.vnode`
|
||||||
);
|
);
|
||||||
ctx.addIf(`utils.shallowEqual(props${componentID}, w${componentID}.__owl__.renderProps)`);
|
|
||||||
ctx.addLine(`def${defID} = w${componentID}.__owl__.renderPromise;`);
|
|
||||||
ctx.addElse();
|
|
||||||
ctx.addLine(`w${componentID}.destroy();`);
|
ctx.addLine(`w${componentID}.destroy();`);
|
||||||
ctx.addLine(`w${componentID} = false;`);
|
ctx.addLine(`w${componentID} = false;`);
|
||||||
ctx.closeIf();
|
ctx.closeIf();
|
||||||
ctx.closeIf();
|
|
||||||
|
|
||||||
ctx.addIf(`!w${componentID}`);
|
let registerCode = "";
|
||||||
// new component
|
if (shouldProxy) {
|
||||||
ctx.addLine(`let componentKey${componentID} = ${ctx.interpolate(value)};`);
|
registerCode = `utils.defineProxy(vn${ctx.rootNode}, pvnode);`;
|
||||||
ctx.addLine(
|
|
||||||
`let W${componentID} = context.constructor.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}];`
|
|
||||||
);
|
|
||||||
// maybe only do this in dev mode...
|
|
||||||
ctx.addLine(
|
|
||||||
`if (!W${componentID}) {throw new Error('Cannot find the definition of component "' + componentKey${componentID} + '"')}`
|
|
||||||
);
|
|
||||||
if (QWeb.dev) {
|
|
||||||
ctx.addLine(`utils.validateProps(W${componentID}, props${componentID})`);
|
|
||||||
}
|
}
|
||||||
ctx.addLine(`w${componentID} = new W${componentID}(parent, props${componentID});`);
|
|
||||||
ctx.addLine(`parent.__owl__.cmap[${templateID}] = w${componentID}.__owl__.id;`);
|
|
||||||
|
|
||||||
// SLOTS
|
// SLOTS
|
||||||
const varDefs: string[] = [];
|
const varDefs: string[] = [];
|
||||||
@@ -416,7 +357,63 @@ QWeb.addDirective({
|
|||||||
varDefs.push(v["id"]);
|
varDefs.push(v["id"]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let scopeVars;
|
||||||
|
if (hasSlots) {
|
||||||
|
let scope = ctx.scopeVars.length ? `Object.assign({}, scope)` : `{}`;
|
||||||
|
let vars = varDefs.length ? `{${varDefs.join(",")}}` : "undefined";
|
||||||
|
scopeVars = `${scope}, ${vars}`;
|
||||||
|
} else {
|
||||||
|
scopeVars = "undefined, undefined";
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.addIf(`w${componentID}`);
|
||||||
|
|
||||||
|
// need to update component
|
||||||
|
let styleCode = "";
|
||||||
|
if (tattStyle) {
|
||||||
|
styleCode = `.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};w${componentID}.el.style=${tattStyle};});`;
|
||||||
|
}
|
||||||
|
ctx.addLine(
|
||||||
|
`w${componentID}.__updateProps(props${componentID}, extra.fiber${scopeVars &&
|
||||||
|
", " + scopeVars})${styleCode};`
|
||||||
|
);
|
||||||
|
ctx.addLine(`let pvnode = w${componentID}.__owl__.pvnode;`);
|
||||||
|
if (registerCode) {
|
||||||
|
ctx.addLine(registerCode);
|
||||||
|
}
|
||||||
|
if (ctx.parentNode) {
|
||||||
|
ctx.addLine(`c${ctx.parentNode}.push(pvnode);`);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.addElse();
|
||||||
|
|
||||||
|
// new component
|
||||||
|
let dynamicFallback = "";
|
||||||
|
if (!value.match(INTERP_REGEXP)) {
|
||||||
|
dynamicFallback = `|| ${ctx.formatExpression(value)}`;
|
||||||
|
}
|
||||||
|
const interpValue = ctx.interpolate(value);
|
||||||
|
ctx.addLine(`let componentKey${componentID} = ${interpValue};`);
|
||||||
|
ctx.addLine(
|
||||||
|
`let W${componentID} = context.constructor.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}]${dynamicFallback};`
|
||||||
|
);
|
||||||
|
|
||||||
|
// maybe only do this in dev mode...
|
||||||
|
ctx.addLine(
|
||||||
|
`if (!W${componentID}) {throw new Error('Cannot find the definition of component "' + componentKey${componentID} + '"')}`
|
||||||
|
);
|
||||||
|
ctx.addLine(`w${componentID} = new W${componentID}(parent, props${componentID});`);
|
||||||
|
if (transition) {
|
||||||
|
ctx.addLine(`const __patch${componentID} = w${componentID}.__patch;`);
|
||||||
|
ctx.addLine(
|
||||||
|
`w${componentID}.__patch = fiber => {__patch${componentID}.call(w${componentID}, fiber); if(!w${componentID}.__owl__.transitionInserted){w${componentID}.__owl__.transitionInserted = true;utils.transitionInsert(w${componentID}.__owl__.vnode, '${transition}');}};`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ctx.addLine(`parent.__owl__.cmap[${templateKey}] = w${componentID}.__owl__.id;`);
|
||||||
|
|
||||||
|
if (hasSlots) {
|
||||||
const clone = <Element>node.cloneNode(true);
|
const clone = <Element>node.cloneNode(true);
|
||||||
const slotNodes = clone.querySelectorAll("[t-set]");
|
const slotNodes = clone.querySelectorAll("[t-set]");
|
||||||
const slotId = QWeb.nextSlotId++;
|
const slotId = QWeb.nextSlotId++;
|
||||||
@@ -441,66 +438,29 @@ QWeb.addDirective({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let scopeVars = "";
|
ctx.addLine(
|
||||||
if (hasSlots) {
|
`let fiber = w${componentID}.__prepare(extra.fiber, ${scopeVars}, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});`
|
||||||
scopeVars += ctx.scopeVars.length ? `Object.assign({}, scope)` : varDefs.length ? `{}` : "";
|
);
|
||||||
if (varDefs.length) {
|
|
||||||
scopeVars += `, {${varDefs.join(",")}}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx.addLine(`def${defID} = w${componentID}.__prepare(${scopeVars});`);
|
|
||||||
// hack: specify empty remove hook to prevent the node from being removed from the DOM
|
// hack: specify empty remove hook to prevent the node from being removed from the DOM
|
||||||
let registerCode = `c${ctx.parentNode}[_${dummyID}_index]=pvnode;`;
|
const insertHook = refExpr ? `insert(vn) {${refExpr}},` : "";
|
||||||
if (shouldProxy) {
|
|
||||||
registerCode = `utils.defineProxy(vn${ctx.rootNode}, pvnode);`;
|
|
||||||
}
|
|
||||||
ctx.addLine(
|
ctx.addLine(
|
||||||
`def${defID} = def${defID}.then(vnode=>{if (w${componentID}.__owl__.isDestroyed){return}${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${componentID}.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeComponentCode}}}});${registerCode}w${componentID}.__owl__.pvnode = pvnode;});`
|
`let pvnode = h('dummy', {key: ${templateKey}, hook: {${insertHook}remove() {},destroy(vn) {${finalizeComponentCode}}}});`
|
||||||
);
|
);
|
||||||
|
if (registerCode) {
|
||||||
|
ctx.addLine(registerCode);
|
||||||
|
}
|
||||||
|
if (ctx.parentNode) {
|
||||||
|
ctx.addLine(`c${ctx.parentNode}.push(pvnode);`);
|
||||||
|
}
|
||||||
|
ctx.addLine(`w${componentID}.__owl__.pvnode = pvnode;`);
|
||||||
|
|
||||||
ctx.addElse();
|
|
||||||
// need to update component
|
|
||||||
let patchQueueCode = async ? `patchQueue${componentID}` : "extra.patchQueue";
|
|
||||||
if (keepAlive) {
|
|
||||||
// if we have t-keepalive="1", the component could be unmounted, but then
|
|
||||||
// we __updateProps is called. This is ok, but we do not want to call
|
|
||||||
// the willPatch/patched hooks of the component in this case, so we
|
|
||||||
// disable the patch queue
|
|
||||||
patchQueueCode = `w${componentID}.__owl__.isMounted ? ${patchQueueCode} : []`;
|
|
||||||
}
|
|
||||||
if (QWeb.dev) {
|
|
||||||
ctx.addLine(`utils.validateProps(w${componentID}.constructor, props${componentID})`);
|
|
||||||
}
|
|
||||||
ctx.addLine(
|
|
||||||
`def${defID} = def${defID} || w${componentID}.__updateProps(props${componentID}, extra.forceUpdate, ${patchQueueCode}${scopeVars &&
|
|
||||||
", " + scopeVars});`
|
|
||||||
);
|
|
||||||
let keepAliveCode = "";
|
|
||||||
if (keepAlive) {
|
|
||||||
keepAliveCode = `pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w${componentID}.el,vn.elm);vn.elm=w${componentID}.el;w${componentID}.__remount();};`;
|
|
||||||
}
|
|
||||||
ctx.addLine(
|
|
||||||
`def${defID} = def${defID}.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};${
|
|
||||||
tattStyle ? `w${componentID}.el.style=${tattStyle};` : ""
|
|
||||||
}let pvnode=w${componentID}.__owl__.pvnode;${keepAliveCode}${registerCode}});`
|
|
||||||
);
|
|
||||||
ctx.closeIf();
|
ctx.closeIf();
|
||||||
|
|
||||||
if (classObj) {
|
if (classObj) {
|
||||||
ctx.addLine(`w${componentID}.__owl__.classObj=${classObj};`);
|
ctx.addLine(`w${componentID}.__owl__.classObj=${classObj};`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (async) {
|
ctx.addLine(`w${componentID}.__owl__.parentLastFiberId = extra.fiber.id;`);
|
||||||
ctx.addLine(
|
|
||||||
`def${defID}.then(w${componentID}.__applyPatchQueue.bind(w${componentID}, patchQueue${componentID}));`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
ctx.addLine(`extra.promises.push(def${defID});`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.hasAttribute("t-if") || node.hasAttribute("t-else") || node.hasAttribute("t-elif")) {
|
|
||||||
ctx.closeIf();
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,288 @@
|
|||||||
|
import { h, VNode } from "../vdom/index";
|
||||||
|
import { Component } from "./component";
|
||||||
|
import { scheduler } from "./scheduler";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Owl Fiber Class
|
||||||
|
*
|
||||||
|
* Fibers are small abstractions designed to contain all the internal state
|
||||||
|
* associated with a "rendering work unit", relative to a specific component.
|
||||||
|
*
|
||||||
|
* A rendering will cause the creation of a fiber for each impacted components.
|
||||||
|
*
|
||||||
|
* Fibers capture all that necessary information, which is critical to owl
|
||||||
|
* asynchronous rendering pipeline. Fibers can be cancelled, can be in different
|
||||||
|
* states and in general determine the state of the rendering.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class Fiber {
|
||||||
|
static nextId: number = 1;
|
||||||
|
id: number = Fiber.nextId++;
|
||||||
|
|
||||||
|
// The force attribute determines if a rendering should bypass the `shouldUpdate`
|
||||||
|
// method potentially implemented by a component. It is usually set to false.
|
||||||
|
force: boolean;
|
||||||
|
|
||||||
|
// isCompleted means that the rendering corresponding to this fiber's work is
|
||||||
|
// done, either because the component has been mounted or patched, or because
|
||||||
|
// fiber has been cancelled.
|
||||||
|
isCompleted: boolean = false;
|
||||||
|
|
||||||
|
// the fibers corresponding to component updates (updateProps) need to call
|
||||||
|
// the willPatch and patched hooks from the corresponding component. However,
|
||||||
|
// fibers corresponding to a new component do not need to do that. So, the
|
||||||
|
// shouldPatch hook is the boolean that we check whenever we need to apply
|
||||||
|
// a patch.
|
||||||
|
shouldPatch: boolean = true;
|
||||||
|
|
||||||
|
// isRendered is the last state of a fiber. If true, this means that it has
|
||||||
|
// been rendered and is inert (so, it should not be taken into account when
|
||||||
|
// counting the number of active fibers).
|
||||||
|
isRendered: boolean = false;
|
||||||
|
|
||||||
|
// the counter number is a critical information. It is only necessary for a
|
||||||
|
// root fiber. For that fiber, this number counts the number of active sub
|
||||||
|
// fibers. When that number reaches 0, the fiber can be applied by the
|
||||||
|
// scheduler.
|
||||||
|
counter: number = 0;
|
||||||
|
|
||||||
|
target: HTMLElement | null;
|
||||||
|
|
||||||
|
scope: any;
|
||||||
|
vars: any;
|
||||||
|
|
||||||
|
component: Component<any, any>;
|
||||||
|
vnode: VNode | null = null;
|
||||||
|
|
||||||
|
root: Fiber;
|
||||||
|
child: Fiber | null = null;
|
||||||
|
sibling: Fiber | null = null;
|
||||||
|
lastChild: Fiber | null = null;
|
||||||
|
parent: Fiber | null = null;
|
||||||
|
|
||||||
|
error?: Error;
|
||||||
|
|
||||||
|
constructor(parent: Fiber | null, component: Component<any, any>, force, target) {
|
||||||
|
this.component = component;
|
||||||
|
this.force = force;
|
||||||
|
this.target = target;
|
||||||
|
|
||||||
|
const __owl__ = component.__owl__;
|
||||||
|
this.scope = __owl__.scope;
|
||||||
|
this.vars = __owl__.vars;
|
||||||
|
|
||||||
|
this.root = parent ? parent.root : this;
|
||||||
|
this.parent = parent;
|
||||||
|
|
||||||
|
let oldFiber = __owl__.currentFiber;
|
||||||
|
if (oldFiber && !oldFiber.isCompleted) {
|
||||||
|
if (oldFiber.root === oldFiber && !parent) {
|
||||||
|
// both oldFiber and this fiber are root fibers
|
||||||
|
this._reuseFiber(oldFiber);
|
||||||
|
return oldFiber;
|
||||||
|
} else {
|
||||||
|
this._remapFiber(oldFiber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.root.counter++;
|
||||||
|
|
||||||
|
__owl__.currentFiber = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the oldFiber is not completed yet, and both oldFiber and this fiber
|
||||||
|
* are root fibers, we want to reuse the oldFiber instead of creating a new
|
||||||
|
* one. Doing so will guarantee that the initiator(s) of those renderings will
|
||||||
|
* be notified (the promise will resolve) when the last rendering will be done.
|
||||||
|
*
|
||||||
|
* This function thus assumes that oldFiber is a root fiber.
|
||||||
|
*/
|
||||||
|
_reuseFiber(oldFiber: Fiber) {
|
||||||
|
oldFiber.cancel(); // cancel children fibers
|
||||||
|
oldFiber.isCompleted = false; // keep the root fiber alive
|
||||||
|
oldFiber.isRendered = false; // the fiber has to be re-rendered
|
||||||
|
if (oldFiber.child) {
|
||||||
|
// remove relation to children
|
||||||
|
oldFiber.child.parent = null;
|
||||||
|
oldFiber.child = null;
|
||||||
|
oldFiber.lastChild = null;
|
||||||
|
}
|
||||||
|
oldFiber.counter = 1; // re-initialize counter
|
||||||
|
oldFiber.id = Fiber.nextId++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In some cases, a rendering initiated at some component can detect that it
|
||||||
|
* should be part of a larger rendering initiated somewhere up the component
|
||||||
|
* tree. In that case, it needs to cancel the previous rendering and
|
||||||
|
* remap itself as a part of the current parent rendering.
|
||||||
|
*/
|
||||||
|
_remapFiber(oldFiber: Fiber) {
|
||||||
|
oldFiber.cancel();
|
||||||
|
this.shouldPatch = oldFiber.shouldPatch;
|
||||||
|
if (oldFiber === oldFiber.root) {
|
||||||
|
oldFiber.counter++;
|
||||||
|
}
|
||||||
|
if (oldFiber.parent && !this.parent) {
|
||||||
|
// re-map links
|
||||||
|
this.parent = oldFiber.parent;
|
||||||
|
this.root = this.parent.root;
|
||||||
|
this.sibling = oldFiber.sibling;
|
||||||
|
if (this.parent.lastChild === oldFiber) {
|
||||||
|
this.parent.lastChild = this;
|
||||||
|
}
|
||||||
|
if (this.parent.child === oldFiber) {
|
||||||
|
this.parent.child = this;
|
||||||
|
} else {
|
||||||
|
let current = this.parent.child!;
|
||||||
|
while (true) {
|
||||||
|
if (current.sibling === oldFiber) {
|
||||||
|
current.sibling = this;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
current = current.sibling!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function has been taken from
|
||||||
|
* https://medium.com/react-in-depth/the-how-and-why-on-reacts-usage-of-linked-list-in-fiber-67f1014d0eb7
|
||||||
|
*/
|
||||||
|
_walk(doWork: (f: Fiber) => Fiber | null) {
|
||||||
|
let root = this;
|
||||||
|
let current: Fiber = this;
|
||||||
|
while (true) {
|
||||||
|
const child = doWork(current);
|
||||||
|
if (child) {
|
||||||
|
current = child;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (current === root) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
while (!current.sibling) {
|
||||||
|
if (!current.parent || current.parent === root) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
current = current.parent;
|
||||||
|
}
|
||||||
|
current = current.sibling;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Successfully complete the work of the fiber: call the mount or patch hooks
|
||||||
|
* and patch the DOM. This function is called once the fiber and its children
|
||||||
|
* are ready, and the scheduler decides to process it.
|
||||||
|
*/
|
||||||
|
complete() {
|
||||||
|
let component = this.component;
|
||||||
|
this.isCompleted = true;
|
||||||
|
if (!this.target && !component.__owl__.isMounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// build patchQueue
|
||||||
|
const patchQueue: Fiber[] = [];
|
||||||
|
const doWork: (Fiber) => Fiber | null = function(f) {
|
||||||
|
patchQueue.push(f);
|
||||||
|
return f.child;
|
||||||
|
};
|
||||||
|
this._walk(doWork);
|
||||||
|
const patchLen = patchQueue.length;
|
||||||
|
|
||||||
|
// call willPatch hook on each fiber of patchQueue
|
||||||
|
for (let i = 0; i < patchLen; i++) {
|
||||||
|
const fiber = patchQueue[i];
|
||||||
|
if (fiber.shouldPatch) {
|
||||||
|
component = fiber.component;
|
||||||
|
if (component.__owl__.willPatchCB) {
|
||||||
|
component.__owl__.willPatchCB();
|
||||||
|
}
|
||||||
|
component.willPatch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// call __patch on each fiber of (reversed) patchQueue
|
||||||
|
for (let i = patchLen - 1; i >= 0; i--) {
|
||||||
|
const fiber = patchQueue[i];
|
||||||
|
component = fiber.component;
|
||||||
|
component.__patch(fiber.vnode!);
|
||||||
|
if (!fiber.shouldPatch && (!fiber.target || i !== 0)) {
|
||||||
|
component.__owl__.pvnode!.elm = component.__owl__.vnode!.elm;
|
||||||
|
}
|
||||||
|
component.__owl__.currentFiber = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert into the DOM (mount case)
|
||||||
|
let inDOM = false;
|
||||||
|
if (this.target) {
|
||||||
|
this.target.appendChild(this.component.el!);
|
||||||
|
inDOM = document.body.contains(this.target);
|
||||||
|
this.component.env.qweb.trigger("dom-appended");
|
||||||
|
}
|
||||||
|
|
||||||
|
// call patched/mounted hook on each fiber of (reversed) patchQueue
|
||||||
|
for (let i = patchLen - 1; i >= 0; i--) {
|
||||||
|
const fiber = patchQueue[i];
|
||||||
|
component = fiber.component;
|
||||||
|
if (fiber.shouldPatch && !this.target) {
|
||||||
|
component.patched();
|
||||||
|
if (component.__owl__.patchedCB) {
|
||||||
|
component.__owl__.patchedCB();
|
||||||
|
}
|
||||||
|
} else if (this.target ? inDOM : true) {
|
||||||
|
component.__callMounted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel a fiber and all its children.
|
||||||
|
*/
|
||||||
|
cancel() {
|
||||||
|
this._walk(f => {
|
||||||
|
if (!f.isRendered) {
|
||||||
|
f.root.counter--;
|
||||||
|
}
|
||||||
|
f.isCompleted = true;
|
||||||
|
return f.child;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the global error handler for errors occurring in Owl main lifecycle
|
||||||
|
* methods. Caught errors are triggered on the QWeb instance, and are
|
||||||
|
* potentially given to some parent component which implements `catchError`.
|
||||||
|
*
|
||||||
|
* If there are no such component, we destroy everything. This is better than
|
||||||
|
* being in a corrupted state.
|
||||||
|
*/
|
||||||
|
handleError(error: Error) {
|
||||||
|
let component = this.component;
|
||||||
|
this.vnode = component.__owl__.vnode || h("div");
|
||||||
|
|
||||||
|
const qweb = component.env.qweb;
|
||||||
|
let root = component;
|
||||||
|
let canCatch = false;
|
||||||
|
while (component && !(canCatch = !!component.catchError)) {
|
||||||
|
root = component;
|
||||||
|
component = component.__owl__.parent!;
|
||||||
|
}
|
||||||
|
qweb.trigger("error", error);
|
||||||
|
|
||||||
|
if (canCatch) {
|
||||||
|
component.catchError!(error);
|
||||||
|
} else {
|
||||||
|
// the 3 next lines aim to mark the root fiber as being in error, and
|
||||||
|
// to force it to end, without waiting for its children
|
||||||
|
this.root.counter = 0;
|
||||||
|
this.root.error = error;
|
||||||
|
scheduler.flush();
|
||||||
|
root.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ QWeb.utils.validateProps = function(Widget, props: Object) {
|
|||||||
// optional prop
|
// optional prop
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (!props[propName]) {
|
if (!(propName in props)) {
|
||||||
throw new Error(`Missing props '${propsDef[i]}' (component '${Widget.name}')`);
|
throw new Error(`Missing props '${propsDef[i]}' (component '${Widget.name}')`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,9 +40,15 @@ QWeb.utils.validateProps = function(Widget, props: Object) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let isValid = isValidProp(props[propName], propsDef[propName]);
|
let isValid;
|
||||||
|
try {
|
||||||
|
isValid = isValidProp(props[propName], propsDef[propName]);
|
||||||
|
} catch (e) {
|
||||||
|
e.message = `Invalid prop '${propName}' in component ${Widget.name} (${e.message})`;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
throw new Error(`Props '${propName}' of invalid type in component '${Widget.name}'`);
|
throw new Error(`Invalid Prop '${propName}' in component '${Widget.name}'`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (let propName in props) {
|
for (let propName in props) {
|
||||||
@@ -80,17 +86,30 @@ function isValidProp(prop, propDef): boolean {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
// propsDef is an object
|
// propsDef is an object
|
||||||
let result = isValidProp(prop, propDef.type);
|
if (propDef.optional && prop === undefined) {
|
||||||
if (propDef.type === Array) {
|
return true;
|
||||||
|
}
|
||||||
|
let result = propDef.type ? isValidProp(prop, propDef.type) : true;
|
||||||
|
if (propDef.validate) {
|
||||||
|
result = result && propDef.validate(prop);
|
||||||
|
}
|
||||||
|
if (propDef.type === Array && propDef.element) {
|
||||||
for (let i = 0, iLen = prop.length; i < iLen; i++) {
|
for (let i = 0, iLen = prop.length; i < iLen; i++) {
|
||||||
result = result && isValidProp(prop[i], propDef.element);
|
result = result && isValidProp(prop[i], propDef.element);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (propDef.type === Object) {
|
if (propDef.type === Object && propDef.shape) {
|
||||||
const shape = propDef.shape;
|
const shape = propDef.shape;
|
||||||
for (let key in shape) {
|
for (let key in shape) {
|
||||||
result = result && isValidProp(prop[key], shape[key]);
|
result = result && isValidProp(prop[key], shape[key]);
|
||||||
}
|
}
|
||||||
|
if (result) {
|
||||||
|
for (let propName in prop) {
|
||||||
|
if (!(propName in shape)) {
|
||||||
|
throw new Error(`unknown prop '${propName}'`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { Fiber } from "./fiber";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Owl Scheduler Class
|
||||||
|
*
|
||||||
|
* The scheduler is the part of Owl that will effectively apply a rendering
|
||||||
|
* whenever a fiber is ready.
|
||||||
|
*
|
||||||
|
* Briefly, it can be used to register root fibers. Whenever there is an
|
||||||
|
* active root fiber, it will poll continuously each animation frame (so, about
|
||||||
|
* once every 16ms) and whenever a root fiber is ready, it will apply it.
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface Task {
|
||||||
|
fiber: Fiber;
|
||||||
|
callback: (err?: Error) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Scheduler {
|
||||||
|
tasks: Task[] = [];
|
||||||
|
isRunning: boolean = false;
|
||||||
|
requestAnimationFrame: typeof window.requestAnimationFrame;
|
||||||
|
|
||||||
|
constructor(requestAnimationFrame) {
|
||||||
|
this.requestAnimationFrame = requestAnimationFrame;
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
this.isRunning = true;
|
||||||
|
this.scheduleTasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this.isRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
addFiber(fiber): Promise<void> {
|
||||||
|
// if the fiber was remapped into a larger rendering fiber, it may not be a
|
||||||
|
// root fiber. But we only want to register root fibers
|
||||||
|
fiber = fiber.root;
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (fiber.error) {
|
||||||
|
return reject(fiber.error);
|
||||||
|
}
|
||||||
|
this.tasks.push({
|
||||||
|
fiber,
|
||||||
|
callback: () => {
|
||||||
|
if (fiber.error) {
|
||||||
|
return reject(fiber.error);
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!this.isRunning) {
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process all current tasks. This only applies to the fibers that are ready.
|
||||||
|
* Other tasks are left unchanged.
|
||||||
|
*/
|
||||||
|
flush() {
|
||||||
|
let tasks = this.tasks;
|
||||||
|
this.tasks = [];
|
||||||
|
tasks = tasks.filter(task => {
|
||||||
|
if (task.fiber.isCompleted) {
|
||||||
|
task.callback();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (task.fiber.counter === 0) {
|
||||||
|
if (!task.fiber.error) {
|
||||||
|
try {
|
||||||
|
task.fiber.complete();
|
||||||
|
} catch (e) {
|
||||||
|
task.fiber.handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
task.callback();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
this.tasks = tasks.concat(this.tasks);
|
||||||
|
if (this.tasks.length === 0) {
|
||||||
|
this.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleTasks() {
|
||||||
|
this.requestAnimationFrame(() => {
|
||||||
|
this.flush();
|
||||||
|
if (this.isRunning) {
|
||||||
|
this.scheduleTasks();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const raf = window.requestAnimationFrame.bind(window);
|
||||||
|
export const scheduler = new Scheduler(raf);
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { QWeb } from "./qweb/index";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file creates and exports the OWL 'config' object, with keys:
|
||||||
|
* - 'mode': 'prod' or 'dev',
|
||||||
|
* - 'env': the environment to use in root components.
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface Config {
|
||||||
|
mode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = {} as Config;
|
||||||
|
|
||||||
|
Object.defineProperty(config, "mode", {
|
||||||
|
get() {
|
||||||
|
return QWeb.dev ? "dev" : "prod";
|
||||||
|
},
|
||||||
|
set(mode: string) {
|
||||||
|
QWeb.dev = mode === "dev";
|
||||||
|
if (QWeb.dev) {
|
||||||
|
const url = `https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode`;
|
||||||
|
console.warn(
|
||||||
|
`Owl is running in 'dev' mode. This is not suitable for production use. See ${url} for more information.`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log(`Owl is now running in 'prod' mode.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
+148
@@ -0,0 +1,148 @@
|
|||||||
|
import { Component } from "./component/component";
|
||||||
|
import { scheduler } from "./component/scheduler";
|
||||||
|
import { EventBus } from "./core/event_bus";
|
||||||
|
import { Observer } from "./core/observer";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
* 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, even though some or most of them will not use the information.
|
||||||
|
*
|
||||||
|
* With a `Context` object, each component can subscribe (with the `useContext`
|
||||||
|
* hook) to its state, and will be updated whenever the context state is updated.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function partitionBy<T>(arr: T[], fn: (t: T) => boolean) {
|
||||||
|
let lastGroup: T[] | false = false;
|
||||||
|
let lastValue;
|
||||||
|
return arr.reduce((acc: T[][], cur) => {
|
||||||
|
let curVal = fn(cur);
|
||||||
|
if (lastGroup) {
|
||||||
|
if (curVal === lastValue) {
|
||||||
|
lastGroup.push(cur);
|
||||||
|
} else {
|
||||||
|
lastGroup = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!lastGroup) {
|
||||||
|
lastGroup = [cur];
|
||||||
|
acc.push(lastGroup);
|
||||||
|
}
|
||||||
|
lastValue = curVal;
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Context extends EventBus {
|
||||||
|
state: any;
|
||||||
|
observer: Observer;
|
||||||
|
rev: number = 1;
|
||||||
|
// mapping from component id to last observed context id
|
||||||
|
mapping: { [componentId: number]: number } = {};
|
||||||
|
|
||||||
|
constructor(state: Object = {}) {
|
||||||
|
super();
|
||||||
|
this.observer = new Observer();
|
||||||
|
this.observer.notifyCB = () => {
|
||||||
|
// notify components in the next microtask tick to ensure that subscribers
|
||||||
|
// are notified only once for all changes that occur in the same micro tick
|
||||||
|
let rev = this.rev;
|
||||||
|
return Promise.resolve().then(() => {
|
||||||
|
if (rev === this.rev) {
|
||||||
|
this.__notifyComponents();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
this.state = this.observer.observe(state);
|
||||||
|
this.subscriptions.update = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instead of using trigger to emit an update event, we actually implement
|
||||||
|
* our own function to do that. The reason is that we need to be smarter than
|
||||||
|
* a simple trigger function: we need to wait for parent components to be
|
||||||
|
* done before doing children components. More precisely, if an update
|
||||||
|
* as an effect of destroying a children, we do not want to call any code
|
||||||
|
* from the child, and certainly not render it.
|
||||||
|
*
|
||||||
|
* This method implements a simple grouping algorithm by depth. If we have
|
||||||
|
* connected components of depths [2, 4,4,4,4, 3,8,8], the Context will notify
|
||||||
|
* them in the following groups: [2], [4,4,4,4], [3], [8,8]. Each group will
|
||||||
|
* be updated sequentially, but each components in a given group will be done in
|
||||||
|
* parallel.
|
||||||
|
*
|
||||||
|
* This is a very simple algorithm, but it avoids checking if a given
|
||||||
|
* component is a child of another.
|
||||||
|
*/
|
||||||
|
async __notifyComponents() {
|
||||||
|
const rev = ++this.rev;
|
||||||
|
const subscriptions = this.subscriptions.update;
|
||||||
|
const groups = partitionBy(subscriptions, s => (s.owner ? s.owner.__owl__.depth : -1));
|
||||||
|
for (let group of groups) {
|
||||||
|
const proms = group.map(sub => sub.callback.call(sub.owner, rev));
|
||||||
|
// at this point, each component in the current group has registered a
|
||||||
|
// top level fiber in the scheduler. It could happen that rendering these
|
||||||
|
// components is done (if they have no children). This is why we manually
|
||||||
|
// flush the scheduler. This will force the scheduler to check
|
||||||
|
// immediately if they are done, which will cause their rendering
|
||||||
|
// promise to resolve earlier, which means that there is a chance of
|
||||||
|
// processing the next group in the same frame.
|
||||||
|
scheduler.flush();
|
||||||
|
await Promise.all(proms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The`useContext` hook is the normal way for a component to register themselve
|
||||||
|
* to context state changes. The `useContext` method returns the context state
|
||||||
|
*/
|
||||||
|
export function useContext(ctx: Context): any {
|
||||||
|
const component: Component<any, any> = Component.current!;
|
||||||
|
return useContextWithCB(ctx, component, component.render.bind(component));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useContextWithCB(ctx: Context, component: Component<any, any>, method): any {
|
||||||
|
const __owl__ = component.__owl__;
|
||||||
|
const id = __owl__.id;
|
||||||
|
const mapping = ctx.mapping;
|
||||||
|
if (id in mapping) {
|
||||||
|
return ctx.state;
|
||||||
|
}
|
||||||
|
if (!__owl__.observer) {
|
||||||
|
__owl__.observer = new Observer();
|
||||||
|
__owl__.observer.notifyCB = component.render.bind(component);
|
||||||
|
}
|
||||||
|
const currentCB = __owl__.observer.notifyCB;
|
||||||
|
__owl__.observer.notifyCB = function() {
|
||||||
|
if (ctx.rev > mapping[id]) {
|
||||||
|
// in this case, the context has been updated since we were rendering
|
||||||
|
// last, and we do not need to render here with the observer. A
|
||||||
|
// rendering is coming anyway, with the correct props.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentCB();
|
||||||
|
};
|
||||||
|
|
||||||
|
mapping[id] = 0;
|
||||||
|
const renderFn = __owl__.renderFn;
|
||||||
|
__owl__.renderFn = function(comp, params) {
|
||||||
|
mapping[id] = ctx.rev;
|
||||||
|
return renderFn(comp, params);
|
||||||
|
};
|
||||||
|
ctx.on("update", component, async contextRev => {
|
||||||
|
if (mapping[id] < contextRev) {
|
||||||
|
mapping[id] = contextRev;
|
||||||
|
await method();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const __destroy = component.__destroy;
|
||||||
|
component.__destroy = parent => {
|
||||||
|
ctx.off("update", component);
|
||||||
|
delete mapping[id];
|
||||||
|
__destroy.call(component, parent);
|
||||||
|
};
|
||||||
|
return ctx.state;
|
||||||
|
}
|
||||||
+4
-18
@@ -24,16 +24,8 @@ export class Observer {
|
|||||||
weakMap: WeakMap<any, any> = new WeakMap();
|
weakMap: WeakMap<any, any> = new WeakMap();
|
||||||
|
|
||||||
notifyCB() {}
|
notifyCB() {}
|
||||||
async notifyChange() {
|
|
||||||
this.dirty = true;
|
|
||||||
await Promise.resolve();
|
|
||||||
if (this.dirty) {
|
|
||||||
this.dirty = false;
|
|
||||||
this.notifyCB();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
observe(value: any, parent?: any): any {
|
observe<T>(value: T, parent?: any): T {
|
||||||
if (value === null || typeof value !== "object" || value instanceof Date) {
|
if (value === null || typeof value !== "object" || value instanceof Date) {
|
||||||
// fun fact: typeof null === 'object'
|
// fun fact: typeof null === 'object'
|
||||||
return value;
|
return value;
|
||||||
@@ -46,10 +38,6 @@ export class Observer {
|
|||||||
const metadata = this.weakMap.get(value);
|
const metadata = this.weakMap.get(value);
|
||||||
return metadata ? metadata.rev : 0;
|
return metadata ? metadata.rev : 0;
|
||||||
}
|
}
|
||||||
deepRevNumber(value): number {
|
|
||||||
const metadata = this.weakMap.get(value);
|
|
||||||
return metadata ? metadata.deepRev : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
_observe(value, parent) {
|
_observe(value, parent) {
|
||||||
var self = this;
|
var self = this;
|
||||||
@@ -69,7 +57,7 @@ export class Observer {
|
|||||||
}
|
}
|
||||||
self._updateRevNumber(target);
|
self._updateRevNumber(target);
|
||||||
target[key] = newVal;
|
target[key] = newVal;
|
||||||
self.notifyChange();
|
self.notifyCB();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
@@ -77,7 +65,7 @@ export class Observer {
|
|||||||
if (key in target) {
|
if (key in target) {
|
||||||
delete target[key];
|
delete target[key];
|
||||||
self._updateRevNumber(target);
|
self._updateRevNumber(target);
|
||||||
self.notifyChange();
|
self.notifyCB();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -87,7 +75,6 @@ export class Observer {
|
|||||||
value,
|
value,
|
||||||
proxy,
|
proxy,
|
||||||
rev: this.rev,
|
rev: this.rev,
|
||||||
deepRev: this.rev,
|
|
||||||
parent
|
parent
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -99,11 +86,10 @@ export class Observer {
|
|||||||
_updateRevNumber(target: any) {
|
_updateRevNumber(target: any) {
|
||||||
this.rev++;
|
this.rev++;
|
||||||
let metadata = this.weakMap.get(target);
|
let metadata = this.weakMap.get(target);
|
||||||
metadata.rev!++;
|
|
||||||
let parent = target;
|
let parent = target;
|
||||||
do {
|
do {
|
||||||
metadata = this.weakMap.get(parent);
|
metadata = this.weakMap.get(parent);
|
||||||
metadata.deepRev++;
|
metadata.rev++;
|
||||||
} while ((parent = metadata.parent) && parent !== target);
|
} while ((parent = metadata.parent) && parent !== target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Component } from "../component/component";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We define here OwlEvent, a subclass of CustomEvent, with an additional
|
||||||
|
* attribute:
|
||||||
|
* - originalComponent: the component that triggered the event
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class OwlEvent<T> extends CustomEvent<T> {
|
||||||
|
originalComponent: Component<any, any>;
|
||||||
|
constructor(component, eventType, options) {
|
||||||
|
super(eventType, options);
|
||||||
|
this.originalComponent = component;
|
||||||
|
}
|
||||||
|
}
|
||||||
+133
@@ -0,0 +1,133 @@
|
|||||||
|
import { Component } from "./component/component";
|
||||||
|
import { Observer } from "./core/observer";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Owl Hook System
|
||||||
|
*
|
||||||
|
* This file introduces the concept of hooks, similar to React or Vue hooks.
|
||||||
|
* We have currently an implementation of:
|
||||||
|
* - useState (reactive state)
|
||||||
|
* - onMounted
|
||||||
|
* - onWillUnmount
|
||||||
|
* - useRef
|
||||||
|
*/
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// useState
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the main way a component can be made reactive. The useState hook
|
||||||
|
* will return an observed object (or array). Changes to that value will then
|
||||||
|
* trigger a rerendering of the current component.
|
||||||
|
*/
|
||||||
|
export function useState<T>(state: T): T {
|
||||||
|
const component: Component<any, any> = Component.current!;
|
||||||
|
const __owl__ = component.__owl__;
|
||||||
|
if (!__owl__.observer) {
|
||||||
|
__owl__.observer = new Observer();
|
||||||
|
__owl__.observer.notifyCB = component.render.bind(component);
|
||||||
|
}
|
||||||
|
return __owl__.observer.observe(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Life cycle hooks
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
function makeLifecycleHook(method: string, reverse: boolean = false) {
|
||||||
|
if (reverse) {
|
||||||
|
return function(cb) {
|
||||||
|
const component: Component<any, any> = Component.current!;
|
||||||
|
if (component.__owl__[method]) {
|
||||||
|
const current = component.__owl__[method];
|
||||||
|
component.__owl__[method] = function() {
|
||||||
|
current.call(component);
|
||||||
|
cb.call(component);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
component.__owl__[method] = cb;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return function(cb) {
|
||||||
|
const component: Component<any, any> = Component.current!;
|
||||||
|
if (component.__owl__[method]) {
|
||||||
|
const current = component.__owl__[method];
|
||||||
|
component.__owl__[method] = function() {
|
||||||
|
cb.call(component);
|
||||||
|
current.call(component);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
component.__owl__[method] = cb;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeAsyncHook(method: string) {
|
||||||
|
return function(cb) {
|
||||||
|
const component: Component<any, any> = Component.current!;
|
||||||
|
if (component.__owl__[method]) {
|
||||||
|
const current = component.__owl__[method];
|
||||||
|
component.__owl__[method] = function(...args) {
|
||||||
|
return Promise.all[(current.call(component, ...args), cb.call(component, ...args))];
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
component.__owl__[method] = cb;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const onMounted = makeLifecycleHook("mountedCB", true);
|
||||||
|
export const onWillUnmount = makeLifecycleHook("willUnmountCB");
|
||||||
|
export const onWillPatch = makeLifecycleHook("willPatchCB");
|
||||||
|
export const onPatched = makeLifecycleHook("patchedCB", true);
|
||||||
|
|
||||||
|
export const onWillStart = makeAsyncHook("willStartCB");
|
||||||
|
export const onWillUpdateProps = makeAsyncHook("willUpdatePropsCB");
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// useRef
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The purpose of this hook is to allow components to get a reference to a sub
|
||||||
|
* html node or component.
|
||||||
|
*/
|
||||||
|
interface Ref {
|
||||||
|
el: HTMLElement | null;
|
||||||
|
comp: Component<any, any> | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRef(name: string): Ref {
|
||||||
|
const __owl__ = Component.current!.__owl__;
|
||||||
|
return {
|
||||||
|
get el(): HTMLElement | null {
|
||||||
|
const val = __owl__.refs && __owl__.refs[name];
|
||||||
|
if (val instanceof HTMLElement) {
|
||||||
|
return val;
|
||||||
|
} else if (val instanceof Component) {
|
||||||
|
return val.el;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
get comp(): Component<any, any> | null {
|
||||||
|
const val = __owl__.refs && __owl__.refs[name];
|
||||||
|
return val instanceof Component ? val : null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// useSubEnv
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This hook is a simple way to let components use a sub environment. Note that
|
||||||
|
* like for all hooks, it is important that this is only called in the
|
||||||
|
* constructor method.
|
||||||
|
*/
|
||||||
|
export function useSubEnv(nextEnv) {
|
||||||
|
const component = Component.current!;
|
||||||
|
component.env = Object.assign(Object.create(component.env), nextEnv);
|
||||||
|
}
|
||||||
+21
-23
@@ -7,36 +7,34 @@
|
|||||||
import { EventBus } from "./core/event_bus";
|
import { EventBus } from "./core/event_bus";
|
||||||
import { Observer } from "./core/observer";
|
import { Observer } from "./core/observer";
|
||||||
import { QWeb } from "./qweb/index";
|
import { QWeb } from "./qweb/index";
|
||||||
import { ConnectedComponent } from "./store/connected_component";
|
import { config } from "./config";
|
||||||
import { Store } from "./store/store";
|
import * as _store from "./store";
|
||||||
import * as _utils from "./utils";
|
import * as _utils from "./utils";
|
||||||
import * as _tags from "./tags";
|
import * as _tags from "./tags";
|
||||||
import { Link } from "./router/Link";
|
import { AsyncRoot } from "./misc/async_root";
|
||||||
import { RouteComponent } from "./router/RouteComponent";
|
import { Portal } from "./misc/portal";
|
||||||
import { Router } from "./router/Router";
|
import * as _hooks from "./hooks";
|
||||||
|
import * as _context from "./context";
|
||||||
|
import { Link } from "./router/link";
|
||||||
|
import { RouteComponent } from "./router/route_component";
|
||||||
|
import { Router } from "./router/router";
|
||||||
|
|
||||||
export { Component } from "./component/component";
|
export { Component } from "./component/component";
|
||||||
export { QWeb };
|
export { QWeb };
|
||||||
|
export { config };
|
||||||
|
|
||||||
|
export const Context = _context.Context;
|
||||||
|
export const useState = _hooks.useState;
|
||||||
export const core = { EventBus, Observer };
|
export const core = { EventBus, Observer };
|
||||||
export const router = { Router, RouteComponent, Link };
|
export const router = { Router, RouteComponent, Link };
|
||||||
export const store = { Store, ConnectedComponent };
|
export const Store = _store.Store;
|
||||||
export const utils = _utils;
|
export const utils = _utils;
|
||||||
export const tags = _tags;
|
export const tags = _tags;
|
||||||
export const __info__ = {};
|
export const misc = { AsyncRoot, Portal };
|
||||||
|
export const hooks = Object.assign({}, _hooks, {
|
||||||
Object.defineProperty(__info__, "mode", {
|
useContext: _context.useContext,
|
||||||
get() {
|
useDispatch: _store.useDispatch,
|
||||||
return QWeb.dev ? "dev" : "prod";
|
useGetters: _store.useGetters,
|
||||||
},
|
useStore: _store.useStore
|
||||||
set(mode: string) {
|
|
||||||
QWeb.dev = mode === "dev";
|
|
||||||
if (QWeb.dev) {
|
|
||||||
const url = `https://github.com/odoo/owl/blob/master/doc/tooling.md#development-mode`;
|
|
||||||
console.warn(
|
|
||||||
`Owl is running in 'dev' mode. This is not suitable for production use. See ${url} for more information.`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.log(`Owl is now running in 'prod' mode.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
export const __info__ = {};
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Component } from "../component/component";
|
||||||
|
import { xml } from "../tags";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AsyncRoot
|
||||||
|
*
|
||||||
|
* Owl is by default asynchronous, and the user interface will wait for all its
|
||||||
|
* subcomponents to be rendered before updating the DOM. This is most of the
|
||||||
|
* time what we want, but in some cases, it makes sense to "detach" a component
|
||||||
|
* from this coordination. This is the goal of the AsyncRoot component.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class AsyncRoot extends Component<any, any> {
|
||||||
|
static template = xml`<t t-slot="default"/>`;
|
||||||
|
|
||||||
|
async __updateProps(nextProps, parentFiber) {
|
||||||
|
this.render(parentFiber.force);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import { Component, portalSymbol } from "../component/component";
|
||||||
|
import { VNode, patch } from "../vdom/index";
|
||||||
|
import { xml } from "../tags";
|
||||||
|
import { OwlEvent } from "../core/owl_event";
|
||||||
|
import { useSubEnv } from "../hooks";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Portal
|
||||||
|
*
|
||||||
|
* The Portal component allows to render a part of a component outside it's DOM.
|
||||||
|
* It is for example useful for dialogs: for css reasons, dialogs are in general
|
||||||
|
* placed in a specific spot of the DOM (e.g. directly in the body). With the
|
||||||
|
* Portal, a component can conditionally specify in its tempate that it contains
|
||||||
|
* a dialog, and where this dialog should be inserted in the DOM.
|
||||||
|
*
|
||||||
|
* The Portal component ensures that the communication between the content of
|
||||||
|
* the Portal and its parent properly works: business events reaching the Portal
|
||||||
|
* are re-triggered on an empty <portal> node located in the parent's DOM.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class Portal extends Component<any, any> {
|
||||||
|
static template = xml`<portal><t t-slot="default"/></portal>`;
|
||||||
|
static props = {
|
||||||
|
target: {
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// boolean to indicate whether or not we must listen to 'dom-appended' event
|
||||||
|
// to hook on the moment when the target is inserted into the DOM (because it
|
||||||
|
// is not when the portal is rendered)
|
||||||
|
doTargetLookUp: boolean = true;
|
||||||
|
// set of encountered events that need to be redirected
|
||||||
|
_handledEvents: Set<string> = new Set();
|
||||||
|
// function that will be the event's tunnel (needs to be an arrow function to
|
||||||
|
// avoid having to rebind `this`)
|
||||||
|
_handlerTunnel: (f: OwlEvent<any>) => void = (ev: OwlEvent<any>) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
this.__trigger(ev.originalComponent, ev.type, ev.detail);
|
||||||
|
};
|
||||||
|
// Storing the parent's env
|
||||||
|
parentEnv: any = null;
|
||||||
|
// represents the element that is moved somewhere else
|
||||||
|
portal: VNode | null = null;
|
||||||
|
// the target where we will move `portal`
|
||||||
|
target: HTMLElement | null = null;
|
||||||
|
|
||||||
|
constructor(parent, props) {
|
||||||
|
super(parent, props);
|
||||||
|
this.parentEnv = parent ? parent.env : {};
|
||||||
|
// put a callback in the env that is propagated to children s.t. portal can
|
||||||
|
// register an handler to those events just before children will trigger them
|
||||||
|
useSubEnv({
|
||||||
|
[portalSymbol]: ev => {
|
||||||
|
if (!this._handledEvents.has(ev.type)) {
|
||||||
|
this.portal!.elm!.addEventListener(ev.type, this._handlerTunnel);
|
||||||
|
this._handledEvents.add(ev.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* At each DOM change, we must ensure that the portal contains exactly one
|
||||||
|
* child
|
||||||
|
*/
|
||||||
|
__checkVNodeStructure(vnode: VNode) {
|
||||||
|
const children = vnode.children!;
|
||||||
|
let countRealNodes = 0;
|
||||||
|
for (let child of children) {
|
||||||
|
if ((child as VNode).sel) {
|
||||||
|
countRealNodes++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (countRealNodes !== 1) {
|
||||||
|
throw new Error(`Portal must have exactly one non-text child (has ${countRealNodes})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Ensure the target is still there at whichever time we render
|
||||||
|
*/
|
||||||
|
__checkTargetPresence() {
|
||||||
|
if (!this.target || !document.contains(this.target)) {
|
||||||
|
throw new Error(`Could not find any match for "${this.props.target}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Move the portal's element to the target
|
||||||
|
*/
|
||||||
|
__deployPortal() {
|
||||||
|
this.__checkTargetPresence();
|
||||||
|
this.target!.appendChild(this.portal!.elm!);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Override to remove from the DOM the element we have teleported
|
||||||
|
*
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
__destroy(parent) {
|
||||||
|
if (this.portal && this.portal.elm) {
|
||||||
|
const displacedElm = this.portal.elm!;
|
||||||
|
const parent = displacedElm.parentNode;
|
||||||
|
if (parent) {
|
||||||
|
parent.removeChild(displacedElm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.__destroy(parent);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Override to patch the element that has been teleported
|
||||||
|
*
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
__patch(vnode) {
|
||||||
|
if (this.doTargetLookUp) {
|
||||||
|
const target = document.querySelector(this.props.target);
|
||||||
|
if (!target) {
|
||||||
|
this.env.qweb.on("dom-appended", this, () => {
|
||||||
|
this.doTargetLookUp = false;
|
||||||
|
this.env.qweb.off("dom-appended", this);
|
||||||
|
this.target = document.querySelector(this.props.target);
|
||||||
|
this.__deployPortal();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.doTargetLookUp = false;
|
||||||
|
this.target = target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.__checkVNodeStructure(vnode);
|
||||||
|
const shouldDeploy = !this.portal && !this.doTargetLookUp;
|
||||||
|
|
||||||
|
if (!this.doTargetLookUp && !shouldDeploy) {
|
||||||
|
// Only on pure patching, provided the
|
||||||
|
// this.target's parent has not been unmounted
|
||||||
|
this.__checkTargetPresence();
|
||||||
|
}
|
||||||
|
|
||||||
|
const portalPatch = this.portal ? this.portal : document.createElement(vnode.children[0].sel);
|
||||||
|
this.portal = patch(portalPatch, vnode.children![0] as VNode);
|
||||||
|
vnode.children = [];
|
||||||
|
|
||||||
|
super.__patch(vnode);
|
||||||
|
|
||||||
|
if (shouldDeploy) {
|
||||||
|
this.__deployPortal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Override to set the env
|
||||||
|
*/
|
||||||
|
__trigger(component: Component<any, any>, eventType: string, payload?: any) {
|
||||||
|
const env = this.env;
|
||||||
|
this.env = this.parentEnv;
|
||||||
|
super.__trigger(component, eventType, payload);
|
||||||
|
this.env = env;
|
||||||
|
}
|
||||||
|
}
|
||||||
+46
-64
@@ -1,6 +1,6 @@
|
|||||||
import { Context } from "./context";
|
import { CompilationContext } from "./compilation_context";
|
||||||
import { QWebExprVar } from "./expression_parser";
|
|
||||||
import { QWeb } from "./qweb";
|
import { QWeb } from "./qweb";
|
||||||
|
import { htmlToVDOM } from "../vdom/html_to_vdom";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Owl QWeb Directives
|
* Owl QWeb Directives
|
||||||
@@ -25,13 +25,18 @@ QWeb.utils.getFragment = function(str: string): DocumentFragment {
|
|||||||
return temp.content;
|
return temp.content;
|
||||||
};
|
};
|
||||||
|
|
||||||
function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Context) {
|
QWeb.utils.htmlToVDOM = htmlToVDOM;
|
||||||
if (value === "0" && ctx.caller) {
|
|
||||||
qweb._compileNode(ctx.caller, ctx);
|
function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: CompilationContext) {
|
||||||
|
if (value === "0") {
|
||||||
|
const caller = ctx.getCaller();
|
||||||
|
if (caller) {
|
||||||
|
qweb._compileNode(caller, ctx.getInliningContext());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (value.xml instanceof NodeList) {
|
if (value.xml instanceof NodeList && !value.id) {
|
||||||
for (let node of Array.from(value.xml)) {
|
for (let node of Array.from(value.xml)) {
|
||||||
qweb._compileNode(<ChildNode>node, ctx);
|
qweb._compileNode(<ChildNode>node, ctx);
|
||||||
}
|
}
|
||||||
@@ -55,24 +60,25 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Context) {
|
|||||||
ctx.rootContext.rootNode = nodeID;
|
ctx.rootContext.rootNode = nodeID;
|
||||||
ctx.rootContext.parentTextNode = nodeID;
|
ctx.rootContext.parentTextNode = nodeID;
|
||||||
ctx.addLine(`var vn${nodeID} = {text: ${exprID}};`);
|
ctx.addLine(`var vn${nodeID} = {text: ${exprID}};`);
|
||||||
|
if (ctx.rootContext.shouldDefineResult) {
|
||||||
ctx.addLine(`result = vn${nodeID}`);
|
ctx.addLine(`result = vn${nodeID}`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let fragID = ctx.generateID();
|
|
||||||
ctx.rootContext.shouldDefineUtils = true;
|
ctx.rootContext.shouldDefineUtils = true;
|
||||||
ctx.addLine(`var frag${fragID} = utils.getFragment(${exprID})`);
|
ctx.addLine(`c${ctx.parentNode}.push(...utils.htmlToVDOM(${exprID}));`);
|
||||||
let tempNodeID = ctx.generateID();
|
|
||||||
ctx.addLine(`var p${tempNodeID} = {hook: {`);
|
|
||||||
ctx.addLine(` insert: n => n.elm.parentNode.replaceChild(frag${fragID}, n.elm),`);
|
|
||||||
ctx.addLine(`}};`);
|
|
||||||
ctx.addLine(`var vn${tempNodeID} = h('div', p${tempNodeID})`);
|
|
||||||
ctx.addLine(`c${ctx.parentNode}.push(vn${tempNodeID});`);
|
|
||||||
}
|
}
|
||||||
if (node.childNodes.length) {
|
if (node.childNodes.length) {
|
||||||
ctx.addElse();
|
ctx.addElse();
|
||||||
qweb._compileChildren(node, ctx);
|
qweb._compileChildren(node, ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (value.xml instanceof NodeList && value.id) {
|
||||||
|
ctx.addElse();
|
||||||
|
for (let node of Array.from(value.xml)) {
|
||||||
|
qweb._compileNode(<ChildNode>node, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
ctx.closeIf();
|
ctx.closeIf();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,11 +86,6 @@ QWeb.addDirective({
|
|||||||
name: "esc",
|
name: "esc",
|
||||||
priority: 70,
|
priority: 70,
|
||||||
atNodeEncounter({ node, qweb, ctx }): boolean {
|
atNodeEncounter({ node, qweb, ctx }): boolean {
|
||||||
if (node.nodeName !== "t") {
|
|
||||||
let nodeID = qweb._compileGenericNode(node, ctx);
|
|
||||||
ctx = ctx.withParent(nodeID);
|
|
||||||
ctx = ctx.subContext("currentKey", ctx.lastNodeKey);
|
|
||||||
}
|
|
||||||
let value = ctx.getValue(node.getAttribute("t-esc")!);
|
let value = ctx.getValue(node.getAttribute("t-esc")!);
|
||||||
compileValueNode(value, node, qweb, ctx.subContext("escaping", true));
|
compileValueNode(value, node, qweb, ctx.subContext("escaping", true));
|
||||||
return true;
|
return true;
|
||||||
@@ -95,11 +96,6 @@ QWeb.addDirective({
|
|||||||
name: "raw",
|
name: "raw",
|
||||||
priority: 80,
|
priority: 80,
|
||||||
atNodeEncounter({ node, qweb, ctx }): boolean {
|
atNodeEncounter({ node, qweb, ctx }): boolean {
|
||||||
if (node.nodeName !== "t") {
|
|
||||||
let nodeID = qweb._compileGenericNode(node, ctx);
|
|
||||||
ctx = ctx.withParent(nodeID);
|
|
||||||
ctx = ctx.subContext("currentKey", ctx.lastNodeKey);
|
|
||||||
}
|
|
||||||
let value = ctx.getValue(node.getAttribute("t-raw")!);
|
let value = ctx.getValue(node.getAttribute("t-raw")!);
|
||||||
compileValueNode(value, node, qweb, ctx);
|
compileValueNode(value, node, qweb, ctx);
|
||||||
return true;
|
return true;
|
||||||
@@ -116,22 +112,21 @@ QWeb.addDirective({
|
|||||||
atNodeEncounter({ node, ctx }): boolean {
|
atNodeEncounter({ node, ctx }): boolean {
|
||||||
const variable = node.getAttribute("t-set")!;
|
const variable = node.getAttribute("t-set")!;
|
||||||
let value = node.getAttribute("t-value")!;
|
let value = node.getAttribute("t-value")!;
|
||||||
|
ctx.variables[variable] = ctx.variables[variable] || {};
|
||||||
|
let qwebvar = ctx.variables[variable];
|
||||||
|
|
||||||
if (value) {
|
if (value) {
|
||||||
const formattedValue = ctx.formatExpression(value);
|
const formattedValue = ctx.formatExpression(value);
|
||||||
if (ctx.variables.hasOwnProperty(variable)) {
|
if (ctx.variables.hasOwnProperty(variable) && qwebvar.id) {
|
||||||
ctx.addLine(`${(<QWebExprVar>ctx.variables[variable]).id} = ${formattedValue}`);
|
ctx.addLine(`${qwebvar.id} = ${formattedValue}`);
|
||||||
} else {
|
} else {
|
||||||
const varName = `_${ctx.generateID()}`;
|
const varName = `_${ctx.generateID()}`;
|
||||||
ctx.addLine(`var ${varName} = ${formattedValue};`);
|
ctx.addLine(`var ${varName} = ${formattedValue};`);
|
||||||
ctx.variables[variable] = {
|
qwebvar.id = varName;
|
||||||
id: varName,
|
qwebvar.expr = formattedValue;
|
||||||
expr: formattedValue
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ctx.variables[variable] = {
|
qwebvar.xml = node.childNodes;
|
||||||
xml: node.childNodes
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -145,7 +140,7 @@ QWeb.addDirective({
|
|||||||
priority: 20,
|
priority: 20,
|
||||||
atNodeEncounter({ node, ctx }): boolean {
|
atNodeEncounter({ node, ctx }): boolean {
|
||||||
let cond = ctx.getValue(node.getAttribute("t-if")!);
|
let cond = ctx.getValue(node.getAttribute("t-if")!);
|
||||||
ctx.addIf(`${ctx.formatExpression(cond)}`);
|
ctx.addIf(typeof cond === "string" ? ctx.formatExpression(cond) : cond.id!);
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
finalize({ ctx }) {
|
finalize({ ctx }) {
|
||||||
@@ -158,7 +153,7 @@ QWeb.addDirective({
|
|||||||
priority: 30,
|
priority: 30,
|
||||||
atNodeEncounter({ node, ctx }): boolean {
|
atNodeEncounter({ node, ctx }): boolean {
|
||||||
let cond = ctx.getValue(node.getAttribute("t-elif")!);
|
let cond = ctx.getValue(node.getAttribute("t-elif")!);
|
||||||
ctx.addLine(`else if (${ctx.formatExpression(cond)}) {`);
|
ctx.addLine(`else if (${typeof cond === "string" ? ctx.formatExpression(cond) : cond.id}) {`);
|
||||||
ctx.indent();
|
ctx.indent();
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
@@ -199,12 +194,10 @@ QWeb.addDirective({
|
|||||||
nodeCopy.removeAttribute("t-call");
|
nodeCopy.removeAttribute("t-call");
|
||||||
|
|
||||||
// extract variables from nodecopy
|
// extract variables from nodecopy
|
||||||
const tempCtx = new Context();
|
const tempCtx = new CompilationContext();
|
||||||
tempCtx.nextID = ctx.rootContext.nextID;
|
|
||||||
tempCtx.allowMultipleRoots = true;
|
tempCtx.allowMultipleRoots = true;
|
||||||
qweb._compileNode(nodeCopy, tempCtx);
|
qweb._compileNode(nodeCopy, tempCtx);
|
||||||
const vars = Object.assign({}, ctx.variables, tempCtx.variables);
|
const vars = Object.assign({}, ctx.variables, tempCtx.variables);
|
||||||
ctx.rootContext.nextID = tempCtx.nextID;
|
|
||||||
|
|
||||||
const templateMap = Object.create(ctx.templates);
|
const templateMap = Object.create(ctx.templates);
|
||||||
// open new scope, if necessary
|
// open new scope, if necessary
|
||||||
@@ -244,7 +237,7 @@ QWeb.addDirective({
|
|||||||
varCode = `{${content}}`;
|
varCode = `{${content}}`;
|
||||||
}
|
}
|
||||||
ctx.addLine(
|
ctx.addLine(
|
||||||
`this.recursiveFns['${subTemplateName}'].call(this, context, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, vars: ${varCode}, scope}));`
|
`this.recursiveFns['${subTemplateName}'].call(this, context, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, fiber: {vars: ${varCode}, scope}}));`
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -256,8 +249,8 @@ QWeb.addDirective({
|
|||||||
// add new variables, if any
|
// add new variables, if any
|
||||||
for (let key in tempCtx.variables) {
|
for (let key in tempCtx.variables) {
|
||||||
const v = tempCtx.variables[key];
|
const v = tempCtx.variables[key];
|
||||||
if ((<QWebExprVar>v).expr) {
|
if (v.expr) {
|
||||||
ctx.addLine(`let ${(<QWebExprVar>v).id} = ${(<QWebExprVar>v).expr};`);
|
ctx.addLine(`let ${v.id} = ${v.expr};`);
|
||||||
}
|
}
|
||||||
// todo: handle XML variables...
|
// todo: handle XML variables...
|
||||||
}
|
}
|
||||||
@@ -270,10 +263,6 @@ QWeb.addDirective({
|
|||||||
ctx.addLine("}");
|
ctx.addLine("}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.hasAttribute("t-if") || node.hasAttribute("t-else") || node.hasAttribute("t-elif")) {
|
|
||||||
ctx.closeIf();
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -287,7 +276,7 @@ QWeb.addDirective({
|
|||||||
priority: 10,
|
priority: 10,
|
||||||
atNodeEncounter({ node, qweb, ctx }): boolean {
|
atNodeEncounter({ node, qweb, ctx }): boolean {
|
||||||
ctx.rootContext.shouldProtectContext = true;
|
ctx.rootContext.shouldProtectContext = true;
|
||||||
ctx = ctx.subContext("inLoop", true);
|
ctx = ctx.subContext("loopNumber", ctx.loopNumber + 1);
|
||||||
const elems = node.getAttribute("t-foreach")!;
|
const elems = node.getAttribute("t-foreach")!;
|
||||||
const name = node.getAttribute("t-as")!;
|
const name = node.getAttribute("t-as")!;
|
||||||
let arrayID = ctx.generateID();
|
let arrayID = ctx.generateID();
|
||||||
@@ -301,33 +290,26 @@ QWeb.addDirective({
|
|||||||
ctx.addLine(`_${valuesID} = Object.values(_${arrayID});`);
|
ctx.addLine(`_${valuesID} = Object.values(_${arrayID});`);
|
||||||
ctx.closeIf();
|
ctx.closeIf();
|
||||||
ctx.addLine(`var _length${keysID} = _${keysID}.length;`);
|
ctx.addLine(`var _length${keysID} = _${keysID}.length;`);
|
||||||
ctx.addLine(`for (let i = 0; i < _length${keysID}; i++) {`);
|
const loopVar = `i${ctx.loopNumber}`;
|
||||||
|
ctx.addLine(`for (let ${loopVar} = 0; ${loopVar} < _length${keysID}; ${loopVar}++) {`);
|
||||||
ctx.indent();
|
ctx.indent();
|
||||||
ctx.addToScope(name + "_first", "i === 0");
|
ctx.addToScope(name + "_first", `${loopVar} === 0`);
|
||||||
ctx.addToScope(name + "_last", `i === _length${keysID} - 1`);
|
ctx.addToScope(name + "_last", `${loopVar} === _length${keysID} - 1`);
|
||||||
ctx.addToScope(name + "_index", "i");
|
ctx.addToScope(name + "_index", loopVar);
|
||||||
ctx.addToScope(name, `_${keysID}[i]`);
|
ctx.addToScope(name, `_${keysID}[${loopVar}]`);
|
||||||
ctx.addToScope(name + "_value", `_${valuesID}[i]`);
|
ctx.addToScope(name + "_value", `_${valuesID}[${loopVar}]`);
|
||||||
const nodeCopy = <Element>node.cloneNode(true);
|
const nodeCopy = <Element>node.cloneNode(true);
|
||||||
let shouldWarn = nodeCopy.tagName !== "t" && !nodeCopy.hasAttribute("t-key");
|
let shouldWarn =
|
||||||
if (!shouldWarn && node.tagName === "t") {
|
!nodeCopy.hasAttribute("t-key") &&
|
||||||
if (node.hasAttribute("t-component") && !node.hasAttribute("t-key")) {
|
|
||||||
shouldWarn = true;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!shouldWarn &&
|
|
||||||
node.children.length === 1 &&
|
node.children.length === 1 &&
|
||||||
node.children[0].tagName !== "t" &&
|
node.children[0].tagName !== "t" &&
|
||||||
!node.children[0].hasAttribute("t-key")
|
!node.children[0].hasAttribute("t-key");
|
||||||
) {
|
|
||||||
shouldWarn = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (shouldWarn) {
|
if (shouldWarn) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`Directive t-foreach should always be used with a t-key! (in template: '${ctx.templateName}')`
|
`Directive t-foreach should always be used with a t-key! (in template: '${ctx.templateName}')`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
nodeCopy.removeAttribute("t-foreach");
|
nodeCopy.removeAttribute("t-foreach");
|
||||||
qweb._compileNode(nodeCopy, ctx);
|
qweb._compileNode(nodeCopy, ctx);
|
||||||
ctx.dedent();
|
ctx.dedent();
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { compileExpr, QWebVar } from "./expression_parser";
|
import { compileExpr, QWebVar } from "./expression_parser";
|
||||||
|
|
||||||
|
export const INTERP_REGEXP = /\{\{.*?\}\}/g;
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Compilation Context
|
// Compilation Context
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
export class Context {
|
export class CompilationContext {
|
||||||
nextID: number = 1;
|
static nextID: number = 1;
|
||||||
code: string[] = [];
|
code: string[] = [];
|
||||||
variables: { [key: string]: QWebVar } = {};
|
variables: { [key: string]: QWebVar } = {};
|
||||||
escaping: boolean = false;
|
escaping: boolean = false;
|
||||||
@@ -13,24 +14,26 @@ export class Context {
|
|||||||
parentTextNode: number | null = null;
|
parentTextNode: number | null = null;
|
||||||
rootNode: number | null = null;
|
rootNode: number | null = null;
|
||||||
indentLevel: number = 0;
|
indentLevel: number = 0;
|
||||||
rootContext: Context;
|
rootContext: CompilationContext;
|
||||||
caller: Element | undefined;
|
caller: Element | undefined;
|
||||||
shouldDefineOwner: boolean = false;
|
shouldDefineOwner: boolean = false;
|
||||||
shouldDefineParent: boolean = false;
|
shouldDefineParent: boolean = false;
|
||||||
shouldDefineQWeb: boolean = false;
|
shouldDefineQWeb: boolean = false;
|
||||||
shouldDefineUtils: boolean = false;
|
shouldDefineUtils: boolean = false;
|
||||||
|
shouldDefineRefs: boolean = false;
|
||||||
shouldDefineResult: boolean = true;
|
shouldDefineResult: boolean = true;
|
||||||
shouldProtectContext: boolean = false;
|
shouldProtectContext: boolean = false;
|
||||||
shouldTrackScope: boolean = false;
|
shouldTrackScope: boolean = false;
|
||||||
inLoop: boolean = false;
|
loopNumber: number = 0;
|
||||||
inPreTag: boolean = false;
|
inPreTag: boolean = false;
|
||||||
templateName: string;
|
templateName: string;
|
||||||
allowMultipleRoots: boolean = false;
|
allowMultipleRoots: boolean = false;
|
||||||
hasParentWidget: boolean = false;
|
hasParentWidget: boolean = false;
|
||||||
scopeVars: any[] = [];
|
scopeVars: any[] = [];
|
||||||
currentKey: string = "";
|
currentKey: string = "";
|
||||||
lastNodeKey: string = ""; // temp variable to communicate to previous caller
|
|
||||||
templates: { [key: string]: boolean } = {};
|
templates: { [key: string]: boolean } = {};
|
||||||
|
callingLevel: number = 0;
|
||||||
|
inliningLevel: number = 0;
|
||||||
|
|
||||||
constructor(name?: string) {
|
constructor(name?: string) {
|
||||||
this.rootContext = this;
|
this.rootContext = this;
|
||||||
@@ -40,8 +43,31 @@ export class Context {
|
|||||||
}
|
}
|
||||||
|
|
||||||
generateID(): number {
|
generateID(): number {
|
||||||
const id = this.rootContext.nextID++;
|
return CompilationContext.nextID++;
|
||||||
return id;
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method generates a "template key", which is basically a unique key
|
||||||
|
* which depends on the currently set keys, and on the iteration numbers (if
|
||||||
|
* we are in a loop).
|
||||||
|
*
|
||||||
|
* Such a key is necessary when we need to associate an id to some element
|
||||||
|
* generated by a template (for example, a component)
|
||||||
|
*/
|
||||||
|
generateTemplateKey(): string {
|
||||||
|
const id = this.generateID();
|
||||||
|
let locationExpr = `\`__${this.generateID()}__`;
|
||||||
|
for (let i = 0; i < this.loopNumber - 1; i++) {
|
||||||
|
locationExpr += `\${i${i + 1}}__`;
|
||||||
|
}
|
||||||
|
if (this.currentKey) {
|
||||||
|
const k = this.currentKey;
|
||||||
|
this.addLine(`let k${id} = ${locationExpr}\` + ${k};`);
|
||||||
|
} else {
|
||||||
|
locationExpr += this.loopNumber ? `\${i${this.loopNumber}}__\`` : "`";
|
||||||
|
this.addLine(`let k${id} = ${locationExpr};`);
|
||||||
|
}
|
||||||
|
return `k${id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
generateCode(): string[] {
|
generateCode(): string[] {
|
||||||
@@ -61,6 +87,9 @@ export class Context {
|
|||||||
if (this.shouldDefineResult) {
|
if (this.shouldDefineResult) {
|
||||||
this.code.unshift(" let result;");
|
this.code.unshift(" let result;");
|
||||||
}
|
}
|
||||||
|
if (this.shouldDefineRefs) {
|
||||||
|
this.code.unshift(" context.__owl__.refs = context.__owl__.refs || {};");
|
||||||
|
}
|
||||||
if (this.shouldDefineOwner) {
|
if (this.shouldDefineOwner) {
|
||||||
// this is necessary to prevent some directives (t-forach for ex) to
|
// this is necessary to prevent some directives (t-forach for ex) to
|
||||||
// pollute the rendering context by adding some keys in it.
|
// pollute the rendering context by adding some keys in it.
|
||||||
@@ -82,7 +111,7 @@ export class Context {
|
|||||||
return this.code;
|
return this.code;
|
||||||
}
|
}
|
||||||
|
|
||||||
withParent(node: number): Context {
|
withParent(node: number): CompilationContext {
|
||||||
if (
|
if (
|
||||||
!this.allowMultipleRoots &&
|
!this.allowMultipleRoots &&
|
||||||
this === this.rootContext &&
|
this === this.rootContext &&
|
||||||
@@ -93,15 +122,19 @@ export class Context {
|
|||||||
if (!this.rootContext.rootNode) {
|
if (!this.rootContext.rootNode) {
|
||||||
this.rootContext.rootNode = node;
|
this.rootContext.rootNode = node;
|
||||||
}
|
}
|
||||||
if (!this.parentNode) {
|
if (!this.parentNode && this.rootContext.shouldDefineResult) {
|
||||||
this.addLine(`result = vn${node};`);
|
this.addLine(`result = vn${node};`);
|
||||||
}
|
}
|
||||||
return this.subContext("parentNode", node);
|
return this.subContext("parentNode", node);
|
||||||
}
|
}
|
||||||
|
|
||||||
subContext(key: keyof Context, value: any): Context {
|
subContext(key: keyof CompilationContext, value: any): CompilationContext {
|
||||||
const newContext = Object.create(this);
|
const newContext = Object.create(this);
|
||||||
newContext[key] = value;
|
newContext[key] = value;
|
||||||
|
if (key === "caller") {
|
||||||
|
newContext.callingLevel++;
|
||||||
|
newContext.inliningLevel++;
|
||||||
|
}
|
||||||
return newContext;
|
return newContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,8 +172,29 @@ export class Context {
|
|||||||
this.dedent();
|
this.dedent();
|
||||||
this.addLine("}");
|
this.addLine("}");
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Recursively (inverse) fetches the `caller` of a context
|
||||||
|
* Useful to determine to which t-call a t-raw="0" refers
|
||||||
|
*/
|
||||||
|
getCaller(targetLevel?: number): Element | null {
|
||||||
|
if (targetLevel === undefined) {
|
||||||
|
targetLevel = this.inliningLevel;
|
||||||
|
}
|
||||||
|
if (targetLevel === this.callingLevel) {
|
||||||
|
return this.caller || null;
|
||||||
|
}
|
||||||
|
const proto = (this as any).__proto__;
|
||||||
|
return proto ? proto.getCaller(targetLevel) : null;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Marks the context with the current recursive level
|
||||||
|
* in which we are for inlining archs (t-raw="0")
|
||||||
|
*/
|
||||||
|
getInliningContext(): CompilationContext {
|
||||||
|
return this.subContext("inliningLevel", this.inliningLevel - 1);
|
||||||
|
}
|
||||||
|
|
||||||
getValue(val: any): any {
|
getValue(val: any): QWebVar | string {
|
||||||
return val in this.variables ? this.getValue(this.variables[val]) : val;
|
return val in this.variables ? this.getValue(this.variables[val]) : val;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +217,7 @@ export class Context {
|
|||||||
* '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b')
|
* '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b')
|
||||||
*/
|
*/
|
||||||
interpolate(s: string): string {
|
interpolate(s: string): string {
|
||||||
let matches = s.match(/\{\{.*?\}\}/g);
|
let matches = s.match(INTERP_REGEXP);
|
||||||
if (matches && matches[0].length === s.length) {
|
if (matches && matches[0].length === s.length) {
|
||||||
return `(${this.formatExpression(s.slice(2, -2))})`;
|
return `(${this.formatExpression(s.slice(2, -2))})`;
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
// Misc types, constants and helpers
|
// Misc types, constants and helpers
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date".split(
|
const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,eval,void,Math,RegExp,Array,Object,Date".split(
|
||||||
","
|
","
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -38,17 +38,12 @@ const WORD_REPLACEMENT = {
|
|||||||
lte: "<="
|
lte: "<="
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface QWebExprVar {
|
export interface QWebVar {
|
||||||
id: string;
|
id?: string;
|
||||||
expr: string;
|
expr?: string;
|
||||||
|
xml?: NodeList;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QWebXMLVar {
|
|
||||||
xml: NodeList;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type QWebVar = QWebExprVar | QWebXMLVar;
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Tokenizer
|
// Tokenizer
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
@@ -82,7 +77,9 @@ const STATIC_TOKEN_MAP: { [key: string]: TKind } = {
|
|||||||
")": "RIGHT_PAREN"
|
")": "RIGHT_PAREN"
|
||||||
};
|
};
|
||||||
|
|
||||||
const OPERATORS = ".,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%".split(",");
|
// note that the space after typeof is relevant. It makes sure that the formatted
|
||||||
|
// expression has a space after typeof
|
||||||
|
const OPERATORS = ".,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ".split(",");
|
||||||
|
|
||||||
type Tokenizer = (expr: string) => Token | false;
|
type Tokenizer = (expr: string) => Token | false;
|
||||||
|
|
||||||
@@ -165,9 +162,9 @@ const tokenizeOperator: Tokenizer = function(expr) {
|
|||||||
const TOKENIZERS = [
|
const TOKENIZERS = [
|
||||||
tokenizeString,
|
tokenizeString,
|
||||||
tokenizeNumber,
|
tokenizeNumber,
|
||||||
|
tokenizeOperator,
|
||||||
tokenizeSymbol,
|
tokenizeSymbol,
|
||||||
tokenizeStatic,
|
tokenizeStatic
|
||||||
tokenizeOperator
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -252,7 +249,7 @@ export function compileExpr(expr: string, vars: { [key: string]: QWebVar }): str
|
|||||||
}
|
}
|
||||||
if (isVar) {
|
if (isVar) {
|
||||||
if (token.value in vars && "id" in vars[token.value]) {
|
if (token.value in vars && "id" in vars[token.value]) {
|
||||||
token.value = (<QWebExprVar>vars[token.value]).id;
|
token.value = vars[token.value].id!;
|
||||||
} else {
|
} else {
|
||||||
token.value = `context['${token.value}']`;
|
token.value = `context['${token.value}']`;
|
||||||
}
|
}
|
||||||
|
|||||||
+64
-61
@@ -40,34 +40,34 @@ QWeb.addDirective({
|
|||||||
extraArgs = args.slice(1, -1);
|
extraArgs = args.slice(1, -1);
|
||||||
return "";
|
return "";
|
||||||
});
|
});
|
||||||
ctx.addIf(`!context['${handlerName}']`);
|
|
||||||
ctx.addLine(
|
|
||||||
`throw new Error('Missing handler \\'' + '${handlerName}' + \`\\' when evaluating template '${ctx.templateName.replace(
|
|
||||||
/`/g,
|
|
||||||
"'"
|
|
||||||
)}'\`)`
|
|
||||||
);
|
|
||||||
ctx.closeIf();
|
|
||||||
let params = extraArgs ? `owner, ${ctx.formatExpression(extraArgs)}` : "owner";
|
let params = extraArgs ? `owner, ${ctx.formatExpression(extraArgs)}` : "owner";
|
||||||
let handler;
|
let handler = `function (e) {`;
|
||||||
if (mods.length > 0) {
|
|
||||||
handler = `function (e) {`;
|
|
||||||
handler += mods
|
handler += mods
|
||||||
.map(function(mod) {
|
.map(function(mod) {
|
||||||
return MODS_CODE[mod];
|
return MODS_CODE[mod];
|
||||||
})
|
})
|
||||||
.join("");
|
.join("");
|
||||||
handler += `context['${handlerName}'].call(${params}, e);}`;
|
if (handlerName) {
|
||||||
} else {
|
if (!extraArgs) {
|
||||||
handler = `context['${handlerName}'].bind(${params})`;
|
handler += `const fn = context['${handlerName}'];`;
|
||||||
}
|
handler += `if (fn) { fn.call(${params}, e); } else { context.${handlerName}; }`;
|
||||||
if (extraArgs) {
|
handler += `}`;
|
||||||
ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
|
|
||||||
} else {
|
|
||||||
ctx.addLine(
|
ctx.addLine(
|
||||||
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`
|
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`
|
||||||
);
|
);
|
||||||
ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`);
|
ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`);
|
||||||
|
} else {
|
||||||
|
const handlerKey = `handler${ctx.generateID()}`;
|
||||||
|
ctx.addLine(
|
||||||
|
`const ${handlerKey} = context['${handlerName}'] && context['${handlerName}'].bind(${params});`
|
||||||
|
);
|
||||||
|
handler += `if (${handlerKey}) { ${handlerKey}(e); } else { context.${value}; }`;
|
||||||
|
handler += `}`;
|
||||||
|
ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handler += "}";
|
||||||
|
ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -79,9 +79,11 @@ QWeb.addDirective({
|
|||||||
name: "ref",
|
name: "ref",
|
||||||
priority: 95,
|
priority: 95,
|
||||||
atNodeCreation({ ctx, value, addNodeHook }) {
|
atNodeCreation({ ctx, value, addNodeHook }) {
|
||||||
|
ctx.rootContext.shouldDefineRefs = true;
|
||||||
const refKey = `ref${ctx.generateID()}`;
|
const refKey = `ref${ctx.generateID()}`;
|
||||||
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
|
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
|
||||||
addNodeHook("create", `context.refs[${refKey}] = n.elm;`);
|
addNodeHook("create", `context.__owl__.refs[${refKey}] = n.elm;`);
|
||||||
|
addNodeHook("destroy", `delete context.__owl__.refs[${refKey}];`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -103,6 +105,8 @@ QWeb.utils.transitionInsert = function(vn: VNode, name: string) {
|
|||||||
|
|
||||||
elm.classList.add(name + "-enter");
|
elm.classList.add(name + "-enter");
|
||||||
elm.classList.add(name + "-enter-active");
|
elm.classList.add(name + "-enter-active");
|
||||||
|
elm.classList.remove(name + "-leave-active");
|
||||||
|
elm.classList.remove(name + "-leave-to");
|
||||||
const finalize = () => {
|
const finalize = () => {
|
||||||
elm.classList.remove(name + "-enter-active");
|
elm.classList.remove(name + "-enter-active");
|
||||||
elm.classList.remove(name + "-enter-to");
|
elm.classList.remove(name + "-enter-to");
|
||||||
@@ -121,6 +125,9 @@ QWeb.utils.transitionRemove = function(vn: VNode, name: string, rm: () => void)
|
|||||||
elm.classList.add(name + "-leave");
|
elm.classList.add(name + "-leave");
|
||||||
elm.classList.add(name + "-leave-active");
|
elm.classList.add(name + "-leave-active");
|
||||||
const finalize = () => {
|
const finalize = () => {
|
||||||
|
if (!elm.classList.contains(name + "-leave-active")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
elm.classList.remove(name + "-leave-active");
|
elm.classList.remove(name + "-leave-active");
|
||||||
elm.classList.remove(name + "-leave-to");
|
elm.classList.remove(name + "-leave-to");
|
||||||
rm();
|
rm();
|
||||||
@@ -182,42 +189,6 @@ QWeb.addDirective({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// t-mounted
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
QWeb.addDirective({
|
|
||||||
name: "mounted",
|
|
||||||
priority: 97,
|
|
||||||
atNodeCreation({ ctx, fullName, value, nodeID, addNodeHook }) {
|
|
||||||
ctx.rootContext.shouldDefineOwner = true;
|
|
||||||
const eventName = fullName.slice(5);
|
|
||||||
if (!eventName) {
|
|
||||||
throw new Error("Missing event name with t-on directive");
|
|
||||||
}
|
|
||||||
let extraArgs;
|
|
||||||
let handler = value.replace(/\(.*\)/, function(args) {
|
|
||||||
extraArgs = args.slice(1, -1);
|
|
||||||
return "";
|
|
||||||
});
|
|
||||||
let error = `(function () {throw new Error('Missing handler \\'' + '${handler}' + \`\\' when evaluating template '${ctx.templateName.replace(
|
|
||||||
/`/g,
|
|
||||||
"'"
|
|
||||||
)}'\`)})()`;
|
|
||||||
if (extraArgs) {
|
|
||||||
ctx.addLine(
|
|
||||||
`extra.mountedHandlers[${nodeID}] = (context['${handler}'] || ${error}).bind(owner, ${ctx.formatExpression(
|
|
||||||
extraArgs
|
|
||||||
)});`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
ctx.addLine(
|
|
||||||
`extra.mountedHandlers[${nodeID}] = extra.mountedHandlers[${nodeID}] || (context['${handler}'] || ${error}).bind(owner);`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
addNodeHook("insert", `if (context.__owl__.isMounted) { extra.mountedHandlers[${nodeID}](); }`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// t-slot
|
// t-slot
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
@@ -231,9 +202,20 @@ QWeb.addDirective({
|
|||||||
`const slot${slotKey} = this.constructor.slots[context.__owl__.slotId + '_' + '${value}'];`
|
`const slot${slotKey} = this.constructor.slots[context.__owl__.slotId + '_' + '${value}'];`
|
||||||
);
|
);
|
||||||
ctx.addIf(`slot${slotKey}`);
|
ctx.addIf(`slot${slotKey}`);
|
||||||
|
let parentNode = `c${ctx.parentNode}`;
|
||||||
|
if (!ctx.parentNode) {
|
||||||
|
ctx.rootContext.shouldDefineResult = true;
|
||||||
|
ctx.rootContext.shouldDefineUtils = true;
|
||||||
|
parentNode = `children${ctx.generateID()}`;
|
||||||
|
ctx.addLine(`let ${parentNode}= []`);
|
||||||
|
ctx.addLine(`result = {}`);
|
||||||
|
}
|
||||||
ctx.addLine(
|
ctx.addLine(
|
||||||
`slot${slotKey}.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, vars: extra.vars, parent: owner}));`
|
`slot${slotKey}.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: ${parentNode}, vars: extra.vars, parent: extra.parent || owner}));`
|
||||||
);
|
);
|
||||||
|
if (!ctx.parentNode) {
|
||||||
|
ctx.addLine(`utils.defineProxy(result, ${parentNode}[0]);`);
|
||||||
|
}
|
||||||
ctx.closeIf();
|
ctx.closeIf();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -254,7 +236,17 @@ QWeb.addDirective({
|
|||||||
const type = node.getAttribute("type");
|
const type = node.getAttribute("type");
|
||||||
let handler;
|
let handler;
|
||||||
let event = fullName.includes(".lazy") ? "change" : "input";
|
let event = fullName.includes(".lazy") ? "change" : "input";
|
||||||
const expr = ctx.formatExpression(`state.${value}`);
|
|
||||||
|
// we keep here a reference to the "base expression" (if the expression
|
||||||
|
// is `t-model="some.expr.value", then the base expression is "some.expr").
|
||||||
|
// This is necessary so we can capture it in the handler closure.
|
||||||
|
let expr = ctx.formatExpression(value);
|
||||||
|
const index = expr.lastIndexOf(".");
|
||||||
|
const baseExpr = expr.slice(0, index);
|
||||||
|
ctx.addLine(`let expr${nodeID} = ${baseExpr};`);
|
||||||
|
|
||||||
|
expr = `expr${nodeID}.${expr.slice(index + 1)}`;
|
||||||
|
const key = ctx.generateTemplateKey();
|
||||||
if (node.tagName === "select") {
|
if (node.tagName === "select") {
|
||||||
ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
|
ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
|
||||||
addNodeHook("create", `n.elm.value=${expr};`);
|
addNodeHook("create", `n.elm.value=${expr};`);
|
||||||
@@ -278,9 +270,20 @@ QWeb.addDirective({
|
|||||||
}
|
}
|
||||||
handler = `(ev) => {${expr} = ${valueCode}}`;
|
handler = `(ev) => {${expr} = ${valueCode}}`;
|
||||||
}
|
}
|
||||||
ctx.addLine(
|
ctx.addLine(`extra.handlers[${key}] = extra.handlers[${key}] || (${handler});`);
|
||||||
`extra.handlers['${event}' + ${nodeID}] = extra.handlers['${event}' + ${nodeID}] || (${handler});`
|
ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers[${key}];`);
|
||||||
);
|
}
|
||||||
ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers['${event}' + ${nodeID}];`);
|
});
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// t-key
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
QWeb.addDirective({
|
||||||
|
name: "key",
|
||||||
|
priority: 45,
|
||||||
|
atNodeEncounter({ ctx, value }) {
|
||||||
|
let id = ctx.generateID();
|
||||||
|
ctx.addLine(`const nodeKey${id} = ${ctx.formatExpression(value)};`);
|
||||||
|
ctx.currentKey = `nodeKey${id}`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+96
-36
@@ -1,7 +1,8 @@
|
|||||||
import { EventBus } from "../core/event_bus";
|
import { EventBus } from "../core/event_bus";
|
||||||
import { h, patch, VNode } from "../vdom/index";
|
import { h, patch, VNode } from "../vdom/index";
|
||||||
import { Context } from "./context";
|
import { CompilationContext } from "./compilation_context";
|
||||||
import { shallowEqual } from "../utils";
|
import { shallowEqual } from "../utils";
|
||||||
|
import { addNS } from "../vdom/vdom";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Owl QWeb Engine
|
* Owl QWeb Engine
|
||||||
@@ -35,7 +36,7 @@ interface Template {
|
|||||||
interface CompilationInfo {
|
interface CompilationInfo {
|
||||||
node: Element;
|
node: Element;
|
||||||
qweb: QWeb;
|
qweb: QWeb;
|
||||||
ctx: Context;
|
ctx: CompilationContext;
|
||||||
fullName: string;
|
fullName: string;
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
@@ -56,19 +57,27 @@ export interface Directive {
|
|||||||
finalize?(info: CompilationInfo): void;
|
finalize?(info: CompilationInfo): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface QWebConfig {
|
||||||
|
templates?: string;
|
||||||
|
translateFn?(text: string): string;
|
||||||
|
}
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Const/global stuff/helpers
|
// Const/global stuff/helpers
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
const DISABLED_TAGS = ["input", "textarea", "button", "select", "option", "optgroup"];
|
const DISABLED_TAGS = ["input", "textarea", "button", "select", "option", "optgroup"];
|
||||||
|
|
||||||
|
const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
|
||||||
|
|
||||||
const lineBreakRE = /[\r\n]/;
|
const lineBreakRE = /[\r\n]/;
|
||||||
const whitespaceRE = /\s+/g;
|
const whitespaceRE = /\s+/g;
|
||||||
|
|
||||||
const NODE_HOOKS_PARAMS = {
|
const NODE_HOOKS_PARAMS = {
|
||||||
create: "(_, n)",
|
create: "(_, n)",
|
||||||
insert: "vn",
|
insert: "vn",
|
||||||
remove: "(vn, rm)"
|
remove: "(vn, rm)",
|
||||||
|
destroy: "()"
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Utils {
|
interface Utils {
|
||||||
@@ -93,11 +102,15 @@ const UTILS: Utils = {
|
|||||||
}
|
}
|
||||||
return expr;
|
return expr;
|
||||||
},
|
},
|
||||||
shallowEqual
|
shallowEqual,
|
||||||
|
addNameSpace(vnode) {
|
||||||
|
addNS(vnode.data, vnode.children, vnode.sel);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function parseXML(xml: string): Document {
|
function parseXML(xml: string): Document {
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
|
|
||||||
const doc = parser.parseFromString(xml, "text/xml");
|
const doc = parser.parseFromString(xml, "text/xml");
|
||||||
if (doc.getElementsByTagName("parsererror").length) {
|
if (doc.getElementsByTagName("parsererror").length) {
|
||||||
let msg = "Invalid XML in template.";
|
let msg = "Invalid XML in template.";
|
||||||
@@ -128,6 +141,7 @@ function parseXML(xml: string): Document {
|
|||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// QWeb rendering engine
|
// QWeb rendering engine
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
export class QWeb extends EventBus {
|
export class QWeb extends EventBus {
|
||||||
templates: { [name: string]: Template };
|
templates: { [name: string]: Template };
|
||||||
static utils = UTILS;
|
static utils = UTILS;
|
||||||
@@ -137,7 +151,7 @@ export class QWeb extends EventBus {
|
|||||||
name: 1,
|
name: 1,
|
||||||
att: 1,
|
att: 1,
|
||||||
attf: 1,
|
attf: 1,
|
||||||
key: 1
|
translation: 1
|
||||||
};
|
};
|
||||||
static DIRECTIVES: Directive[] = [];
|
static DIRECTIVES: Directive[] = [];
|
||||||
|
|
||||||
@@ -160,12 +174,16 @@ export class QWeb extends EventBus {
|
|||||||
recursiveFns = {};
|
recursiveFns = {};
|
||||||
|
|
||||||
isUpdating: boolean = false;
|
isUpdating: boolean = false;
|
||||||
|
translateFn?: QWebConfig["translateFn"];
|
||||||
|
|
||||||
constructor(data?: string) {
|
constructor(config: QWebConfig = {}) {
|
||||||
super();
|
super();
|
||||||
this.templates = Object.create(QWeb.TEMPLATES);
|
this.templates = Object.create(QWeb.TEMPLATES);
|
||||||
if (data) {
|
if (config.templates) {
|
||||||
this.addTemplates(data);
|
this.addTemplates(config.templates);
|
||||||
|
}
|
||||||
|
if (config.translateFn) {
|
||||||
|
this.translateFn = config.translateFn;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,7 +259,7 @@ export class QWeb extends EventBus {
|
|||||||
this._processTemplate(elem);
|
this._processTemplate(elem);
|
||||||
const template = {
|
const template = {
|
||||||
elem,
|
elem,
|
||||||
fn: (context, extra) => {
|
fn: function(this: QWeb, context, extra) {
|
||||||
const compiledFunction = this._compile(name, elem);
|
const compiledFunction = this._compile(name, elem);
|
||||||
template.fn = compiledFunction;
|
template.fn = compiledFunction;
|
||||||
return compiledFunction.call(this, context, extra);
|
return compiledFunction.call(this, context, extra);
|
||||||
@@ -309,8 +327,8 @@ export class QWeb extends EventBus {
|
|||||||
* to render a full component tree, since this is an asynchronous operation.
|
* to render a full component tree, since this is an asynchronous operation.
|
||||||
* This method can only render templates without components.
|
* This method can only render templates without components.
|
||||||
*/
|
*/
|
||||||
renderToString(name: string, context: EvalContext = {}): string {
|
renderToString(name: string, context: EvalContext = {}, extra?: any): string {
|
||||||
const vnode = this.render(name, context);
|
const vnode = this.render(name, context, extra);
|
||||||
if (vnode.sel === undefined) {
|
if (vnode.sel === undefined) {
|
||||||
return vnode.text!;
|
return vnode.text!;
|
||||||
}
|
}
|
||||||
@@ -335,17 +353,16 @@ export class QWeb extends EventBus {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_compile(name: string, elem: Element, parentContext?: Context): CompiledTemplate {
|
_compile(name: string, elem: Element, parentContext?: CompilationContext): CompiledTemplate {
|
||||||
const isDebug = elem.attributes.hasOwnProperty("t-debug");
|
const isDebug = elem.attributes.hasOwnProperty("t-debug");
|
||||||
const ctx = new Context(name);
|
const ctx = new CompilationContext(name);
|
||||||
if (elem.tagName !== "t") {
|
if (elem.tagName !== "t") {
|
||||||
ctx.shouldDefineResult = false;
|
ctx.shouldDefineResult = false;
|
||||||
}
|
}
|
||||||
if (parentContext) {
|
if (parentContext) {
|
||||||
ctx.templates = Object.create(parentContext.templates);
|
ctx.templates = Object.create(parentContext.templates);
|
||||||
ctx.variables = Object.create(parentContext.variables);
|
ctx.variables = Object.create(parentContext.variables);
|
||||||
ctx.nextID = parentContext.parentNode! + 1;
|
ctx.parentNode = parentContext.parentNode || ctx.generateID();
|
||||||
ctx.parentNode = parentContext.parentNode!;
|
|
||||||
ctx.allowMultipleRoots = true;
|
ctx.allowMultipleRoots = true;
|
||||||
ctx.hasParentWidget = true;
|
ctx.hasParentWidget = true;
|
||||||
ctx.shouldDefineResult = false;
|
ctx.shouldDefineResult = false;
|
||||||
@@ -354,12 +371,12 @@ export class QWeb extends EventBus {
|
|||||||
for (let v in parentContext.variables) {
|
for (let v in parentContext.variables) {
|
||||||
let variable = <any>parentContext.variables[v];
|
let variable = <any>parentContext.variables[v];
|
||||||
if (variable.id) {
|
if (variable.id) {
|
||||||
ctx.addLine(`let ${variable.id} = extra.vars.${variable.id}`);
|
ctx.addLine(`let ${variable.id} = extra.fiber.vars.${variable.id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (parentContext) {
|
if (parentContext) {
|
||||||
ctx.addLine(" Object.assign(context, extra.scope);");
|
ctx.addLine(" Object.assign(context, extra.fiber.scope);");
|
||||||
}
|
}
|
||||||
this._compileNode(elem, ctx);
|
this._compileNode(elem, ctx);
|
||||||
|
|
||||||
@@ -402,7 +419,7 @@ export class QWeb extends EventBus {
|
|||||||
* Generate code from an xml node
|
* Generate code from an xml node
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
_compileNode(node: ChildNode, ctx: Context) {
|
_compileNode(node: ChildNode, ctx: CompilationContext) {
|
||||||
if (!(node instanceof Element)) {
|
if (!(node instanceof Element)) {
|
||||||
// this is a text node, there are no directive to apply
|
// this is a text node, there are no directive to apply
|
||||||
let text = node.textContent!;
|
let text = node.textContent!;
|
||||||
@@ -412,8 +429,17 @@ export class QWeb extends EventBus {
|
|||||||
}
|
}
|
||||||
text = text.replace(whitespaceRE, " ");
|
text = text.replace(whitespaceRE, " ");
|
||||||
}
|
}
|
||||||
|
if (this.translateFn) {
|
||||||
|
if ((node.parentNode as any).getAttribute("t-translation") !== "off") {
|
||||||
|
text = this.translateFn(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (ctx.parentNode) {
|
if (ctx.parentNode) {
|
||||||
|
if (node.nodeType === 3) {
|
||||||
ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`);
|
ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`);
|
||||||
|
} else if (node.nodeType === 8) {
|
||||||
|
ctx.addLine(`c${ctx.parentNode}.push(h('!', \`${text}\`));`);
|
||||||
|
}
|
||||||
} else if (ctx.parentTextNode) {
|
} else if (ctx.parentTextNode) {
|
||||||
ctx.addLine(`vn${ctx.parentTextNode}.text += \`${text}\`;`);
|
ctx.addLine(`vn${ctx.parentTextNode}.text += \`${text}\`;`);
|
||||||
} else {
|
} else {
|
||||||
@@ -428,11 +454,19 @@ export class QWeb extends EventBus {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ctx !== ctx.rootContext) {
|
||||||
|
ctx = ctx.subContext("currentKey", ctx.currentKey);
|
||||||
|
}
|
||||||
|
|
||||||
const firstLetter = node.tagName[0];
|
const firstLetter = node.tagName[0];
|
||||||
if (firstLetter === firstLetter.toUpperCase()) {
|
if (firstLetter === firstLetter.toUpperCase()) {
|
||||||
// this is a component, we modify in place the xml document to change
|
// this is a component, we modify in place the xml document to change
|
||||||
// <SomeComponent ... /> to <t t-component="SomeComponent" ... />
|
// <SomeComponent ... /> to <t t-component="SomeComponent" ... />
|
||||||
node.setAttribute("t-component", node.tagName);
|
node.setAttribute("t-component", node.tagName);
|
||||||
|
} else if (node.tagName !== "t" && node.hasAttribute("t-component")) {
|
||||||
|
throw new Error(
|
||||||
|
`Directive 't-component' can only be used on <t> nodes (used on a <${node.tagName}>)`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const attributes = (<Element>node).attributes;
|
const attributes = (<Element>node).attributes;
|
||||||
|
|
||||||
@@ -442,7 +476,7 @@ export class QWeb extends EventBus {
|
|||||||
fullName: string;
|
fullName: string;
|
||||||
}[] = [];
|
}[] = [];
|
||||||
|
|
||||||
let withHandlers = false;
|
const finalizers: typeof validDirectives = [];
|
||||||
|
|
||||||
// maybe this is not optimal: we iterate on all attributes here, and again
|
// maybe this is not optimal: we iterate on all attributes here, and again
|
||||||
// just after for each directive.
|
// just after for each directive.
|
||||||
@@ -453,11 +487,21 @@ export class QWeb extends EventBus {
|
|||||||
if (!(dName in QWeb.DIRECTIVE_NAMES)) {
|
if (!(dName in QWeb.DIRECTIVE_NAMES)) {
|
||||||
throw new Error(`Unknown QWeb directive: '${attrName}'`);
|
throw new Error(`Unknown QWeb directive: '${attrName}'`);
|
||||||
}
|
}
|
||||||
|
if (node.tagName !== "t" && (attrName === "t-esc" || attrName === "t-raw")) {
|
||||||
|
const tNode = document.createElement("t");
|
||||||
|
tNode.setAttribute(attrName, node.getAttribute(attrName)!);
|
||||||
|
for (let child of Array.from(node.childNodes)) {
|
||||||
|
tNode.appendChild(child);
|
||||||
|
}
|
||||||
|
node.appendChild(tNode);
|
||||||
|
node.removeAttribute(attrName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const DIR_N = QWeb.DIRECTIVES.length;
|
const DIR_N = QWeb.DIRECTIVES.length;
|
||||||
const ATTR_N = attributes.length;
|
const ATTR_N = attributes.length;
|
||||||
|
let withHandlers = false;
|
||||||
for (let i = 0; i < DIR_N; i++) {
|
for (let i = 0; i < DIR_N; i++) {
|
||||||
let directive = QWeb.DIRECTIVES[i];
|
let directive = QWeb.DIRECTIVES[i];
|
||||||
let fullName;
|
let fullName;
|
||||||
@@ -478,7 +522,11 @@ export class QWeb extends EventBus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let { directive, value, fullName } of validDirectives) {
|
for (let { directive, value, fullName } of validDirectives) {
|
||||||
|
if (directive.finalize) {
|
||||||
|
finalizers.push({ directive, value, fullName });
|
||||||
|
}
|
||||||
if (directive.atNodeEncounter) {
|
if (directive.atNodeEncounter) {
|
||||||
const isDone = directive.atNodeEncounter({
|
const isDone = directive.atNodeEncounter({
|
||||||
node,
|
node,
|
||||||
@@ -488,6 +536,9 @@ export class QWeb extends EventBus {
|
|||||||
value
|
value
|
||||||
});
|
});
|
||||||
if (isDone) {
|
if (isDone) {
|
||||||
|
for (let { directive, value, fullName } of finalizers) {
|
||||||
|
directive.finalize!({ node, qweb: this, ctx, fullName, value });
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -496,7 +547,6 @@ export class QWeb extends EventBus {
|
|||||||
if (node.nodeName !== "t") {
|
if (node.nodeName !== "t") {
|
||||||
let nodeID = this._compileGenericNode(node, ctx, withHandlers);
|
let nodeID = this._compileGenericNode(node, ctx, withHandlers);
|
||||||
ctx = ctx.withParent(nodeID);
|
ctx = ctx.withParent(nodeID);
|
||||||
ctx = ctx.subContext("currentKey", ctx.lastNodeKey);
|
|
||||||
let nodeHooks = {};
|
let nodeHooks = {};
|
||||||
let addNodeHook = function(hook, handler) {
|
let addNodeHook = function(hook, handler) {
|
||||||
nodeHooks[hook] = nodeHooks[hook] || [];
|
nodeHooks[hook] = nodeHooks[hook] || [];
|
||||||
@@ -534,15 +584,28 @@ export class QWeb extends EventBus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this._compileChildren(node, ctx);
|
this._compileChildren(node, ctx);
|
||||||
|
// svg support
|
||||||
for (let { directive, value, fullName } of validDirectives) {
|
// we hadd svg namespace if it is a svg or if it is a g, but only if it is
|
||||||
if (directive.finalize) {
|
// the root node. This is the easiest way to support svg sub components:
|
||||||
directive.finalize({ node, qweb: this, ctx, fullName, value });
|
// they need to have a g tag as root. Otherwise, we would need a complete
|
||||||
|
// list of allowed svg tags.
|
||||||
|
const shouldAddNS =
|
||||||
|
node.nodeName === "svg" || (node.nodeName === "g" && ctx.rootNode === ctx.parentNode);
|
||||||
|
if (shouldAddNS) {
|
||||||
|
ctx.rootContext.shouldDefineUtils = true;
|
||||||
|
ctx.addLine(`utils.addNameSpace(vn${ctx.parentNode});`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (let { directive, value, fullName } of finalizers) {
|
||||||
|
directive.finalize!({ node, qweb: this, ctx, fullName, value });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_compileGenericNode(node: ChildNode, ctx: Context, withHandlers: boolean = true): number {
|
_compileGenericNode(
|
||||||
|
node: ChildNode,
|
||||||
|
ctx: CompilationContext,
|
||||||
|
withHandlers: boolean = true
|
||||||
|
): number {
|
||||||
// nodeType 1 is generic tag
|
// nodeType 1 is generic tag
|
||||||
if (node.nodeType !== 1) {
|
if (node.nodeType !== 1) {
|
||||||
throw new Error("unsupported node type");
|
throw new Error("unsupported node type");
|
||||||
@@ -577,7 +640,11 @@ export class QWeb extends EventBus {
|
|||||||
|
|
||||||
for (let i = 0; i < attributes.length; i++) {
|
for (let i = 0; i < attributes.length; i++) {
|
||||||
let name = attributes[i].name;
|
let name = attributes[i].name;
|
||||||
const value = attributes[i].textContent!;
|
let value = attributes[i].textContent!;
|
||||||
|
|
||||||
|
if (this.translateFn && TRANSLATABLE_ATTRS.includes(name)) {
|
||||||
|
value = this.translateFn(value);
|
||||||
|
}
|
||||||
|
|
||||||
// regular attributes
|
// regular attributes
|
||||||
if (!name.startsWith("t-") && !(<Element>node).getAttribute("t-attf-" + name)) {
|
if (!name.startsWith("t-") && !(<Element>node).getAttribute("t-attf-" + name)) {
|
||||||
@@ -605,7 +672,7 @@ export class QWeb extends EventBus {
|
|||||||
if (name.startsWith("t-att-")) {
|
if (name.startsWith("t-att-")) {
|
||||||
let attName = name.slice(6);
|
let attName = name.slice(6);
|
||||||
const v = ctx.getValue(value);
|
const v = ctx.getValue(value);
|
||||||
let formattedValue = v.id || ctx.formatExpression(v);
|
let formattedValue = typeof v === "string" ? ctx.formatExpression(v) : v.id;
|
||||||
|
|
||||||
if (attName === "class") {
|
if (attName === "class") {
|
||||||
ctx.rootContext.shouldDefineUtils = true;
|
ctx.rootContext.shouldDefineUtils = true;
|
||||||
@@ -663,14 +730,7 @@ export class QWeb extends EventBus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let nodeID = ctx.generateID();
|
let nodeID = ctx.generateID();
|
||||||
let nodeKey: any = (<Element>node).getAttribute("t-key");
|
let nodeKey = ctx.currentKey || nodeID;
|
||||||
if (nodeKey) {
|
|
||||||
ctx.addLine(`const nodeKey${nodeID} = ${ctx.formatExpression(nodeKey)}`);
|
|
||||||
nodeKey = `nodeKey${nodeID}`;
|
|
||||||
ctx.lastNodeKey = nodeKey;
|
|
||||||
} else {
|
|
||||||
nodeKey = nodeID;
|
|
||||||
}
|
|
||||||
const parts = [`key:${nodeKey}`];
|
const parts = [`key:${nodeKey}`];
|
||||||
if (attrs.length + tattrs.length > 0) {
|
if (attrs.length + tattrs.length > 0) {
|
||||||
parts.push(`attrs:{${attrs.join(",")}}`);
|
parts.push(`attrs:{${attrs.join(",")}}`);
|
||||||
@@ -705,7 +765,7 @@ export class QWeb extends EventBus {
|
|||||||
return nodeID;
|
return nodeID;
|
||||||
}
|
}
|
||||||
|
|
||||||
_compileChildren(node: ChildNode, ctx: Context) {
|
_compileChildren(node: ChildNode, ctx: CompilationContext) {
|
||||||
if (node.childNodes.length > 0) {
|
if (node.childNodes.length > 0) {
|
||||||
for (let child of Array.from(node.childNodes)) {
|
for (let child of Array.from(node.childNodes)) {
|
||||||
this._compileNode(child, ctx);
|
this._compileNode(child, ctx);
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import { Component } from "../component/component";
|
|
||||||
import { xml } from "../tags";
|
|
||||||
|
|
||||||
export class RouteComponent extends Component<any, {}, {}> {
|
|
||||||
static template = xml`
|
|
||||||
<t t-foreach="routes" t-as="route">
|
|
||||||
<t t-if="env.router.currentRouteName === route.name">
|
|
||||||
<t t-component="{{route.component}}" t-props="env.router.currentParams"/>
|
|
||||||
</t>
|
|
||||||
</t>
|
|
||||||
`;
|
|
||||||
|
|
||||||
routes: any[] = [];
|
|
||||||
constructor(parent, props) {
|
|
||||||
super(parent, props);
|
|
||||||
const router = this.env.router;
|
|
||||||
for (let name of router.routeIds) {
|
|
||||||
const route = router.routes[name];
|
|
||||||
if (route.component) {
|
|
||||||
this.routes.push({
|
|
||||||
name: route.name,
|
|
||||||
component: "__component__" + route.name
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Component } from "../component/component";
|
import { Component } from "../component/component";
|
||||||
import { xml } from "../tags";
|
import { xml } from "../tags";
|
||||||
import { Destination, RouterEnv } from "./Router";
|
import { Destination, RouterEnv } from "./router";
|
||||||
|
|
||||||
type Props = Destination;
|
type Props = Destination;
|
||||||
|
|
||||||
export class Link<Env extends RouterEnv> extends Component<Env, Props, {}> {
|
export class Link<Env extends RouterEnv> extends Component<Env, Props> {
|
||||||
static template = xml`
|
static template = xml`
|
||||||
<a t-att-class="{'router-link-active': isActive }"
|
<a t-att-class="{'router-link-active': isActive }"
|
||||||
t-att-href="href"
|
t-att-href="href"
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Component } from "../component/component";
|
||||||
|
import { xml } from "../tags";
|
||||||
|
|
||||||
|
export class RouteComponent extends Component<any, {}> {
|
||||||
|
static template = xml`
|
||||||
|
<t>
|
||||||
|
<t
|
||||||
|
t-if="routeComponent"
|
||||||
|
t-component="routeComponent"
|
||||||
|
t-key="env.router.currentRouteName"
|
||||||
|
t-props="env.router.currentParams" />
|
||||||
|
</t>
|
||||||
|
`;
|
||||||
|
|
||||||
|
get routeComponent(): any {
|
||||||
|
return this.env.router.currentRoute && this.env.router.currentRoute.component;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -148,7 +148,7 @@ export class Router {
|
|||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
private setUrlFromPath(path: string) {
|
private setUrlFromPath(path: string) {
|
||||||
const separator = this.mode === "hash" ? "/" : "";
|
const separator = this.mode === "hash" ? location.pathname : "";
|
||||||
const url = location.origin + separator + path;
|
const url = location.origin + separator + path;
|
||||||
if (url !== window.location.href) {
|
if (url !== window.location.href) {
|
||||||
window.history.pushState({}, path, url);
|
window.history.pushState({}, path, url);
|
||||||
+156
@@ -0,0 +1,156 @@
|
|||||||
|
import { Component } from "./component/component";
|
||||||
|
import { Env } from "./component/component";
|
||||||
|
import { Context, useContextWithCB } from "./context";
|
||||||
|
import { onWillUpdateProps } from "./hooks";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Owl Store
|
||||||
|
*
|
||||||
|
* We have here:
|
||||||
|
* - a Store class
|
||||||
|
* - useStore hook
|
||||||
|
* - useDispatch hook
|
||||||
|
* - useGetters hook
|
||||||
|
*
|
||||||
|
* The Owl store is our answer to the problem of managing complex state across
|
||||||
|
* components. The main idea is that the store owns some state, allow external
|
||||||
|
* code to modify it through actions, and for each state changes,
|
||||||
|
* connected component will be notified, and updated if necessary.
|
||||||
|
*
|
||||||
|
* Note that this code is partly inspired by VueX and React/Redux
|
||||||
|
*/
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Store Definition
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export type Action = ({ state, dispatch, env, getters }, ...payload: any) => any;
|
||||||
|
export type Getter = ({ state: any, getters }, payload?) => any;
|
||||||
|
|
||||||
|
interface StoreConfig {
|
||||||
|
env?: Env;
|
||||||
|
state?: any;
|
||||||
|
actions?: { [name: string]: Action };
|
||||||
|
getters?: { [name: string]: Getter };
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Store extends Context {
|
||||||
|
actions: any;
|
||||||
|
env: any;
|
||||||
|
getters: { [name: string]: (payload?) => any };
|
||||||
|
updateFunctions: { [key: number]: (() => boolean)[] };
|
||||||
|
|
||||||
|
constructor(config: StoreConfig) {
|
||||||
|
super(config.state);
|
||||||
|
this.actions = config.actions;
|
||||||
|
this.env = config.env;
|
||||||
|
this.getters = {};
|
||||||
|
this.updateFunctions = [];
|
||||||
|
if (config.getters) {
|
||||||
|
const firstArg = {
|
||||||
|
state: this.state,
|
||||||
|
getters: this.getters
|
||||||
|
};
|
||||||
|
for (let g in config.getters) {
|
||||||
|
this.getters[g] = config.getters[g].bind(this, firstArg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(action: string, ...payload: any): Promise<void> | void {
|
||||||
|
if (!this.actions[action]) {
|
||||||
|
throw new Error(`[Error] action ${action} is undefined`);
|
||||||
|
}
|
||||||
|
const result = this.actions[action](
|
||||||
|
{
|
||||||
|
dispatch: this.dispatch.bind(this),
|
||||||
|
env: this.env,
|
||||||
|
state: this.state,
|
||||||
|
getters: this.getters
|
||||||
|
},
|
||||||
|
...payload
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SelectorOptions {
|
||||||
|
store?: Store;
|
||||||
|
isEqual?: (a: any, b: any) => boolean;
|
||||||
|
onUpdate?: (result: any) => any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isStrictEqual = (a, b) => a === b;
|
||||||
|
|
||||||
|
export function useStore(selector, options: SelectorOptions = {}): any {
|
||||||
|
const component: Component<any, any> = Component.current!;
|
||||||
|
const componentId = component.__owl__.id;
|
||||||
|
const store = options.store || (component.env.store as Store);
|
||||||
|
if (!(store instanceof Store)) {
|
||||||
|
throw new Error(`No store found when connecting '${component.constructor.name}'`);
|
||||||
|
}
|
||||||
|
let result = selector(store.state, component.props);
|
||||||
|
const hashFn = store.observer.revNumber.bind(store.observer);
|
||||||
|
let revNumber = hashFn(result);
|
||||||
|
const isEqual = options.isEqual || isStrictEqual;
|
||||||
|
if (!store.updateFunctions[componentId]) {
|
||||||
|
store.updateFunctions[componentId] = [];
|
||||||
|
}
|
||||||
|
function selectCompareUpdate(state, props): boolean {
|
||||||
|
const oldResult = result;
|
||||||
|
result = selector(state, props);
|
||||||
|
const newRevNumber = hashFn(result);
|
||||||
|
if ((newRevNumber > 0 && revNumber !== newRevNumber) || !isEqual(oldResult, result)) {
|
||||||
|
revNumber = newRevNumber;
|
||||||
|
if (options.onUpdate) {
|
||||||
|
options.onUpdate(result);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
store.updateFunctions[componentId].push(function(): boolean {
|
||||||
|
return selectCompareUpdate(store!.state, component.props);
|
||||||
|
});
|
||||||
|
|
||||||
|
useContextWithCB(store, component, function(): Promise<void> | void {
|
||||||
|
let shouldRender = false;
|
||||||
|
for (let fn of store.updateFunctions[componentId]) {
|
||||||
|
shouldRender = fn() || shouldRender;
|
||||||
|
}
|
||||||
|
if (shouldRender) {
|
||||||
|
return component.render();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onWillUpdateProps(props => {
|
||||||
|
selectCompareUpdate(store.state, props);
|
||||||
|
});
|
||||||
|
|
||||||
|
const __destroy = component.__destroy;
|
||||||
|
component.__destroy = parent => {
|
||||||
|
delete store.updateFunctions[componentId];
|
||||||
|
__destroy.call(component, parent);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof result !== "object") {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return new Proxy(result, {
|
||||||
|
get(target, k) {
|
||||||
|
return result[k];
|
||||||
|
},
|
||||||
|
set(target, k, v) {
|
||||||
|
throw new Error("Store state should only be modified through actions");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDispatch(store?: Store): Store["dispatch"] {
|
||||||
|
store = store || (Component.current!.env.store as Store);
|
||||||
|
return store.dispatch.bind(store);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGetters(store?: Store): Store["getters"] {
|
||||||
|
store = store || (Component.current!.env.store as Store);
|
||||||
|
return store.getters;
|
||||||
|
}
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
import { Component, Env } from "../component/component";
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Connect function
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
type HashFunction = (a: any, b: any) => number;
|
|
||||||
|
|
||||||
export class ConnectedComponent<T extends Env, P, S> extends Component<T, P, S> {
|
|
||||||
deep: boolean = true;
|
|
||||||
getStore(env) {
|
|
||||||
return env.store;
|
|
||||||
}
|
|
||||||
|
|
||||||
storeProps: any;
|
|
||||||
|
|
||||||
hashFunction: HashFunction = (storeProps, options) => {
|
|
||||||
const revFn = (this.__owl__ as any).revFn;
|
|
||||||
const rev = revFn(storeProps);
|
|
||||||
if (rev > 0) {
|
|
||||||
return rev;
|
|
||||||
}
|
|
||||||
let hash = 0;
|
|
||||||
for (let key in storeProps) {
|
|
||||||
const val = storeProps[key];
|
|
||||||
const hashVal = revFn(val);
|
|
||||||
if (hashVal === 0) {
|
|
||||||
if (val !== options.prevStoreProps[key]) {
|
|
||||||
options.didChange = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
hash += hashVal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return hash;
|
|
||||||
};
|
|
||||||
|
|
||||||
static mapStoreToProps(storeState, ownProps, getters) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(name, ...payload) {
|
|
||||||
return (this.__owl__ as any).store.dispatch(name, ...payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Need to do this here so 'deep' can be overrided by subcomponent easily
|
|
||||||
*/
|
|
||||||
async __prepareAndRender(
|
|
||||||
scope?: Object,
|
|
||||||
vars?: any
|
|
||||||
): ReturnType<Component<any, any, any>["__prepareAndRender"]> {
|
|
||||||
const store = this.getStore(this.env);
|
|
||||||
const ownProps = this.props || {};
|
|
||||||
this.storeProps = (<any>this.constructor).mapStoreToProps(store.state, ownProps, store.getters);
|
|
||||||
const observer = store.observer;
|
|
||||||
const revFn = this.deep ? observer.deepRevNumber : observer.revNumber;
|
|
||||||
(this.__owl__ as any).store = store;
|
|
||||||
(this.__owl__ as any).ownProps = this.props;
|
|
||||||
(this.__owl__ as any).revFn = revFn.bind(observer);
|
|
||||||
(this.__owl__ as any).storeHash = this.hashFunction(this.storeProps, {
|
|
||||||
prevStoreProps: this.storeProps
|
|
||||||
});
|
|
||||||
(this.__owl__ as any).rev = observer.rev;
|
|
||||||
return super.__prepareAndRender(scope, vars);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* We do not use the mounted hook here for a subtle reason: we want the
|
|
||||||
* updates to be called for the parents before the children. However,
|
|
||||||
* if we use the mounted hook, this will be done in the reverse order.
|
|
||||||
*/
|
|
||||||
__callMounted() {
|
|
||||||
(this.__owl__ as any).store.on("update", this, this.__checkUpdate);
|
|
||||||
super.__callMounted();
|
|
||||||
}
|
|
||||||
__callWillUnmount() {
|
|
||||||
(this.__owl__ as any).store.off("update", this);
|
|
||||||
super.__callWillUnmount();
|
|
||||||
}
|
|
||||||
__destroy(parent: any) {
|
|
||||||
(this.__owl__ as any).store.off("update", this);
|
|
||||||
super.__destroy(parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
async render(force: boolean = false) {
|
|
||||||
this.__updateStoreProps(this.props);
|
|
||||||
|
|
||||||
// this is quite technical, so this deserves some explanation.
|
|
||||||
// When we have a connected component, it can be updated for 3 reasons:
|
|
||||||
// - some internal state changes (this will go through this method)
|
|
||||||
// - some props changes (if a parent is changed and need to rerender itself)
|
|
||||||
// - a store update
|
|
||||||
//
|
|
||||||
// It is possible (with connected component and parent) to have the following
|
|
||||||
// situation: the parent component is rendered first (from its state change),
|
|
||||||
// then immediately after, it is rendered (from store update). Then, if the
|
|
||||||
// __checkUpdate method is immediately over, the children component will
|
|
||||||
// be rendered again by the store update, even though it is supposed to be
|
|
||||||
// destroyed by the first rendering.
|
|
||||||
//
|
|
||||||
// So, the solution is to keep the information that there is a current
|
|
||||||
// rendering occuring with the same store state, the same props, and return
|
|
||||||
// that in the __checkUpdate method. To do this, we use the renderPromise
|
|
||||||
// deferred, which is not used by the component system once the
|
|
||||||
// component is ready, so we can use it for our own purpose.
|
|
||||||
(this.__owl__ as any).renderPromise = super.render(force);
|
|
||||||
return (this.__owl__ as any).renderPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
async __updateProps(nextProps: P, f, p, s, v) {
|
|
||||||
this.__updateStoreProps(nextProps);
|
|
||||||
return super.__updateProps(nextProps, f, p, s, v);
|
|
||||||
}
|
|
||||||
|
|
||||||
__updateStoreProps(nextProps): boolean {
|
|
||||||
const __owl__ = this.__owl__ as any;
|
|
||||||
const store = __owl__.store;
|
|
||||||
const observer = store.observer;
|
|
||||||
if (observer.rev === __owl__.rev && nextProps === __owl__.ownProps) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const storeProps = (<any>this.constructor).mapStoreToProps(
|
|
||||||
store.state,
|
|
||||||
nextProps,
|
|
||||||
store.getters
|
|
||||||
);
|
|
||||||
const options = { prevStoreProps: this.storeProps, didChange: false };
|
|
||||||
const storeHash = this.hashFunction(storeProps, options);
|
|
||||||
this.storeProps = storeProps;
|
|
||||||
let didChange = options.didChange;
|
|
||||||
if (storeHash !== __owl__.storeHash) {
|
|
||||||
__owl__.storeHash = storeHash;
|
|
||||||
didChange = true;
|
|
||||||
}
|
|
||||||
__owl__.rev = store.observer.rev;
|
|
||||||
__owl__.ownProps = nextProps;
|
|
||||||
return didChange;
|
|
||||||
}
|
|
||||||
|
|
||||||
async __checkUpdate() {
|
|
||||||
const didChange = this.__updateStoreProps(this.props);
|
|
||||||
if (didChange) {
|
|
||||||
return this.render();
|
|
||||||
}
|
|
||||||
// see note in render method
|
|
||||||
return (this.__owl__ as any).renderPromise;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import { Env } from "../component/component";
|
|
||||||
import { EventBus } from "../core/event_bus";
|
|
||||||
import { Observer } from "../core/observer";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Owl Store
|
|
||||||
*
|
|
||||||
* We have here:
|
|
||||||
* - a Store class
|
|
||||||
* - the ConnectedComponent class
|
|
||||||
*
|
|
||||||
* The Owl store is our answer to the problem of managing complex state across
|
|
||||||
* components. The main idea is that the store owns some state, allow external
|
|
||||||
* code to modify it through actions, and for each state changes,
|
|
||||||
* connected component will be notified, and updated if necessary.
|
|
||||||
*
|
|
||||||
* Note that this code is partly inspired by VueX and React/Redux
|
|
||||||
*/
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Store Definition
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
export type Action = ({ state, dispatch, env, getters }, ...payload: any) => any;
|
|
||||||
export type Getter = ({ state: any, getters }, payload?) => any;
|
|
||||||
|
|
||||||
interface StoreConfig {
|
|
||||||
env?: Env;
|
|
||||||
state?: any;
|
|
||||||
actions?: { [name: string]: Action };
|
|
||||||
getters?: { [name: string]: Getter };
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StoreOption {
|
|
||||||
debug?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Store extends EventBus {
|
|
||||||
state: any;
|
|
||||||
actions: any;
|
|
||||||
mutations: any;
|
|
||||||
debug: boolean;
|
|
||||||
env: any;
|
|
||||||
observer: Observer;
|
|
||||||
getters: { [name: string]: (payload?) => any };
|
|
||||||
|
|
||||||
constructor(config: StoreConfig, options: StoreOption = {}) {
|
|
||||||
super();
|
|
||||||
this.debug = options.debug || false;
|
|
||||||
this.actions = config.actions;
|
|
||||||
this.env = config.env;
|
|
||||||
this.observer = new Observer();
|
|
||||||
this.observer.notifyCB = this.__notifyComponents.bind(this);
|
|
||||||
this.state = this.observer.observe(config.state || {});
|
|
||||||
this.getters = {};
|
|
||||||
if (config.getters) {
|
|
||||||
const firstArg = {
|
|
||||||
state: this.state,
|
|
||||||
getters: this.getters
|
|
||||||
};
|
|
||||||
for (let g in config.getters) {
|
|
||||||
this.getters[g] = config.getters[g].bind(this, firstArg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(action: string, ...payload: any): Promise<void> | void {
|
|
||||||
if (!this.actions[action]) {
|
|
||||||
throw new Error(`[Error] action ${action} is undefined`);
|
|
||||||
}
|
|
||||||
const result = this.actions[action](
|
|
||||||
{
|
|
||||||
dispatch: this.dispatch.bind(this),
|
|
||||||
env: this.env,
|
|
||||||
state: this.state,
|
|
||||||
getters: this.getters
|
|
||||||
},
|
|
||||||
...payload
|
|
||||||
);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instead of using trigger to emit an update event, we actually implement
|
|
||||||
* our own function to do that. The reason is that we need to be smarter than
|
|
||||||
* a simple trigger function: we need to wait for parent components to be
|
|
||||||
* done before doing children components. The reason is that if an update
|
|
||||||
* as an effect of destroying a children, we do not want to call the
|
|
||||||
* mapStoreToProps function of the child, nor rendering it.
|
|
||||||
*
|
|
||||||
* This method is not optimal if we have a bunch of asynchronous components:
|
|
||||||
* we wait sequentially for each component to be completed before updating the
|
|
||||||
* next. However, the only things that matters is that children are updated
|
|
||||||
* after their parents. So, this could be optimized by being smarter, and
|
|
||||||
* updating all widgets concurrently, except for parents/children.
|
|
||||||
*/
|
|
||||||
async __notifyComponents() {
|
|
||||||
const subs = this.subscriptions.update || [];
|
|
||||||
for (let i = 0, iLen = subs.length; i < iLen; i++) {
|
|
||||||
const sub = subs[i];
|
|
||||||
const shouldCallback = sub.owner ? sub.owner.__owl__.isMounted : true;
|
|
||||||
if (shouldCallback) {
|
|
||||||
await sub.callback.call(sub.owner);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+3
-4
@@ -10,8 +10,6 @@ import { QWeb } from "./qweb/index";
|
|||||||
* The plan is to add a few other tags such as css, globalcss.
|
* The plan is to add a few other tags such as css, globalcss.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* XML tag helper for defining templates. With this, one can simply define
|
* XML tag helper for defining templates. With this, one can simply define
|
||||||
* an inline template with just the template xml:
|
* an inline template with just the template xml:
|
||||||
@@ -21,8 +19,9 @@ import { QWeb } from "./qweb/index";
|
|||||||
* }
|
* }
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function xml(strings) {
|
export function xml(strings, ...args) {
|
||||||
const name = `__template__${QWeb.nextId++}`;
|
const name = `__template__${QWeb.nextId++}`;
|
||||||
QWeb.registerTemplate(name, strings[0]);
|
const value = String.raw(strings, ...args);
|
||||||
|
QWeb.registerTemplate(name, value);
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-5
@@ -5,7 +5,7 @@
|
|||||||
*
|
*
|
||||||
* - whenReady
|
* - whenReady
|
||||||
* - loadJS
|
* - loadJS
|
||||||
* - loadTemplates
|
* - loadFile
|
||||||
* - escape
|
* - escape
|
||||||
* - debounce
|
* - debounce
|
||||||
*/
|
*/
|
||||||
@@ -43,14 +43,12 @@ export function loadJS(url: string): Promise<void> {
|
|||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadTemplates(url: string): Promise<string> {
|
export async function loadFile(url: string): Promise<string> {
|
||||||
const result = await fetch(url);
|
const result = await fetch(url);
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
throw new Error("Error while fetching xml templates");
|
throw new Error("Error while fetching xml templates");
|
||||||
}
|
}
|
||||||
let templates = await result.text();
|
return await result.text();
|
||||||
templates = templates.replace(/<!--[\s\S]*?-->/g, "");
|
|
||||||
return templates;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function escape(str: string | number | undefined): string {
|
export function escape(str: string | number | undefined): string {
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { VNode, h } from "./vdom";
|
||||||
|
|
||||||
|
const parser = new DOMParser();
|
||||||
|
|
||||||
|
export function htmlToVDOM(html: string): VNode[] {
|
||||||
|
const doc = parser.parseFromString(html, "text/html");
|
||||||
|
const result: VNode[] = [];
|
||||||
|
for (let child of doc.body.childNodes) {
|
||||||
|
result.push(htmlToVNode(child));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function htmlToVNode(node: ChildNode): VNode {
|
||||||
|
if (!(node instanceof Element)) {
|
||||||
|
return { text: node.textContent! } as VNode;
|
||||||
|
}
|
||||||
|
const attrs = {};
|
||||||
|
for (let attr of node.attributes) {
|
||||||
|
attrs[attr.name] = attr.textContent;
|
||||||
|
}
|
||||||
|
const children: VNode[] = [];
|
||||||
|
for (let c of node.childNodes) {
|
||||||
|
children.push(htmlToVNode(c));
|
||||||
|
}
|
||||||
|
return h((node as Element).tagName, { attrs }, children);
|
||||||
|
}
|
||||||
+15
-23
@@ -101,7 +101,7 @@ function isVnode(vnode: any): vnode is VNode {
|
|||||||
|
|
||||||
type KeyToIndexMap = { [key: string]: number };
|
type KeyToIndexMap = { [key: string]: number };
|
||||||
|
|
||||||
type ArraysOf<T> = { [K in keyof T]: (T[K])[] };
|
type ArraysOf<T> = { [K in keyof T]: T[K][] };
|
||||||
|
|
||||||
type ModuleHooks = ArraysOf<Module>;
|
type ModuleHooks = ArraysOf<Module>;
|
||||||
|
|
||||||
@@ -176,18 +176,12 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
|||||||
}
|
}
|
||||||
vnode.elm = api.createComment(vnode.text as string);
|
vnode.elm = api.createComment(vnode.text as string);
|
||||||
} else if (sel !== undefined) {
|
} else if (sel !== undefined) {
|
||||||
// Parse selector
|
const elm =
|
||||||
const hashIdx = sel.indexOf("#");
|
vnode.elm ||
|
||||||
const dotIdx = sel.indexOf(".", hashIdx);
|
(vnode.elm =
|
||||||
const hash = hashIdx > 0 ? hashIdx : sel.length;
|
|
||||||
const dot = dotIdx > 0 ? dotIdx : sel.length;
|
|
||||||
const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel;
|
|
||||||
const elm = (vnode.elm =
|
|
||||||
isDef(data) && isDef((i = (data as VNodeData).ns))
|
isDef(data) && isDef((i = (data as VNodeData).ns))
|
||||||
? api.createElementNS(i, tag)
|
? api.createElementNS(i, sel)
|
||||||
: api.createElement(tag));
|
: api.createElement(sel));
|
||||||
if (hash < dot) elm.setAttribute("id", sel.slice(hash + 1, dot));
|
|
||||||
if (dotIdx > 0) elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " "));
|
|
||||||
for (i = 0, iLen = cbs.create.length; i < iLen; ++i) cbs.create[i](emptyNode, vnode);
|
for (i = 0, iLen = cbs.create.length; i < iLen; ++i) cbs.create[i](emptyNode, vnode);
|
||||||
if (array(children)) {
|
if (array(children)) {
|
||||||
for (i = 0, iLen = children.length; i < iLen; ++i) {
|
for (i = 0, iLen = children.length; i < iLen; ++i) {
|
||||||
@@ -564,13 +558,19 @@ type VNodeChildElement = VNode | string | number | undefined | null;
|
|||||||
type ArrayOrElement<T> = T | T[];
|
type ArrayOrElement<T> = T | T[];
|
||||||
type VNodeChildren = ArrayOrElement<VNodeChildElement>;
|
type VNodeChildren = ArrayOrElement<VNodeChildElement>;
|
||||||
|
|
||||||
function addNS(data: any, children: VNodes | undefined, sel: string | undefined): void {
|
export function addNS(data: any, children: VNodes | undefined, sel: string | undefined): void {
|
||||||
|
if (sel === "dummy") {
|
||||||
|
// we do not need to add the namespace on dummy elements, they come from a
|
||||||
|
// subcomponent, which will handle the namespace itself
|
||||||
|
return;
|
||||||
|
}
|
||||||
data.ns = "http://www.w3.org/2000/svg";
|
data.ns = "http://www.w3.org/2000/svg";
|
||||||
if (sel !== "foreignObject" && children !== undefined) {
|
if (sel !== "foreignObject" && children !== undefined) {
|
||||||
for (let i = 0, iLen = children.length; i < iLen; ++i) {
|
for (let i = 0, iLen = children.length; i < iLen; ++i) {
|
||||||
let childData = children[i].data;
|
const child = children[i];
|
||||||
|
let childData = child.data;
|
||||||
if (childData !== undefined) {
|
if (childData !== undefined) {
|
||||||
addNS(childData, (children[i] as VNode).children as VNodes, children[i].sel);
|
addNS(childData, (child as VNode).children as VNodes, child.sel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -612,13 +612,5 @@ export function h(sel: any, b?: any, c?: any): VNode {
|
|||||||
children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
|
children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
sel[0] === "s" &&
|
|
||||||
sel[1] === "v" &&
|
|
||||||
sel[2] === "g" &&
|
|
||||||
(sel.length === 3 || sel[3] === "." || sel[3] === "#")
|
|
||||||
) {
|
|
||||||
addNS(data, children, sel);
|
|
||||||
}
|
|
||||||
return vnode(sel, data, children, text, undefined);
|
return vnode(sel, data, children, text, undefined);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,37 +10,36 @@ exports[`animations t-transition combined with component 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
//COMPONENT
|
//COMPONENT
|
||||||
let def3;
|
let k3 = \`__4__\`;
|
||||||
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
let w2 = k3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k3]] : false;
|
||||||
let _2_index = c1.length;
|
let props2 = {};
|
||||||
c1.push(null);
|
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
|
||||||
let props4 = {};
|
w2.destroy();
|
||||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
w2 = false;
|
||||||
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
}
|
||||||
def3 = w4.__owl__.renderPromise;
|
if (w2) {
|
||||||
|
w2.__updateProps(props2, extra.fiber, undefined, undefined);
|
||||||
|
let pvnode = w2.__owl__.pvnode;
|
||||||
|
c1.push(pvnode);
|
||||||
} else {
|
} else {
|
||||||
w4.destroy();
|
let componentKey2 = \`Child\`;
|
||||||
w4 = false;
|
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| context['Child'];
|
||||||
}
|
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
|
||||||
}
|
w2 = new W2(parent, props2);
|
||||||
if (!w4) {
|
const __patch2 = w2.__patch;
|
||||||
let componentKey4 = \`Child\`;
|
w2.__patch = fiber => {__patch2.call(w2, fiber); if(!w2.__owl__.transitionInserted){w2.__owl__.transitionInserted = true;utils.transitionInsert(w2.__owl__.vnode, 'chimay');}};
|
||||||
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4];
|
parent.__owl__.cmap[k3] = w2.__owl__.id;
|
||||||
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
let fiber = w2.__prepare(extra.fiber, undefined, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
|
||||||
w4 = new W4(parent, props4);
|
let pvnode = h('dummy', {key: k3, hook: {remove() {},destroy(vn) {let finalize = () => {
|
||||||
parent.__owl__.cmap[4] = w4.__owl__.id;
|
w2.destroy();
|
||||||
def3 = w4.__prepare();
|
|
||||||
def3 = def3.then(vnode=>{if (w4.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
|
|
||||||
w4.destroy();
|
|
||||||
};
|
};
|
||||||
utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
delete w2.__owl__.transitionInserted;
|
||||||
} else {
|
utils.transitionRemove(vn, 'chimay', finalize);}}});
|
||||||
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
c1.push(pvnode);
|
||||||
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
w2.__owl__.pvnode = pvnode;
|
||||||
}
|
}
|
||||||
extra.promises.push(def3);
|
w2.__owl__.parentLastFiberId = extra.fiber.id;
|
||||||
return vn1;
|
return vn1;
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
@@ -55,38 +54,83 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
if (context['state'].display) {
|
if (context['state'].display) {
|
||||||
//COMPONENT
|
//COMPONENT
|
||||||
let def3;
|
let k3 = \`__4__\`;
|
||||||
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
let w2 = k3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k3]] : false;
|
||||||
let _2_index = c1.length;
|
let props2 = {};
|
||||||
c1.push(null);
|
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
|
||||||
let props4 = {};
|
w2.destroy();
|
||||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
w2 = false;
|
||||||
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
}
|
||||||
def3 = w4.__owl__.renderPromise;
|
if (w2) {
|
||||||
|
w2.__updateProps(props2, extra.fiber, undefined, undefined);
|
||||||
|
let pvnode = w2.__owl__.pvnode;
|
||||||
|
c1.push(pvnode);
|
||||||
} else {
|
} else {
|
||||||
w4.destroy();
|
let componentKey2 = \`Child\`;
|
||||||
w4 = false;
|
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| context['Child'];
|
||||||
}
|
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
|
||||||
}
|
w2 = new W2(parent, props2);
|
||||||
if (!w4) {
|
const __patch2 = w2.__patch;
|
||||||
let componentKey4 = \`Child\`;
|
w2.__patch = fiber => {__patch2.call(w2, fiber); if(!w2.__owl__.transitionInserted){w2.__owl__.transitionInserted = true;utils.transitionInsert(w2.__owl__.vnode, 'chimay');}};
|
||||||
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4];
|
parent.__owl__.cmap[k3] = w2.__owl__.id;
|
||||||
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
let fiber = w2.__prepare(extra.fiber, undefined, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
|
||||||
w4 = new W4(parent, props4);
|
let pvnode = h('dummy', {key: k3, hook: {remove() {},destroy(vn) {let finalize = () => {
|
||||||
parent.__owl__.cmap[4] = w4.__owl__.id;
|
w2.destroy();
|
||||||
def3 = w4.__prepare();
|
|
||||||
def3 = def3.then(vnode=>{if (w4.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
|
|
||||||
w4.destroy();
|
|
||||||
};
|
};
|
||||||
utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
delete w2.__owl__.transitionInserted;
|
||||||
} else {
|
utils.transitionRemove(vn, 'chimay', finalize);}}});
|
||||||
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
c1.push(pvnode);
|
||||||
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
w2.__owl__.pvnode = pvnode;
|
||||||
}
|
}
|
||||||
extra.promises.push(def3);
|
w2.__owl__.parentLastFiberId = extra.fiber.id;
|
||||||
|
}
|
||||||
|
return vn1;
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`animations t-transition combined with t-component, remove and re-add before transitionend 1`] = `
|
||||||
|
"function anonymous(context,extra
|
||||||
|
) {
|
||||||
|
let utils = this.constructor.utils;
|
||||||
|
let QWeb = this.constructor;
|
||||||
|
let parent = context;
|
||||||
|
let owner = context;
|
||||||
|
var h = this.h;
|
||||||
|
let c1 = [], p1 = {key:1};
|
||||||
|
var vn1 = h('div', p1, c1);
|
||||||
|
if (context['state'].flag) {
|
||||||
|
//COMPONENT
|
||||||
|
let k3 = \`__4__\`;
|
||||||
|
let w2 = k3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k3]] : false;
|
||||||
|
let props2 = {};
|
||||||
|
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
|
||||||
|
w2.destroy();
|
||||||
|
w2 = false;
|
||||||
|
}
|
||||||
|
if (w2) {
|
||||||
|
w2.__updateProps(props2, extra.fiber, undefined, undefined);
|
||||||
|
let pvnode = w2.__owl__.pvnode;
|
||||||
|
c1.push(pvnode);
|
||||||
|
} else {
|
||||||
|
let componentKey2 = \`Child\`;
|
||||||
|
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| context['Child'];
|
||||||
|
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
|
||||||
|
w2 = new W2(parent, props2);
|
||||||
|
const __patch2 = w2.__patch;
|
||||||
|
w2.__patch = fiber => {__patch2.call(w2, fiber); if(!w2.__owl__.transitionInserted){w2.__owl__.transitionInserted = true;utils.transitionInsert(w2.__owl__.vnode, 'chimay');}};
|
||||||
|
parent.__owl__.cmap[k3] = w2.__owl__.id;
|
||||||
|
let fiber = w2.__prepare(extra.fiber, undefined, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
|
||||||
|
let pvnode = h('dummy', {key: k3, hook: {remove() {},destroy(vn) {let finalize = () => {
|
||||||
|
w2.destroy();
|
||||||
|
};
|
||||||
|
delete w2.__owl__.transitionInserted;
|
||||||
|
utils.transitionRemove(vn, 'chimay', finalize);}}});
|
||||||
|
c1.push(pvnode);
|
||||||
|
w2.__owl__.pvnode = pvnode;
|
||||||
|
}
|
||||||
|
w2.__owl__.parentLastFiberId = extra.fiber.id;
|
||||||
}
|
}
|
||||||
return vn1;
|
return vn1;
|
||||||
}"
|
}"
|
||||||
@@ -99,7 +143,6 @@ exports[`animations t-transition with no delay/duration 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('span', p1, c1);
|
var vn1 = h('span', p1, c1);
|
||||||
result = vn1;
|
|
||||||
p1.hook = {
|
p1.hook = {
|
||||||
insert: vn => {
|
insert: vn => {
|
||||||
utils.transitionInsert(vn, 'jupiler');
|
utils.transitionInsert(vn, 'jupiler');
|
||||||
@@ -120,7 +163,6 @@ exports[`animations t-transition, on a simple node (insert) 1`] = `
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('span', p1, c1);
|
var vn1 = h('span', p1, c1);
|
||||||
result = vn1;
|
|
||||||
p1.hook = {
|
p1.hook = {
|
||||||
insert: vn => {
|
insert: vn => {
|
||||||
utils.transitionInsert(vn, 'chimay');
|
utils.transitionInsert(vn, 'chimay');
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`various scenarios scenarios with async store updates and some components events 1`] = `"<div><button>Do stuff</button><div><span>Attachment 100</span><span>Name: text.txt</span></div></div>"`;
|
||||||
|
|
||||||
|
exports[`various scenarios scenarios with async store updates and some components events 2`] = `"<div><button>Do stuff</button></div>"`;
|
||||||
+91
-66
@@ -1,13 +1,15 @@
|
|||||||
import { Component, Env } from "../src/component/component";
|
import { Component, Env } from "../src/component/component";
|
||||||
import { QWeb } from "../src/qweb/index";
|
import { QWeb } from "../src/qweb/index";
|
||||||
|
import { useState, useRef } from "../src/hooks";
|
||||||
|
import { xml } from "../src/tags";
|
||||||
import {
|
import {
|
||||||
makeDeferred,
|
makeDeferred,
|
||||||
makeTestFixture,
|
makeTestFixture,
|
||||||
makeTestEnv,
|
makeTestEnv,
|
||||||
nextTick,
|
|
||||||
patchNextFrame,
|
patchNextFrame,
|
||||||
renderToDOM,
|
renderToDOM,
|
||||||
unpatchNextFrame
|
unpatchNextFrame,
|
||||||
|
nextTick
|
||||||
} from "./helpers";
|
} from "./helpers";
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
@@ -29,6 +31,7 @@ let cssEl: HTMLElement;
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = makeTestFixture();
|
fixture = makeTestFixture();
|
||||||
env = makeTestEnv();
|
env = makeTestEnv();
|
||||||
|
Component.env = env;
|
||||||
qweb = new QWeb();
|
qweb = new QWeb();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -36,7 +39,7 @@ afterEach(() => {
|
|||||||
fixture.remove();
|
fixture.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
class Widget extends Component<any, any, any> {}
|
class Widget extends Component<any, any> {}
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Tests
|
// Tests
|
||||||
@@ -106,9 +109,9 @@ describe("animations", () => {
|
|||||||
`<div><span t-if="!state.hide" t-transition="chimay">blue</span></div>`
|
`<div><span t-if="!state.hide" t-transition="chimay">blue</span></div>`
|
||||||
);
|
);
|
||||||
class TestWidget extends Widget {
|
class TestWidget extends Widget {
|
||||||
state = { hide: false };
|
state = useState({ hide: false });
|
||||||
}
|
}
|
||||||
const widget = new TestWidget(env);
|
const widget = new TestWidget();
|
||||||
|
|
||||||
// insert widget into the DOM
|
// insert widget into the DOM
|
||||||
let def = makeDeferred();
|
let def = makeDeferred();
|
||||||
@@ -148,9 +151,10 @@ describe("animations", () => {
|
|||||||
`<div><span t-ref="span" t-transition="chimay">blue</span></div>`
|
`<div><span t-ref="span" t-transition="chimay">blue</span></div>`
|
||||||
);
|
);
|
||||||
class TestWidget extends Widget {
|
class TestWidget extends Widget {
|
||||||
state = { hide: false };
|
state = useState({ hide: false });
|
||||||
|
span = useRef("span");
|
||||||
}
|
}
|
||||||
const widget = new TestWidget(env);
|
const widget = new TestWidget();
|
||||||
|
|
||||||
// insert widget into the DOM
|
// insert widget into the DOM
|
||||||
let def = makeDeferred();
|
let def = makeDeferred();
|
||||||
@@ -163,7 +167,7 @@ describe("animations", () => {
|
|||||||
});
|
});
|
||||||
await widget.mount(fixture);
|
await widget.mount(fixture);
|
||||||
spanNode = widget.el!.children[0];
|
spanNode = widget.el!.children[0];
|
||||||
expect(widget.refs.span).toBe(spanNode);
|
expect(widget.span.el).toBe(spanNode);
|
||||||
expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
|
expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
|
||||||
await def; // wait for the mocked repaint to be done
|
await def; // wait for the mocked repaint to be done
|
||||||
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||||
@@ -179,7 +183,7 @@ describe("animations", () => {
|
|||||||
class Parent extends Widget {
|
class Parent extends Widget {
|
||||||
static components = { Child: Child };
|
static components = { Child: Child };
|
||||||
}
|
}
|
||||||
const widget = new Parent(env);
|
const widget = new Parent();
|
||||||
|
|
||||||
let def = makeDeferred();
|
let def = makeDeferred();
|
||||||
var spanNode;
|
var spanNode;
|
||||||
@@ -217,9 +221,9 @@ describe("animations", () => {
|
|||||||
class Child extends Widget {}
|
class Child extends Widget {}
|
||||||
class Parent extends Widget {
|
class Parent extends Widget {
|
||||||
static components = { Child: Child };
|
static components = { Child: Child };
|
||||||
state = { display: true };
|
state = useState({ display: true });
|
||||||
}
|
}
|
||||||
const widget = new Parent(env);
|
const widget = new Parent();
|
||||||
|
|
||||||
let def = makeDeferred();
|
let def = makeDeferred();
|
||||||
var spanNode;
|
var spanNode;
|
||||||
@@ -250,11 +254,11 @@ describe("animations", () => {
|
|||||||
widget.state.display = false;
|
widget.state.display = false;
|
||||||
patchNextFrame(cb => {
|
patchNextFrame(cb => {
|
||||||
expect(fixture.innerHTML).toBe(
|
expect(fixture.innerHTML).toBe(
|
||||||
'<div><span class="chimay-leave chimay-leave-active" data-owl-key="4">blue</span></div>'
|
'<div><span class="chimay-leave chimay-leave-active" data-owl-key="__4__">blue</span></div>'
|
||||||
);
|
);
|
||||||
cb();
|
cb();
|
||||||
expect(fixture.innerHTML).toBe(
|
expect(fixture.innerHTML).toBe(
|
||||||
'<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="4">blue</span></div>'
|
'<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="__4__">blue</span></div>'
|
||||||
);
|
);
|
||||||
def.resolve();
|
def.resolve();
|
||||||
});
|
});
|
||||||
@@ -263,28 +267,6 @@ describe("animations", () => {
|
|||||||
expect(fixture.innerHTML).toBe("<div></div>");
|
expect(fixture.innerHTML).toBe("<div></div>");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("t-transition combined with t-mounted", async () => {
|
|
||||||
env.qweb.addTemplate(
|
|
||||||
"TestWidget",
|
|
||||||
`<div><span t-if="state.flag" t-mounted="f" t-transition="chimay">blue</span></div>`
|
|
||||||
);
|
|
||||||
class TestWidget extends Widget {
|
|
||||||
state = { flag: false };
|
|
||||||
f() {}
|
|
||||||
}
|
|
||||||
const widget = new TestWidget(env);
|
|
||||||
widget.f = jest.fn();
|
|
||||||
await widget.mount(fixture);
|
|
||||||
|
|
||||||
patchNextFrame(cb => cb());
|
|
||||||
expect(widget.f).toHaveBeenCalledTimes(0);
|
|
||||||
|
|
||||||
widget.state.flag = true;
|
|
||||||
await nextTick();
|
|
||||||
expect(widget.f).toHaveBeenCalledTimes(1);
|
|
||||||
unpatchNextFrame();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("t-transition, remove and re-add before transitionend", async () => {
|
test("t-transition, remove and re-add before transitionend", async () => {
|
||||||
expect.assertions(11);
|
expect.assertions(11);
|
||||||
|
|
||||||
@@ -297,16 +279,14 @@ describe("animations", () => {
|
|||||||
</templates>`
|
</templates>`
|
||||||
);
|
);
|
||||||
class Parent extends Widget {
|
class Parent extends Widget {
|
||||||
constructor(parent) {
|
state = useState({ flag: false });
|
||||||
super(parent);
|
|
||||||
this.state = { flag: false };
|
|
||||||
}
|
|
||||||
toggle() {
|
toggle() {
|
||||||
this.state.flag = !this.state.flag;
|
this.state.flag = !this.state.flag;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const widget = new Parent(env);
|
const widget = new Parent();
|
||||||
await widget.mount(fixture);
|
await widget.mount(fixture);
|
||||||
let button = widget.el!.querySelector("button");
|
let button = widget.el!.querySelector("button");
|
||||||
|
|
||||||
@@ -343,32 +323,23 @@ describe("animations", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("t-transition combined with t-component, remove and re-add before transitionend", async () => {
|
test("t-transition combined with t-component, remove and re-add before transitionend", async () => {
|
||||||
expect.assertions(11);
|
expect.assertions(12);
|
||||||
|
|
||||||
env.qweb.addTemplates(
|
class Child extends Widget {
|
||||||
`<templates>
|
static template = xml`<span>blue</span>`;
|
||||||
<div t-name="Parent">
|
}
|
||||||
<button t-on-click="toggle">Toggle</button>
|
|
||||||
<t t-if="state.flag" t-component="Child" t-transition="chimay"/>
|
|
||||||
</div>
|
|
||||||
<span t-name="Child">blue</span>
|
|
||||||
</templates>`
|
|
||||||
);
|
|
||||||
class Child extends Widget {}
|
|
||||||
class Parent extends Widget {
|
class Parent extends Widget {
|
||||||
|
static template = xml`
|
||||||
|
<div t-name="Parent">
|
||||||
|
<t t-if="state.flag" t-component="Child" t-transition="chimay"/>
|
||||||
|
</div>`;
|
||||||
static components = { Child };
|
static components = { Child };
|
||||||
constructor(parent) {
|
state = useState({ flag: false });
|
||||||
super(parent);
|
|
||||||
this.state = { flag: false };
|
|
||||||
}
|
|
||||||
toggle() {
|
|
||||||
this.state.flag = !this.state.flag;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const widget = new Parent(env);
|
const widget = new Parent();
|
||||||
await widget.mount(fixture);
|
await widget.mount(fixture);
|
||||||
let button = widget.el!.querySelector("button");
|
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
|
||||||
|
|
||||||
let def = makeDeferred();
|
let def = makeDeferred();
|
||||||
let phase = "enter";
|
let phase = "enter";
|
||||||
@@ -381,24 +352,78 @@ describe("animations", () => {
|
|||||||
def.resolve();
|
def.resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
// click display the span
|
// display the span
|
||||||
button!.click();
|
widget.state.flag = true;
|
||||||
await def; // wait for the mocked repaint to be done
|
await def; // wait for the mocked repaint to be done
|
||||||
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||||
expect(fixture.innerHTML).toBe('<div><button>Toggle</button><span class="">blue</span></div>');
|
expect(fixture.innerHTML).toBe('<div><span class="">blue</span></div>');
|
||||||
|
|
||||||
// click to remove the span, and click again to re-add it before transitionend
|
// click to remove the span, and click again to re-add it before transitionend
|
||||||
def = makeDeferred();
|
def = makeDeferred();
|
||||||
phase = "leave";
|
phase = "leave";
|
||||||
button!.click();
|
|
||||||
|
widget.state.flag = false;
|
||||||
|
|
||||||
await def; // wait for the mocked repaint to be done
|
await def; // wait for the mocked repaint to be done
|
||||||
def = makeDeferred();
|
def = makeDeferred();
|
||||||
phase = "enter";
|
phase = "enter";
|
||||||
button!.click();
|
widget.state.flag = true;
|
||||||
|
|
||||||
await def; // wait for the mocked repaint to be done
|
await def; // wait for the mocked repaint to be done
|
||||||
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||||
expect(fixture.innerHTML).toBe('<div><button>Toggle</button><span class="">blue</span></div>');
|
expect(fixture.innerHTML).toBe('<div><span class="" data-owl-key="__4__">blue</span></div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test("transitionInsert is called the correct amount of times", async () => {
|
||||||
|
const oldTransitionInsert = QWeb.utils.transitionInsert;
|
||||||
|
QWeb.utils.transitionInsert = jest.fn(oldTransitionInsert);
|
||||||
|
|
||||||
|
class Child extends Widget {
|
||||||
|
static template = xml`<span>blue</span>`;
|
||||||
|
}
|
||||||
|
class Parent extends Widget {
|
||||||
|
static template = xml`
|
||||||
|
<div t-name="Parent">
|
||||||
|
<Child t-if="state.flag" t-transition="chimay"/>
|
||||||
|
</div>`;
|
||||||
|
static components = { Child };
|
||||||
|
state = useState({ flag: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
patchNextFrame(cb => cb());
|
||||||
|
|
||||||
|
const widget = new Parent();
|
||||||
|
await widget.mount(fixture);
|
||||||
|
|
||||||
|
widget.state.flag = true;
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend"));
|
||||||
|
expect(fixture.innerHTML).toBe('<div><span class="">blue</span></div>');
|
||||||
|
expect(QWeb.utils.transitionInsert).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
widget.state.flag = false;
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe(
|
||||||
|
'<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="__4__">blue</span></div>'
|
||||||
|
);
|
||||||
|
expect(QWeb.utils.transitionInsert).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
widget.state.flag = true;
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe(
|
||||||
|
'<div><span class="chimay-enter-active chimay-enter-to" data-owl-key="__4__">blue</span></div>'
|
||||||
|
);
|
||||||
|
expect(QWeb.utils.transitionInsert).toBeCalledTimes(2);
|
||||||
|
|
||||||
|
widget.state.flag = false;
|
||||||
|
await nextTick();
|
||||||
|
widget.state.flag = true;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(QWeb.utils.transitionInsert).toBeCalledTimes(3);
|
||||||
|
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend"));
|
||||||
|
expect(fixture.innerHTML).toBe('<div><span class="" data-owl-key="__4__">blue</span></div>');
|
||||||
|
QWeb.utils.transitionInsert = oldTransitionInsert;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,5 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`default props default values are also set whenever component is updated 1`] = `"<div>1</div>"`;
|
|
||||||
|
|
||||||
exports[`default props default values are also set whenever component is updated 2`] = `"<div>4</div>"`;
|
|
||||||
|
|
||||||
exports[`props validation props are validated in dev mode (code snapshot) 1`] = `
|
exports[`props validation props are validated in dev mode (code snapshot) 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
@@ -14,36 +10,30 @@ exports[`props validation props are validated in dev mode (code snapshot) 1`] =
|
|||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
result = vn1;
|
|
||||||
//COMPONENT
|
//COMPONENT
|
||||||
let def3;
|
let k3 = \`__4__\`;
|
||||||
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
let w2 = k3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k3]] : false;
|
||||||
let _2_index = c1.length;
|
let props2 = {message:1};
|
||||||
c1.push(null);
|
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
|
||||||
let props4 = {message:1};
|
w2.destroy();
|
||||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
w2 = false;
|
||||||
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
}
|
||||||
def3 = w4.__owl__.renderPromise;
|
if (w2) {
|
||||||
|
w2.__updateProps(props2, extra.fiber, undefined, undefined);
|
||||||
|
let pvnode = w2.__owl__.pvnode;
|
||||||
|
c1.push(pvnode);
|
||||||
} else {
|
} else {
|
||||||
w4.destroy();
|
let componentKey2 = \`Child\`;
|
||||||
w4 = false;
|
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| context['Child'];
|
||||||
|
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
|
||||||
|
w2 = new W2(parent, props2);
|
||||||
|
parent.__owl__.cmap[k3] = w2.__owl__.id;
|
||||||
|
let fiber = w2.__prepare(extra.fiber, undefined, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
|
||||||
|
let pvnode = h('dummy', {key: k3, hook: {remove() {},destroy(vn) {w2.destroy();}}});
|
||||||
|
c1.push(pvnode);
|
||||||
|
w2.__owl__.pvnode = pvnode;
|
||||||
}
|
}
|
||||||
}
|
w2.__owl__.parentLastFiberId = extra.fiber.id;
|
||||||
if (!w4) {
|
|
||||||
let componentKey4 = \`Child\`;
|
|
||||||
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4];
|
|
||||||
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
|
||||||
utils.validateProps(W4, props4)
|
|
||||||
w4 = new W4(parent, props4);
|
|
||||||
parent.__owl__.cmap[4] = w4.__owl__.id;
|
|
||||||
def3 = w4.__prepare();
|
|
||||||
def3 = def3.then(vnode=>{if (w4.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
|
||||||
} else {
|
|
||||||
utils.validateProps(w4.constructor, props4)
|
|
||||||
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
|
||||||
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
|
||||||
}
|
|
||||||
extra.promises.push(def3);
|
|
||||||
return vn1;
|
return vn1;
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|||||||
+2569
-1069
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,8 @@
|
|||||||
import { Component, Env } from "../../src/component/component";
|
import { Component, Env } from "../../src/component/component";
|
||||||
import { makeTestFixture, makeTestEnv } from "../helpers";
|
import { makeTestFixture, makeTestEnv, nextTick } from "../helpers";
|
||||||
|
import { useState } from "../../src/hooks";
|
||||||
import { QWeb } from "../../src/qweb";
|
import { QWeb } from "../../src/qweb";
|
||||||
|
import { xml } from "../../src/tags";
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Setup and helpers
|
// Setup and helpers
|
||||||
@@ -13,6 +15,7 @@ let dev: boolean = false;
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = makeTestFixture();
|
fixture = makeTestFixture();
|
||||||
env = makeTestEnv();
|
env = makeTestEnv();
|
||||||
|
Component.env = env;
|
||||||
dev = QWeb.dev;
|
dev = QWeb.dev;
|
||||||
QWeb.dev = true;
|
QWeb.dev = true;
|
||||||
});
|
});
|
||||||
@@ -22,7 +25,7 @@ afterEach(() => {
|
|||||||
QWeb.dev = dev;
|
QWeb.dev = dev;
|
||||||
});
|
});
|
||||||
|
|
||||||
class Widget extends Component<any, any, any> {}
|
class Widget extends Component<any, any> {}
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Tests
|
// Tests
|
||||||
@@ -31,28 +34,55 @@ describe("props validation", () => {
|
|||||||
test("validation is only done in dev mode", async () => {
|
test("validation is only done in dev mode", async () => {
|
||||||
class TestWidget extends Widget {
|
class TestWidget extends Widget {
|
||||||
static props = ["message"];
|
static props = ["message"];
|
||||||
|
static template = xml`<div>hey</div>`;
|
||||||
|
}
|
||||||
|
class Parent extends Widget {
|
||||||
|
static components = { TestWidget };
|
||||||
|
static template = xml`<div><TestWidget /></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let error;
|
||||||
QWeb.dev = true;
|
QWeb.dev = true;
|
||||||
expect(() => {
|
try {
|
||||||
new TestWidget(env);
|
const p = new Parent();
|
||||||
}).toThrow();
|
await p.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(error.message).toBe(`Missing props 'message' (component 'TestWidget')`);
|
||||||
|
|
||||||
|
error = undefined;
|
||||||
|
|
||||||
QWeb.dev = false;
|
QWeb.dev = false;
|
||||||
|
try {
|
||||||
expect(() => {
|
const p = new Parent();
|
||||||
new TestWidget(env);
|
await p.mount(fixture);
|
||||||
}).not.toThrow();
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("props: list of strings", async () => {
|
test("props: list of strings", async () => {
|
||||||
class TestWidget extends Widget {
|
class TestWidget extends Widget {
|
||||||
static props = ["message"];
|
static props = ["message"];
|
||||||
|
static template = xml`<div>hey</div>`;
|
||||||
|
}
|
||||||
|
class Parent extends Widget {
|
||||||
|
static components = { TestWidget };
|
||||||
|
static template = xml`<div><TestWidget /></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(() => {
|
let error;
|
||||||
new TestWidget(env);
|
try {
|
||||||
}).toThrow("Missing props 'message' (component 'TestWidget')");
|
const p = new Parent();
|
||||||
|
await p.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(error.message).toBe(`Missing props 'message' (component 'TestWidget')`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("validate simple types", async () => {
|
test("validate simple types", async () => {
|
||||||
@@ -65,22 +95,50 @@ describe("props validation", () => {
|
|||||||
{ type: Function, ok: () => {}, ko: "1" }
|
{ type: Function, ok: () => {}, ko: "1" }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
let props;
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static template = xml`<div><TestWidget p="p"/></div>`;
|
||||||
|
get p() {
|
||||||
|
return props.p;
|
||||||
|
}
|
||||||
|
}
|
||||||
for (let test of Tests) {
|
for (let test of Tests) {
|
||||||
let TestWidget = class extends Widget {
|
let TestWidget = class extends Widget {
|
||||||
|
static template = xml`<div>hey</div>`;
|
||||||
static props = { p: test.type };
|
static props = { p: test.type };
|
||||||
};
|
};
|
||||||
|
Parent.components = { TestWidget };
|
||||||
|
|
||||||
expect(() => {
|
let error;
|
||||||
new TestWidget(env);
|
props = {};
|
||||||
}).toThrow("Missing props 'p'");
|
try {
|
||||||
|
const p = new Parent();
|
||||||
|
await p.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(error.message).toBe(`Missing props 'p' (component '_a')`);
|
||||||
|
|
||||||
expect(() => {
|
error = undefined;
|
||||||
new TestWidget(env, { p: test.ok });
|
props = { p: test.ok };
|
||||||
}).not.toThrow();
|
try {
|
||||||
|
const p = new Parent();
|
||||||
|
await p.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeUndefined();
|
||||||
|
|
||||||
expect(() => {
|
props = { p: test.ko };
|
||||||
new TestWidget(env, { p: test.ko });
|
try {
|
||||||
}).toThrow("Props 'p' of invalid type in component");
|
const p = new Parent();
|
||||||
|
await p.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(error.message).toBe("Invalid Prop 'p' in component '_a'");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -94,113 +152,307 @@ describe("props validation", () => {
|
|||||||
{ type: Function, ok: () => {}, ko: "1" }
|
{ type: Function, ok: () => {}, ko: "1" }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
let props;
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static template = xml`<div><TestWidget p="p"/></div>`;
|
||||||
|
get p() {
|
||||||
|
return props.p;
|
||||||
|
}
|
||||||
|
}
|
||||||
for (let test of Tests) {
|
for (let test of Tests) {
|
||||||
let TestWidget = class extends Widget {
|
let TestWidget = class extends Component<any, any> {
|
||||||
static props = { p: { type: test.type } };
|
static props = { p: { type: test.type } };
|
||||||
|
static template = xml`<div>hey</div>`;
|
||||||
};
|
};
|
||||||
|
Parent.components = { TestWidget };
|
||||||
|
|
||||||
expect(() => {
|
let error;
|
||||||
new TestWidget(env);
|
props = {};
|
||||||
}).toThrow("Missing props 'p'");
|
try {
|
||||||
|
const p = new Parent();
|
||||||
|
await p.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(error.message).toBe(`Missing props 'p' (component '_a')`);
|
||||||
|
|
||||||
expect(() => {
|
error = undefined;
|
||||||
new TestWidget(env, { p: test.ok });
|
props = { p: test.ok };
|
||||||
}).not.toThrow();
|
try {
|
||||||
|
const p = new Parent();
|
||||||
|
await p.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeUndefined();
|
||||||
|
|
||||||
expect(() => {
|
props = { p: test.ko };
|
||||||
new TestWidget(env, { p: test.ko });
|
try {
|
||||||
}).toThrow("Props 'p' of invalid type in component");
|
const p = new Parent();
|
||||||
|
await p.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(error.message).toBe("Invalid Prop 'p' in component '_a'");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can validate a prop with multiple types", async () => {
|
test("can validate a prop with multiple types", async () => {
|
||||||
let TestWidget = class extends Widget {
|
class TestWidget extends Component<any, any> {
|
||||||
|
static template = xml`<div>hey</div>`;
|
||||||
static props = { p: [String, Boolean] };
|
static props = { p: [String, Boolean] };
|
||||||
};
|
}
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static template = xml`<div><TestWidget p="p"/></div>`;
|
||||||
|
static components = { TestWidget };
|
||||||
|
get p() {
|
||||||
|
return props.p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
expect(() => {
|
let error;
|
||||||
new TestWidget(env, { p: "string" });
|
let props;
|
||||||
new TestWidget(env, { p: true });
|
try {
|
||||||
}).not.toThrow();
|
props = { p: "string" };
|
||||||
|
const p = new Parent();
|
||||||
|
await p.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeUndefined();
|
||||||
|
|
||||||
expect(() => {
|
try {
|
||||||
new TestWidget(env, { p: 1 });
|
props = { p: true };
|
||||||
}).toThrow("Props 'p' of invalid type in component");
|
const p = new Parent();
|
||||||
|
await p.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeUndefined();
|
||||||
|
|
||||||
|
try {
|
||||||
|
props = { p: 1 };
|
||||||
|
const p = new Parent();
|
||||||
|
await p.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can validate an optional props", async () => {
|
test("can validate an optional props", async () => {
|
||||||
let TestWidget = class extends Widget {
|
class TestWidget extends Component<any, any> {
|
||||||
|
static template = xml`<div>hey</div>`;
|
||||||
static props = { p: { type: String, optional: true } };
|
static props = { p: { type: String, optional: true } };
|
||||||
};
|
}
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static template = xml`<div><TestWidget p="p"/></div>`;
|
||||||
|
static components = { TestWidget };
|
||||||
|
get p() {
|
||||||
|
return props.p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
expect(() => {
|
let error;
|
||||||
new TestWidget(env, { p: "hey" });
|
let props;
|
||||||
new TestWidget(env, {});
|
try {
|
||||||
}).not.toThrow();
|
props = { p: "key" };
|
||||||
|
const p = new Parent();
|
||||||
|
await p.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeUndefined();
|
||||||
|
|
||||||
expect(() => {
|
try {
|
||||||
new TestWidget(env, { p: 1 });
|
props = {};
|
||||||
}).toThrow();
|
const p = new Parent();
|
||||||
|
await p.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeUndefined();
|
||||||
|
|
||||||
|
try {
|
||||||
|
props = { p: 1 };
|
||||||
|
const p = new Parent();
|
||||||
|
await p.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can validate an array with given primitive type", async () => {
|
test("can validate an array with given primitive type", async () => {
|
||||||
let TestWidget = class extends Widget {
|
class TestWidget extends Component<any, any> {
|
||||||
|
static template = xml`<div>hey</div>`;
|
||||||
static props = { p: { type: Array, element: String } };
|
static props = { p: { type: Array, element: String } };
|
||||||
};
|
}
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static template = xml`<div><TestWidget p="p"/></div>`;
|
||||||
|
static components = { TestWidget };
|
||||||
|
get p() {
|
||||||
|
return props.p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
expect(() => {
|
let error;
|
||||||
new TestWidget(env, { p: [] });
|
let props;
|
||||||
new TestWidget(env, { p: ["string"] });
|
try {
|
||||||
}).not.toThrow();
|
props = { p: [] };
|
||||||
|
const p = new Parent();
|
||||||
|
await p.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeUndefined();
|
||||||
|
|
||||||
expect(() => {
|
try {
|
||||||
new TestWidget(env, { p: [1] });
|
props = { p: ["string"] };
|
||||||
}).toThrow();
|
const p = new Parent();
|
||||||
|
await p.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeUndefined();
|
||||||
|
|
||||||
expect(() => {
|
try {
|
||||||
new TestWidget(env, { p: ["string", 1] });
|
props = { p: [1] };
|
||||||
}).toThrow();
|
const p = new Parent();
|
||||||
|
await p.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
|
||||||
|
error = undefined;
|
||||||
|
try {
|
||||||
|
props = { p: ["string", 1] };
|
||||||
|
const p = new Parent();
|
||||||
|
await p.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can validate an array with multiple sub element types", async () => {
|
test("can validate an array with multiple sub element types", async () => {
|
||||||
let TestWidget = class extends Widget {
|
class TestWidget extends Component<any, any> {
|
||||||
|
static template = xml`<div>hey</div>`;
|
||||||
static props = { p: { type: Array, element: [String, Boolean] } };
|
static props = { p: { type: Array, element: [String, Boolean] } };
|
||||||
};
|
}
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static template = xml`<div><TestWidget p="p"/></div>`;
|
||||||
|
static components = { TestWidget };
|
||||||
|
get p() {
|
||||||
|
return props.p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
expect(() => {
|
let error;
|
||||||
new TestWidget(env, { p: [] });
|
let props;
|
||||||
new TestWidget(env, { p: ["string"] });
|
try {
|
||||||
new TestWidget(env, { p: [false, true, "string"] });
|
props = { p: [] };
|
||||||
}).not.toThrow();
|
const p = new Parent();
|
||||||
|
await p.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeUndefined();
|
||||||
|
|
||||||
expect(() => {
|
try {
|
||||||
new TestWidget(env, { p: [true, 1] });
|
props = { p: ["string"] };
|
||||||
}).toThrow();
|
const p = new Parent();
|
||||||
|
await p.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeUndefined();
|
||||||
|
|
||||||
|
try {
|
||||||
|
props = { p: [false, true, "string"] };
|
||||||
|
const p = new Parent();
|
||||||
|
await p.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeUndefined();
|
||||||
|
|
||||||
|
try {
|
||||||
|
props = { p: [true, 1] };
|
||||||
|
const p = new Parent();
|
||||||
|
await p.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can validate an object with simple shape", async () => {
|
test("can validate an object with simple shape", async () => {
|
||||||
let TestWidget = class extends Widget {
|
class TestWidget extends Component<any, any> {
|
||||||
|
static template = xml`<div>hey</div>`;
|
||||||
static props = {
|
static props = {
|
||||||
p: { type: Object, shape: { id: Number, url: String } }
|
p: { type: Object, shape: { id: Number, url: String } }
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static template = xml`<div><TestWidget p="p"/></div>`;
|
||||||
|
static components = { TestWidget };
|
||||||
|
get p() {
|
||||||
|
return props.p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
expect(() => {
|
let error;
|
||||||
new TestWidget(env, { p: { id: 1, url: "url" } });
|
let props;
|
||||||
new TestWidget(env, { p: { id: 1, url: "url", extra: true } });
|
try {
|
||||||
}).not.toThrow();
|
props = { p: { id: 1, url: "url" } };
|
||||||
|
const p = new Parent();
|
||||||
|
await p.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeUndefined();
|
||||||
|
|
||||||
expect(() => {
|
try {
|
||||||
new TestWidget(env, { p: { id: "1", url: "url" } });
|
props = { p: { id: 1, url: "url", extra: true } };
|
||||||
}).toThrow();
|
const p = new Parent();
|
||||||
|
await p.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(error.message).toBe("Invalid prop 'p' in component TestWidget (unknown prop 'extra')");
|
||||||
|
|
||||||
expect(() => {
|
try {
|
||||||
new TestWidget(env, { p: { id: 1 } });
|
props = { p: { id: "1", url: "url" } };
|
||||||
}).toThrow();
|
const p = new Parent();
|
||||||
|
await p.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
|
||||||
|
|
||||||
|
error = undefined;
|
||||||
|
try {
|
||||||
|
props = { p: { id: 1 } };
|
||||||
|
const p = new Parent();
|
||||||
|
await p.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can validate recursively complicated prop def", async () => {
|
test("can validate recursively complicated prop def", async () => {
|
||||||
let TestWidget = class extends Widget {
|
class TestWidget extends Component<any, any> {
|
||||||
|
static template = xml`<div>hey</div>`;
|
||||||
static props = {
|
static props = {
|
||||||
p: {
|
p: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -210,16 +462,141 @@ describe("props validation", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static template = xml`<div><TestWidget p="p"/></div>`;
|
||||||
|
static components = { TestWidget };
|
||||||
|
get p() {
|
||||||
|
return props.p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let error;
|
||||||
|
let props;
|
||||||
|
try {
|
||||||
|
props = { p: { id: 1, url: true } };
|
||||||
|
const p = new Parent();
|
||||||
|
await p.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeUndefined();
|
||||||
|
|
||||||
|
try {
|
||||||
|
props = { p: { id: 1, url: [12] } };
|
||||||
|
const p = new Parent();
|
||||||
|
await p.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeUndefined();
|
||||||
|
|
||||||
|
try {
|
||||||
|
props = { p: { id: 1, url: [12, true] } };
|
||||||
|
const p = new Parent();
|
||||||
|
await p.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can validate optional attributes in nested sub props", () => {
|
||||||
|
class TestComponent extends Component<any, any> {
|
||||||
|
static props = {
|
||||||
|
myprop: {
|
||||||
|
type: Array,
|
||||||
|
element: {
|
||||||
|
type: Object,
|
||||||
|
shape: {
|
||||||
|
num: { type: Number, optional: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
let error;
|
||||||
|
try {
|
||||||
|
QWeb.utils.validateProps(TestComponent, { myprop: [{}] });
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeUndefined();
|
||||||
|
|
||||||
expect(() => {
|
try {
|
||||||
new TestWidget(env, { p: { id: 1, url: true } });
|
QWeb.utils.validateProps(TestComponent, { myprop: [{ a: 1 }] });
|
||||||
new TestWidget(env, { p: { id: 1, url: [12] } });
|
} catch (e) {
|
||||||
}).not.toThrow();
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(error.message).toBe(
|
||||||
|
"Invalid prop 'myprop' in component TestComponent (unknown prop 'a')"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
expect(() => {
|
test("can validate with a custom validator", () => {
|
||||||
new TestWidget(env, { p: { id: 1, url: [12, true] } });
|
class TestComponent extends Component<any, any> {
|
||||||
}).toThrow();
|
static props = {
|
||||||
|
size: {
|
||||||
|
validate: e => ["small", "medium", "large"].includes(e)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let error;
|
||||||
|
try {
|
||||||
|
QWeb.utils.validateProps(TestComponent, { size: "small" });
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeUndefined();
|
||||||
|
|
||||||
|
try {
|
||||||
|
QWeb.utils.validateProps(TestComponent, { size: "abcdef" });
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(error.message).toBe("Invalid Prop 'size' in component 'TestComponent'");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can validate with a custom validator, and a type", () => {
|
||||||
|
const validator = jest.fn(n => 0 <= n && n <= 10);
|
||||||
|
class TestComponent extends Component<any, any> {
|
||||||
|
static props = {
|
||||||
|
n: {
|
||||||
|
type: Number,
|
||||||
|
validate: validator
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let error;
|
||||||
|
try {
|
||||||
|
QWeb.utils.validateProps(TestComponent, { n: 3 });
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeUndefined();
|
||||||
|
expect(validator).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
try {
|
||||||
|
QWeb.utils.validateProps(TestComponent, { n: "str" });
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(error.message).toBe("Invalid Prop 'n' in component 'TestComponent'");
|
||||||
|
expect(validator).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
error = null;
|
||||||
|
try {
|
||||||
|
QWeb.utils.validateProps(TestComponent, { n: 100 });
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(error.message).toBe("Invalid Prop 'n' in component 'TestComponent'");
|
||||||
|
expect(validator).toBeCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("props are validated in dev mode (code snapshot)", async () => {
|
test("props are validated in dev mode (code snapshot)", async () => {
|
||||||
@@ -237,7 +614,7 @@ describe("props validation", () => {
|
|||||||
class App extends Widget {
|
class App extends Widget {
|
||||||
static components = { Child };
|
static components = { Child };
|
||||||
}
|
}
|
||||||
const app = new App(env);
|
const app = new App();
|
||||||
await app.mount(fixture);
|
await app.mount(fixture);
|
||||||
expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
|
expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
|
||||||
// need to make sure there are 2 call to update props. one at component
|
// need to make sure there are 2 call to update props. one at component
|
||||||
@@ -268,6 +645,32 @@ describe("props validation", () => {
|
|||||||
}).toThrow();
|
}).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("props with type array, and no element", async () => {
|
||||||
|
class TestWidget extends Widget {
|
||||||
|
static props = { myprop: { type: Array } };
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
QWeb.utils.validateProps(TestWidget, { myprop: [1] });
|
||||||
|
}).not.toThrow();
|
||||||
|
expect(() => {
|
||||||
|
QWeb.utils.validateProps(TestWidget, { myprop: 1 });
|
||||||
|
}).toThrow(`Invalid Prop 'myprop' in component 'TestWidget'`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("props with type object, and no shape", async () => {
|
||||||
|
class TestWidget extends Widget {
|
||||||
|
static props = { myprop: { type: Object } };
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
QWeb.utils.validateProps(TestWidget, { myprop: { a: 3 } });
|
||||||
|
}).not.toThrow();
|
||||||
|
expect(() => {
|
||||||
|
QWeb.utils.validateProps(TestWidget, { myprop: false });
|
||||||
|
}).toThrow(`Invalid Prop 'myprop' in component 'TestWidget'`);
|
||||||
|
});
|
||||||
|
|
||||||
test("props: extra props cause an error", async () => {
|
test("props: extra props cause an error", async () => {
|
||||||
class TestWidget extends Widget {
|
class TestWidget extends Widget {
|
||||||
static props = ["message"];
|
static props = ["message"];
|
||||||
@@ -310,33 +713,131 @@ describe("props validation", () => {
|
|||||||
QWeb.utils.validateProps(TestWidget, { message: null });
|
QWeb.utils.validateProps(TestWidget, { message: null });
|
||||||
}).toThrow();
|
}).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("missing required boolean prop causes an error", async () => {
|
||||||
|
class TestWidget extends Widget {
|
||||||
|
static props = ["p"];
|
||||||
|
static template = xml`<span><t t-if="props.p">hey</t></span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class App extends Widget {
|
||||||
|
static template = xml`<div><TestWidget/></div>`;
|
||||||
|
static components = { TestWidget };
|
||||||
|
}
|
||||||
|
|
||||||
|
const w = new App(undefined, {});
|
||||||
|
let error;
|
||||||
|
try {
|
||||||
|
await w.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(error.message).toBe("Missing props 'p' (component 'TestWidget')");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("props are validated whenever component is updated", async () => {
|
||||||
|
let error;
|
||||||
|
class TestWidget extends Component<any, any> {
|
||||||
|
static props = { p: { type: Number } };
|
||||||
|
static template = xml`<div><t t-esc="props.p"/></div>`;
|
||||||
|
|
||||||
|
async __updateProps() {
|
||||||
|
try {
|
||||||
|
await Component.prototype.__updateProps.apply(this, arguments);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static template = xml`<div><TestWidget p="state.p"/></div>`;
|
||||||
|
static components = { TestWidget };
|
||||||
|
state: any = useState({ p: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const w = new Parent();
|
||||||
|
await w.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
|
||||||
|
|
||||||
|
w.state.p = undefined;
|
||||||
|
await nextTick();
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(error.message).toBe("Missing props 'p' (component 'TestWidget')");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("default values are applied before validating props at update", async () => {
|
||||||
|
class TestWidget extends Component<any, any> {
|
||||||
|
static props = { p: { type: Number } };
|
||||||
|
static template = xml`<div><t t-esc="props.p"/></div>`;
|
||||||
|
static defaultProps = { p: 4 };
|
||||||
|
}
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static template = xml`<div><TestWidget p="state.p"/></div>`;
|
||||||
|
static components = { TestWidget };
|
||||||
|
state: any = useState({ p: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const w = new Parent();
|
||||||
|
await w.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
|
||||||
|
|
||||||
|
w.state.p = undefined;
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("<div><div>4</div></div>");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("default props", () => {
|
describe("default props", () => {
|
||||||
test("can set default values", async () => {
|
test("can set default values", async () => {
|
||||||
class TestWidget extends Widget {
|
class TestWidget extends Component<any, any> {
|
||||||
static defaultProps = { p: 4 };
|
static defaultProps = { p: 4 };
|
||||||
|
static template = xml`<div><t t-esc="props.p"/></div>`;
|
||||||
|
}
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static template = xml`<div><TestWidget /></div>`;
|
||||||
|
static components = { TestWidget };
|
||||||
}
|
}
|
||||||
|
|
||||||
const w = new TestWidget(env, {});
|
const w = new Parent();
|
||||||
expect(w.props.p).toBe(4);
|
await w.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><div>4</div></div>");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("default values are also set whenever component is updated", async () => {
|
test("default values are also set whenever component is updated", async () => {
|
||||||
class TestWidget extends Widget {
|
class TestWidget extends Widget {
|
||||||
|
static template = xml`<div><t t-esc="props.p"/></div>`;
|
||||||
static defaultProps = { p: 4 };
|
static defaultProps = { p: 4 };
|
||||||
}
|
}
|
||||||
env.qweb.addTemplates(`
|
class Parent extends Widget {
|
||||||
<templates>
|
static template = xml`<div><TestWidget p="state.p"/></div>`;
|
||||||
<div t-name="TestWidget"><t t-esc="props.p"/></div>
|
static components = { TestWidget };
|
||||||
</templates>`);
|
state: any = useState({ p: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
const w = new TestWidget(env, { p: 1 });
|
const w = new Parent();
|
||||||
await w.mount(fixture);
|
await w.mount(fixture);
|
||||||
expect(fixture.innerHTML).toMatchSnapshot();
|
expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
|
||||||
await w.__updateProps({});
|
|
||||||
await w.render();
|
w.state.p = undefined;
|
||||||
expect(w.props.p).toBe(4);
|
await nextTick();
|
||||||
expect(fixture.innerHTML).toMatchSnapshot();
|
expect(fixture.innerHTML).toBe("<div><div>4</div></div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can set default required boolean values", async () => {
|
||||||
|
class TestWidget extends Widget {
|
||||||
|
static props = ["p", "q"];
|
||||||
|
static defaultProps = { p: true, q: false };
|
||||||
|
static template = xml`<span><t t-if="props.p">hey</t><t t-if="!props.q">hey</t></span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class App extends Widget {
|
||||||
|
static template = xml`<div><TestWidget/></div>`;
|
||||||
|
static components = { TestWidget };
|
||||||
|
}
|
||||||
|
|
||||||
|
const w = new App(undefined, {});
|
||||||
|
await w.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>heyhey</span></div>");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,339 @@
|
|||||||
|
import { makeDeferred, makeTestEnv, makeTestFixture, nextTick } from "./helpers";
|
||||||
|
import { Component } from "../src/component/component";
|
||||||
|
import { Context, useContext } from "../src/context";
|
||||||
|
import { xml } from "../src/tags";
|
||||||
|
import { useState } from "../src/hooks";
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Setup and helpers
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// We create before each test:
|
||||||
|
// - fixture: a div, appended to the DOM, intended to be the target of dom
|
||||||
|
// manipulations. Note that it is removed after each test.
|
||||||
|
// - a test env, necessary to create components, that is set as env
|
||||||
|
|
||||||
|
let fixture: HTMLElement;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = makeTestFixture();
|
||||||
|
Component.env = makeTestEnv();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fixture.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Tests
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("Context", () => {
|
||||||
|
test("very simple use, with initial value", async () => {
|
||||||
|
const testContext = new Context({ value: 123 });
|
||||||
|
|
||||||
|
class Test extends Component<any, any> {
|
||||||
|
static template = xml`<div><t t-esc="contextObj.value"/></div>`;
|
||||||
|
contextObj = useContext(testContext);
|
||||||
|
}
|
||||||
|
const test = new Test();
|
||||||
|
await test.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div>123</div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("useContext hook is reactive, for one component", async () => {
|
||||||
|
const testContext = new Context({ value: 123 });
|
||||||
|
|
||||||
|
class Test extends Component<any, any> {
|
||||||
|
static template = xml`<div><t t-esc="contextObj.value"/></div>`;
|
||||||
|
contextObj = useContext(testContext);
|
||||||
|
}
|
||||||
|
const test = new Test();
|
||||||
|
await test.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div>123</div>");
|
||||||
|
test.contextObj.value = 321;
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("<div>321</div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("two components can subscribe to same context", async () => {
|
||||||
|
const testContext = new Context({ value: 123 });
|
||||||
|
|
||||||
|
class Child extends Component<any, any> {
|
||||||
|
static template = xml`<span><t t-esc="contextObj.value"/></span>`;
|
||||||
|
contextObj = useContext(testContext);
|
||||||
|
}
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static template = xml`<div><Child /><Child /></div>`;
|
||||||
|
static components = { Child };
|
||||||
|
}
|
||||||
|
const parent = new Parent();
|
||||||
|
await parent.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>123</span><span>123</span></div>");
|
||||||
|
testContext.state.value = 321;
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>321</span><span>321</span></div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("two async components are updated in parallel", async () => {
|
||||||
|
const testContext = new Context({ value: 123 });
|
||||||
|
const def = makeDeferred();
|
||||||
|
const steps: string[] = [];
|
||||||
|
|
||||||
|
class Child extends Component<any, any> {
|
||||||
|
static template = xml`<span><t t-esc="contextObj.value"/></span>`;
|
||||||
|
contextObj = useContext(testContext);
|
||||||
|
async render() {
|
||||||
|
steps.push("render");
|
||||||
|
await def;
|
||||||
|
return super.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static template = xml`<div><Child /><Child /></div>`;
|
||||||
|
static components = { Child };
|
||||||
|
}
|
||||||
|
const parent = new Parent();
|
||||||
|
await parent.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>123</span><span>123</span></div>");
|
||||||
|
testContext.state.value = 321;
|
||||||
|
await nextTick();
|
||||||
|
expect(steps).toEqual(["render", "render"]);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>123</span><span>123</span></div>");
|
||||||
|
def.resolve();
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>321</span><span>321</span></div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("two async components on two levels are updated (mostly) in parallel", async () => {
|
||||||
|
const testContext = new Context({ value: 123 });
|
||||||
|
const def = makeDeferred();
|
||||||
|
const steps: string[] = [];
|
||||||
|
|
||||||
|
class SlowComp extends Component<any, any> {
|
||||||
|
static template = xml`<p><t t-esc="props.value"/></p>`;
|
||||||
|
willUpdateProps() {
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Child extends Component<any, any> {
|
||||||
|
static template = xml`<span><SlowComp value="contextObj.value"/></span>`;
|
||||||
|
static components = { SlowComp };
|
||||||
|
contextObj = useContext(testContext);
|
||||||
|
|
||||||
|
render() {
|
||||||
|
steps.push("render");
|
||||||
|
return super.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static template = xml`<div><Child /><Child /></div>`;
|
||||||
|
static components = { Child };
|
||||||
|
}
|
||||||
|
|
||||||
|
class App extends Component<any, any> {
|
||||||
|
static template = xml`<div><Child /><Parent /></div>`;
|
||||||
|
static components = { Child, Parent };
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = new App();
|
||||||
|
await app.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe(
|
||||||
|
"<div><span><p>123</p></span><div><span><p>123</p></span><span><p>123</p></span></div></div>"
|
||||||
|
);
|
||||||
|
testContext.state.value = 321;
|
||||||
|
await nextTick();
|
||||||
|
expect(steps).toEqual(["render"]);
|
||||||
|
expect(fixture.innerHTML).toBe(
|
||||||
|
"<div><span><p>123</p></span><div><span><p>123</p></span><span><p>123</p></span></div></div>"
|
||||||
|
);
|
||||||
|
def.resolve();
|
||||||
|
await nextTick();
|
||||||
|
// we need to wait for an extra tick because it could happen (even though it
|
||||||
|
// is rare) that the second batch of renderings is not done yet, because
|
||||||
|
// the initial promise has been given to the macrotask queue, so a small
|
||||||
|
// delay happens.
|
||||||
|
await nextTick();
|
||||||
|
expect(steps).toEqual(["render", "render", "render"]);
|
||||||
|
expect(fixture.innerHTML).toBe(
|
||||||
|
"<div><span><p>321</p></span><div><span><p>321</p></span><span><p>321</p></span></div></div>"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("one components can subscribe twice to same context", async () => {
|
||||||
|
const testContext = new Context({ a: 1, b: 2 });
|
||||||
|
const steps: string[] = [];
|
||||||
|
|
||||||
|
class Child extends Component<any, any> {
|
||||||
|
static template = xml`<span><t t-esc="contextObj1.a"/><t t-esc="contextObj2.b"/></span>`;
|
||||||
|
contextObj1 = useContext(testContext);
|
||||||
|
contextObj2 = useContext(testContext);
|
||||||
|
__render(fiber) {
|
||||||
|
steps.push("child");
|
||||||
|
return super.__render(fiber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static template = xml`<div><Child /></div>`;
|
||||||
|
static components = { Child };
|
||||||
|
}
|
||||||
|
const parent = new Parent();
|
||||||
|
await parent.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>12</span></div>");
|
||||||
|
expect(steps).toEqual(["child"]);
|
||||||
|
testContext.state.a = 3;
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>32</span></div>");
|
||||||
|
expect(steps).toEqual(["child", "child"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parent and children subscribed to same context", async () => {
|
||||||
|
const testContext = new Context({ a: 123, b: 321 });
|
||||||
|
const steps: string[] = [];
|
||||||
|
|
||||||
|
class Child extends Component<any, any> {
|
||||||
|
static template = xml`<span><t t-esc="contextObj.a"/></span>`;
|
||||||
|
contextObj = useContext(testContext);
|
||||||
|
__render(fiber) {
|
||||||
|
steps.push("child");
|
||||||
|
return super.__render(fiber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static template = xml`<div><Child /><t t-esc="contextObj.b"/></div>`;
|
||||||
|
static components = { Child };
|
||||||
|
contextObj = useContext(testContext);
|
||||||
|
__render(fiber) {
|
||||||
|
steps.push("parent");
|
||||||
|
return super.__render(fiber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const parent = new Parent();
|
||||||
|
await parent.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>123</span>321</div>");
|
||||||
|
expect(steps).toEqual(["parent", "child"]);
|
||||||
|
|
||||||
|
parent.contextObj.a = 124;
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>124</span>321</div>");
|
||||||
|
|
||||||
|
// we only want one render from the child here, not two
|
||||||
|
expect(steps).toEqual(["parent", "child", "parent", "child"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("destroyed component is inactive", async () => {
|
||||||
|
const testContext = new Context({ a: 123 });
|
||||||
|
const steps: string[] = [];
|
||||||
|
|
||||||
|
class Child extends Component<any, any> {
|
||||||
|
static template = xml`<span><t t-esc="contextObj.a"/></span>`;
|
||||||
|
contextObj = useContext(testContext);
|
||||||
|
__render(fiber) {
|
||||||
|
steps.push("child");
|
||||||
|
return super.__render(fiber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static template = xml`<div><Child t-if="state.flag"/></div>`;
|
||||||
|
static components = { Child };
|
||||||
|
state = useState({ flag: true });
|
||||||
|
__render(fiber) {
|
||||||
|
steps.push("parent");
|
||||||
|
return super.__render(fiber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const parent = new Parent();
|
||||||
|
await parent.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>123</span></div>");
|
||||||
|
expect(steps).toEqual(["parent", "child"]);
|
||||||
|
expect(testContext.subscriptions.update.length).toBe(1);
|
||||||
|
|
||||||
|
parent.state.flag = false;
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("<div></div>");
|
||||||
|
expect(steps).toEqual(["parent", "child", "parent"]);
|
||||||
|
// kind of whitebox...
|
||||||
|
// we make sure we do not have any pending subscriptions to the 'update'
|
||||||
|
// event
|
||||||
|
expect(testContext.subscriptions.update.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("destroyed component before being mounted is inactive", async () => {
|
||||||
|
const testContext = new Context({ a: 123 });
|
||||||
|
|
||||||
|
class Child extends Component<any, any> {
|
||||||
|
static template = xml`<span><t t-esc="contextObj.a"/></span>`;
|
||||||
|
contextObj = useContext(testContext);
|
||||||
|
willStart() {
|
||||||
|
return makeDeferred();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static template = xml`<div><Child t-if="state.flag"/></div>`;
|
||||||
|
static components = { Child };
|
||||||
|
state = useState({ flag: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = new Parent();
|
||||||
|
const prom = parent.mount(fixture);
|
||||||
|
await nextTick(); // wait for Child to be instantiated
|
||||||
|
expect(testContext.subscriptions.update.length).toBe(1);
|
||||||
|
parent.state.flag = false;
|
||||||
|
await prom;
|
||||||
|
expect(fixture.innerHTML).toBe("<div></div>");
|
||||||
|
// kind of whitebox...
|
||||||
|
// we make sure we do not have any pending subscriptions to the 'update'
|
||||||
|
// event
|
||||||
|
expect(testContext.subscriptions.update.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("concurrent renderings", async () => {
|
||||||
|
const testContext = new Context({ x: { n: 1 }, key: "x" });
|
||||||
|
const def = makeDeferred();
|
||||||
|
let stateC;
|
||||||
|
class ComponentC extends Component<any, any> {
|
||||||
|
static template = xml`<span><t t-esc="context[props.key].n"/><t t-esc="state.x"/></span>`;
|
||||||
|
context = useContext(testContext);
|
||||||
|
state = useState({ x: "a" });
|
||||||
|
|
||||||
|
constructor(parent, props) {
|
||||||
|
super(parent, props);
|
||||||
|
stateC = this.state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class ComponentB extends Component<any, any> {
|
||||||
|
static components = { ComponentC };
|
||||||
|
static template = xml`<p><ComponentC key="props.key"/></p>`;
|
||||||
|
|
||||||
|
willUpdateProps() {
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class ComponentA extends Component<any, any> {
|
||||||
|
static components = { ComponentB };
|
||||||
|
static template = xml`<div><ComponentB key="context.key"/></div>`;
|
||||||
|
context = useContext(testContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
const component = new ComponentA();
|
||||||
|
await component.mount(fixture);
|
||||||
|
|
||||||
|
expect(fixture.innerHTML).toBe("<div><p><span>1a</span></p></div>");
|
||||||
|
testContext.state.key = "y";
|
||||||
|
testContext.state.y = { n: 2 };
|
||||||
|
delete testContext.state.x;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(fixture.innerHTML).toBe("<div><p><span>1a</span></p></div>");
|
||||||
|
stateC.x = "b";
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(fixture.innerHTML).toBe("<div><p><span>1a</span></p></div>");
|
||||||
|
def.resolve();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(fixture.innerHTML).toBe("<div><p><span>2b</span></p></div>");
|
||||||
|
});
|
||||||
|
});
|
||||||
+10
-44
@@ -8,13 +8,11 @@ describe("observer", () => {
|
|||||||
|
|
||||||
expect(typeof obj).toBe("object");
|
expect(typeof obj).toBe("object");
|
||||||
expect(observer.revNumber(obj)).toBe(1);
|
expect(observer.revNumber(obj)).toBe(1);
|
||||||
expect(observer.deepRevNumber(obj)).toBe(1);
|
|
||||||
expect(observer.rev).toBe(1);
|
expect(observer.rev).toBe(1);
|
||||||
|
|
||||||
const obj2: any = observer.observe({ a: 1 });
|
const obj2: any = observer.observe({ a: 1 });
|
||||||
expect(observer.revNumber(obj2)).toBe(1);
|
expect(observer.revNumber(obj2)).toBe(1);
|
||||||
expect(observer.revNumber(obj)).toBe(1);
|
expect(observer.revNumber(obj)).toBe(1);
|
||||||
expect(observer.deepRevNumber(obj)).toBe(1);
|
|
||||||
expect(observer.rev).toBe(1);
|
expect(observer.rev).toBe(1);
|
||||||
|
|
||||||
obj2.a = 2;
|
obj2.a = 2;
|
||||||
@@ -33,23 +31,19 @@ describe("observer", () => {
|
|||||||
const obj: any = observer.observe({ a: null, b: undefined });
|
const obj: any = observer.observe({ a: null, b: undefined });
|
||||||
|
|
||||||
expect(observer.revNumber(obj)).toBe(1);
|
expect(observer.revNumber(obj)).toBe(1);
|
||||||
expect(observer.deepRevNumber(obj)).toBe(1);
|
|
||||||
expect(observer.rev).toBe(1);
|
expect(observer.rev).toBe(1);
|
||||||
|
|
||||||
obj.a = 3;
|
obj.a = 3;
|
||||||
expect(observer.revNumber(obj)).toBe(2);
|
expect(observer.revNumber(obj)).toBe(2);
|
||||||
expect(observer.deepRevNumber(obj)).toBe(2);
|
|
||||||
expect(observer.rev).toBe(2);
|
expect(observer.rev).toBe(2);
|
||||||
|
|
||||||
obj.b = 5;
|
obj.b = 5;
|
||||||
expect(observer.revNumber(obj)).toBe(3);
|
expect(observer.revNumber(obj)).toBe(3);
|
||||||
expect(observer.deepRevNumber(obj)).toBe(3);
|
|
||||||
expect(observer.rev).toBe(3);
|
expect(observer.rev).toBe(3);
|
||||||
|
|
||||||
obj.a = null;
|
obj.a = null;
|
||||||
obj.b = undefined;
|
obj.b = undefined;
|
||||||
expect(observer.revNumber(obj)).toBe(5);
|
expect(observer.revNumber(obj)).toBe(5);
|
||||||
expect(observer.deepRevNumber(obj)).toBe(5);
|
|
||||||
expect(observer.rev).toBe(5);
|
expect(observer.rev).toBe(5);
|
||||||
expect(obj).toEqual({
|
expect(obj).toEqual({
|
||||||
a: null,
|
a: null,
|
||||||
@@ -63,7 +57,6 @@ describe("observer", () => {
|
|||||||
const obj: any = observer.observe({ date });
|
const obj: any = observer.observe({ date });
|
||||||
|
|
||||||
expect(observer.revNumber(obj)).toBe(1);
|
expect(observer.revNumber(obj)).toBe(1);
|
||||||
expect(observer.deepRevNumber(obj)).toBe(1);
|
|
||||||
expect(observer.rev).toBe(1);
|
expect(observer.rev).toBe(1);
|
||||||
expect(typeof obj.date.getFullYear()).toBe("number");
|
expect(typeof obj.date.getFullYear()).toBe("number");
|
||||||
expect(obj.date).toBe(date);
|
expect(obj.date).toBe(date);
|
||||||
@@ -71,7 +64,6 @@ describe("observer", () => {
|
|||||||
obj.date = new Date();
|
obj.date = new Date();
|
||||||
|
|
||||||
expect(observer.revNumber(obj)).toBe(2);
|
expect(observer.revNumber(obj)).toBe(2);
|
||||||
expect(observer.deepRevNumber(obj)).toBe(2);
|
|
||||||
expect(observer.rev).toBe(2);
|
expect(observer.rev).toBe(2);
|
||||||
expect(obj.date).not.toBe(date);
|
expect(obj.date).not.toBe(date);
|
||||||
});
|
});
|
||||||
@@ -82,14 +74,11 @@ describe("observer", () => {
|
|||||||
|
|
||||||
expect(Array.isArray(obj.arr)).toBe(true);
|
expect(Array.isArray(obj.arr)).toBe(true);
|
||||||
expect(observer.revNumber(obj.arr)).toBe(1);
|
expect(observer.revNumber(obj.arr)).toBe(1);
|
||||||
expect(observer.deepRevNumber(obj.arr)).toBe(1);
|
|
||||||
expect(observer.rev).toBe(1);
|
expect(observer.rev).toBe(1);
|
||||||
|
|
||||||
obj.arr[0] = "nope";
|
obj.arr[0] = "nope";
|
||||||
expect(observer.revNumber(obj.arr)).toBe(2);
|
expect(observer.revNumber(obj.arr)).toBe(2);
|
||||||
expect(observer.deepRevNumber(obj.arr)).toBe(2);
|
expect(observer.revNumber(obj)).toBe(2);
|
||||||
expect(observer.revNumber(obj)).toBe(1);
|
|
||||||
expect(observer.deepRevNumber(obj)).toBe(2);
|
|
||||||
expect(observer.rev).toBe(2);
|
expect(observer.rev).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -98,24 +87,20 @@ describe("observer", () => {
|
|||||||
const obj: any = observer.observe({ a: 1 });
|
const obj: any = observer.observe({ a: 1 });
|
||||||
|
|
||||||
expect(observer.revNumber(obj)).toBe(1);
|
expect(observer.revNumber(obj)).toBe(1);
|
||||||
expect(observer.deepRevNumber(obj)).toBe(1);
|
|
||||||
expect(observer.rev).toBe(1);
|
expect(observer.rev).toBe(1);
|
||||||
|
|
||||||
obj.a = 2;
|
obj.a = 2;
|
||||||
|
|
||||||
expect(observer.revNumber(obj)).toBe(2);
|
expect(observer.revNumber(obj)).toBe(2);
|
||||||
expect(observer.deepRevNumber(obj)).toBe(2);
|
|
||||||
expect(observer.rev).toBe(2);
|
expect(observer.rev).toBe(2);
|
||||||
|
|
||||||
// same value again
|
// same value again
|
||||||
obj.a = 2;
|
obj.a = 2;
|
||||||
expect(observer.revNumber(obj)).toBe(2);
|
expect(observer.revNumber(obj)).toBe(2);
|
||||||
expect(observer.deepRevNumber(obj)).toBe(2);
|
|
||||||
expect(observer.rev).toBe(2);
|
expect(observer.rev).toBe(2);
|
||||||
|
|
||||||
obj.a = 3;
|
obj.a = 3;
|
||||||
expect(observer.revNumber(obj)).toBe(3);
|
expect(observer.revNumber(obj)).toBe(3);
|
||||||
expect(observer.deepRevNumber(obj)).toBe(3);
|
|
||||||
expect(observer.rev).toBe(3);
|
expect(observer.rev).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -126,19 +111,16 @@ describe("observer", () => {
|
|||||||
expect(Array.isArray(arr)).toBe(true);
|
expect(Array.isArray(arr)).toBe(true);
|
||||||
expect(arr.length).toBe(0);
|
expect(arr.length).toBe(0);
|
||||||
expect(observer.revNumber(arr)).toBe(1);
|
expect(observer.revNumber(arr)).toBe(1);
|
||||||
expect(observer.deepRevNumber(arr)).toBe(1);
|
|
||||||
expect(observer.rev).toBe(1);
|
expect(observer.rev).toBe(1);
|
||||||
|
|
||||||
arr.push(1);
|
arr.push(1);
|
||||||
expect(observer.revNumber(arr)).toBe(2);
|
expect(observer.revNumber(arr)).toBe(2);
|
||||||
expect(observer.deepRevNumber(arr)).toBe(2);
|
|
||||||
expect(observer.rev).toBe(2);
|
expect(observer.rev).toBe(2);
|
||||||
expect(arr.length).toBe(1);
|
expect(arr.length).toBe(1);
|
||||||
expect(arr).toEqual([1]);
|
expect(arr).toEqual([1]);
|
||||||
|
|
||||||
arr.splice(1, 0, "hey");
|
arr.splice(1, 0, "hey");
|
||||||
expect(observer.revNumber(arr)).toBe(3);
|
expect(observer.revNumber(arr)).toBe(3);
|
||||||
expect(observer.deepRevNumber(arr)).toBe(3);
|
|
||||||
expect(observer.rev).toBe(3);
|
expect(observer.rev).toBe(3);
|
||||||
expect(arr).toEqual([1, "hey"]);
|
expect(arr).toEqual([1, "hey"]);
|
||||||
expect(arr.length).toBe(2);
|
expect(arr.length).toBe(2);
|
||||||
@@ -146,7 +128,6 @@ describe("observer", () => {
|
|||||||
arr.unshift("lindemans");
|
arr.unshift("lindemans");
|
||||||
//it generates 3 primitive operations
|
//it generates 3 primitive operations
|
||||||
expect(observer.revNumber(arr)).toBe(6);
|
expect(observer.revNumber(arr)).toBe(6);
|
||||||
expect(observer.deepRevNumber(arr)).toBe(6);
|
|
||||||
expect(observer.rev).toBe(6);
|
expect(observer.rev).toBe(6);
|
||||||
expect(arr).toEqual(["lindemans", 1, "hey"]);
|
expect(arr).toEqual(["lindemans", 1, "hey"]);
|
||||||
expect(arr.length).toBe(3);
|
expect(arr.length).toBe(3);
|
||||||
@@ -154,21 +135,18 @@ describe("observer", () => {
|
|||||||
arr.reverse();
|
arr.reverse();
|
||||||
//it generates 2 primitive operations
|
//it generates 2 primitive operations
|
||||||
expect(observer.revNumber(arr)).toBe(8);
|
expect(observer.revNumber(arr)).toBe(8);
|
||||||
expect(observer.deepRevNumber(arr)).toBe(8);
|
|
||||||
expect(observer.rev).toBe(8);
|
expect(observer.rev).toBe(8);
|
||||||
expect(arr).toEqual(["hey", 1, "lindemans"]);
|
expect(arr).toEqual(["hey", 1, "lindemans"]);
|
||||||
expect(arr.length).toBe(3);
|
expect(arr.length).toBe(3);
|
||||||
|
|
||||||
arr.pop(); // one set, one delete
|
arr.pop(); // one set, one delete
|
||||||
expect(observer.revNumber(arr)).toBe(10);
|
expect(observer.revNumber(arr)).toBe(10);
|
||||||
expect(observer.deepRevNumber(arr)).toBe(10);
|
|
||||||
expect(observer.rev).toBe(10);
|
expect(observer.rev).toBe(10);
|
||||||
expect(arr).toEqual(["hey", 1]);
|
expect(arr).toEqual(["hey", 1]);
|
||||||
expect(arr.length).toBe(2);
|
expect(arr.length).toBe(2);
|
||||||
|
|
||||||
arr.shift(); // 2 sets, 1 delete
|
arr.shift(); // 2 sets, 1 delete
|
||||||
expect(observer.revNumber(arr)).toBe(13);
|
expect(observer.revNumber(arr)).toBe(13);
|
||||||
expect(observer.deepRevNumber(arr)).toBe(13);
|
|
||||||
expect(observer.rev).toBe(13);
|
expect(observer.rev).toBe(13);
|
||||||
expect(arr).toEqual([1]);
|
expect(arr).toEqual([1]);
|
||||||
expect(arr.length).toBe(1);
|
expect(arr.length).toBe(1);
|
||||||
@@ -187,8 +165,7 @@ describe("observer", () => {
|
|||||||
arr[0].kriek = 6;
|
arr[0].kriek = 6;
|
||||||
|
|
||||||
expect(observer.rev).toBe(3);
|
expect(observer.rev).toBe(3);
|
||||||
expect(observer.revNumber(arr)).toBe(2);
|
expect(observer.revNumber(arr)).toBe(3);
|
||||||
expect(observer.deepRevNumber(arr)).toBe(3);
|
|
||||||
expect(observer.revNumber(arr[0])).toBe(3);
|
expect(observer.revNumber(arr[0])).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -238,7 +215,6 @@ describe("observer", () => {
|
|||||||
|
|
||||||
expect(observer.rev).toBe(1);
|
expect(observer.rev).toBe(1);
|
||||||
expect(observer.revNumber(state)).toBe(1);
|
expect(observer.revNumber(state)).toBe(1);
|
||||||
expect(observer.deepRevNumber(state)).toBe(1);
|
|
||||||
expect(observer.notifyCB).toBeCalledTimes(0);
|
expect(observer.notifyCB).toBeCalledTimes(0);
|
||||||
|
|
||||||
state[1] = "b";
|
state[1] = "b";
|
||||||
@@ -247,7 +223,6 @@ describe("observer", () => {
|
|||||||
|
|
||||||
expect(observer.rev).toBe(2);
|
expect(observer.rev).toBe(2);
|
||||||
expect(observer.revNumber(state)).toBe(2);
|
expect(observer.revNumber(state)).toBe(2);
|
||||||
expect(observer.deepRevNumber(state)).toBe(2);
|
|
||||||
expect(observer.notifyCB).toBeCalledTimes(1);
|
expect(observer.notifyCB).toBeCalledTimes(1);
|
||||||
|
|
||||||
expect(state).toEqual(["a", "b"]);
|
expect(state).toEqual(["a", "b"]);
|
||||||
@@ -259,13 +234,11 @@ describe("observer", () => {
|
|||||||
|
|
||||||
expect(observer.rev).toBe(1);
|
expect(observer.rev).toBe(1);
|
||||||
expect(observer.revNumber(state.arr)).toBe(1);
|
expect(observer.revNumber(state.arr)).toBe(1);
|
||||||
expect(observer.deepRevNumber(state.arr)).toBe(1);
|
|
||||||
expect(state.arr.length).toBe(0);
|
expect(state.arr.length).toBe(0);
|
||||||
|
|
||||||
state.arr.push(1);
|
state.arr.push(1);
|
||||||
expect(observer.rev).toBe(2);
|
expect(observer.rev).toBe(2);
|
||||||
expect(observer.revNumber(state.arr)).toBe(2);
|
expect(observer.revNumber(state.arr)).toBe(2);
|
||||||
expect(observer.deepRevNumber(state.arr)).toBe(2);
|
|
||||||
expect(state.arr.length).toBe(1);
|
expect(state.arr.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -280,7 +253,7 @@ describe("observer", () => {
|
|||||||
|
|
||||||
state.arr[0].something = 2;
|
state.arr[0].something = 2;
|
||||||
expect(observer.rev).toBe(2);
|
expect(observer.rev).toBe(2);
|
||||||
expect(observer.revNumber(state.arr)).toBe(1);
|
expect(observer.revNumber(state.arr)).toBe(2);
|
||||||
expect(observer.revNumber(state.arr[0])).toBe(2);
|
expect(observer.revNumber(state.arr[0])).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -294,7 +267,7 @@ describe("observer", () => {
|
|||||||
|
|
||||||
state.a.b = 2;
|
state.a.b = 2;
|
||||||
expect(observer.rev).toBe(2);
|
expect(observer.rev).toBe(2);
|
||||||
expect(observer.revNumber(state)).toBe(1);
|
expect(observer.revNumber(state)).toBe(2);
|
||||||
expect(observer.revNumber(state.a)).toBe(2);
|
expect(observer.revNumber(state.a)).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -312,7 +285,7 @@ describe("observer", () => {
|
|||||||
expect(observer.revNumber(obj.a)).toBe(2);
|
expect(observer.revNumber(obj.a)).toBe(2);
|
||||||
obj.a.b = 3;
|
obj.a.b = 3;
|
||||||
expect(observer.rev).toBe(3);
|
expect(observer.rev).toBe(3);
|
||||||
expect(observer.revNumber(obj)).toBe(2);
|
expect(observer.revNumber(obj)).toBe(3);
|
||||||
expect(observer.revNumber(obj.a)).toBe(3);
|
expect(observer.revNumber(obj.a)).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -320,22 +293,18 @@ describe("observer", () => {
|
|||||||
const observer = new Observer();
|
const observer = new Observer();
|
||||||
const state: any = observer.observe({ o: { a: 1 }, arr: [1], n: 13 });
|
const state: any = observer.observe({ o: { a: 1 }, arr: [1], n: 13 });
|
||||||
expect(observer.revNumber(state)).toBe(1);
|
expect(observer.revNumber(state)).toBe(1);
|
||||||
expect(observer.deepRevNumber(state)).toBe(1);
|
|
||||||
|
|
||||||
state.o.a = 2;
|
state.o.a = 2;
|
||||||
expect(observer.rev).toBe(2);
|
expect(observer.rev).toBe(2);
|
||||||
expect(observer.revNumber(state)).toBe(1);
|
expect(observer.revNumber(state)).toBe(2);
|
||||||
expect(observer.deepRevNumber(state)).toBe(2);
|
|
||||||
|
|
||||||
state.arr.push(2);
|
state.arr.push(2);
|
||||||
expect(observer.rev).toBe(3);
|
expect(observer.rev).toBe(3);
|
||||||
expect(observer.revNumber(state)).toBe(1);
|
expect(observer.revNumber(state)).toBe(3);
|
||||||
expect(observer.deepRevNumber(state)).toBe(3);
|
|
||||||
|
|
||||||
state.n = 155;
|
state.n = 155;
|
||||||
expect(observer.rev).toBe(4);
|
expect(observer.rev).toBe(4);
|
||||||
expect(observer.revNumber(state)).toBe(2);
|
expect(observer.revNumber(state)).toBe(4);
|
||||||
expect(observer.deepRevNumber(state)).toBe(4);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("properly handle already observed state", () => {
|
test("properly handle already observed state", () => {
|
||||||
@@ -361,18 +330,15 @@ describe("observer", () => {
|
|||||||
const obj: any = observer.observe({});
|
const obj: any = observer.observe({});
|
||||||
|
|
||||||
expect(observer.revNumber(obj)).toBe(1);
|
expect(observer.revNumber(obj)).toBe(1);
|
||||||
expect(observer.deepRevNumber(obj)).toBe(1);
|
|
||||||
expect(observer.rev).toBe(1);
|
expect(observer.rev).toBe(1);
|
||||||
|
|
||||||
obj.aku = "always finds annoying problems";
|
obj.aku = "always finds annoying problems";
|
||||||
expect(observer.revNumber(obj)).toBe(2);
|
expect(observer.revNumber(obj)).toBe(2);
|
||||||
expect(observer.deepRevNumber(obj)).toBe(2);
|
|
||||||
expect(observer.rev).toBe(2);
|
expect(observer.rev).toBe(2);
|
||||||
|
|
||||||
obj.aku = "always finds good problems";
|
obj.aku = "always finds good problems";
|
||||||
|
|
||||||
expect(observer.revNumber(obj)).toBe(3);
|
expect(observer.revNumber(obj)).toBe(3);
|
||||||
expect(observer.deepRevNumber(obj)).toBe(3);
|
|
||||||
expect(observer.rev).toBe(3);
|
expect(observer.rev).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -416,7 +382,7 @@ describe("observer", () => {
|
|||||||
expect(observer.revNumber(obj2)).toBe(1);
|
expect(observer.revNumber(obj2)).toBe(1);
|
||||||
|
|
||||||
obj2.key = 3;
|
obj2.key = 3;
|
||||||
expect(observer.revNumber(obj1)).toBe(1);
|
expect(observer.revNumber(obj1)).toBe(2);
|
||||||
expect(observer.revNumber(obj2)).toBe(2);
|
expect(observer.revNumber(obj2)).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -442,7 +408,7 @@ describe("observer", () => {
|
|||||||
obj.a = 111;
|
obj.a = 111;
|
||||||
obj.f = 222;
|
obj.f = 222;
|
||||||
await nextMicroTick();
|
await nextMicroTick();
|
||||||
expect(observer.notifyCB).toBeCalledTimes(4);
|
expect(observer.notifyCB).toBeCalledTimes(5);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("throw error when state is mutated in object if allowMutation=false", async () => {
|
test("throw error when state is mutated in object if allowMutation=false", async () => {
|
||||||
|
|||||||
@@ -1,126 +0,0 @@
|
|||||||
/**
|
|
||||||
* Doc Link Checker
|
|
||||||
*
|
|
||||||
* We define here a test to make sure that there are no dead link in the Owl
|
|
||||||
* documentation.
|
|
||||||
*/
|
|
||||||
import * as fs from "fs";
|
|
||||||
|
|
||||||
const LINK_REGEXP = /\[([^\[]+)\]\(([^\)]+)\)/g;
|
|
||||||
const HEADING_REGEXP = /\n(#+\s*)(.*)/g;
|
|
||||||
|
|
||||||
// files to be checked
|
|
||||||
function getFiles(): string[] {
|
|
||||||
const DOCFILES = fs.readdirSync("doc").map(f => `doc/${f}`);
|
|
||||||
const MAINREADME = "README.md";
|
|
||||||
return DOCFILES.concat(MAINREADME);
|
|
||||||
}
|
|
||||||
|
|
||||||
test("All markdown links work", () => {
|
|
||||||
let linkNumber = 0;
|
|
||||||
let invalidLinkNumber = 0;
|
|
||||||
const files = getFiles();
|
|
||||||
const data = readDocData(files);
|
|
||||||
for (let file of data) {
|
|
||||||
for (let link of file.links) {
|
|
||||||
// DEBUG: uncomment next line
|
|
||||||
// console.warn(`Checking "${link.name}" in "${file.name}"`);
|
|
||||||
linkNumber++;
|
|
||||||
if (!isLinkValid(link, file, data)) {
|
|
||||||
console.warn(`Invalid Link: "${link.name}" in "${file.name}"`);
|
|
||||||
invalidLinkNumber++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
expect(invalidLinkNumber).toBe(0);
|
|
||||||
expect(linkNumber).toBeGreaterThan(10);
|
|
||||||
});
|
|
||||||
|
|
||||||
interface MarkDownLink {
|
|
||||||
name: string;
|
|
||||||
link: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MarkDownSection {
|
|
||||||
name: string;
|
|
||||||
slug: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FileData {
|
|
||||||
name: string;
|
|
||||||
links: MarkDownLink[];
|
|
||||||
sections: MarkDownSection[];
|
|
||||||
}
|
|
||||||
|
|
||||||
function isLinkValid(link: MarkDownLink, current: FileData, files: FileData[]): boolean {
|
|
||||||
const parts = link.link.split("#");
|
|
||||||
const currentParts = current.name.split("/");
|
|
||||||
const path = currentParts.length > 1 ? currentParts[0] + "/" : "";
|
|
||||||
const fullName = path + parts[0];
|
|
||||||
if (parts.length === 1) {
|
|
||||||
// no # in url
|
|
||||||
if (parts[0].endsWith(".md")) {
|
|
||||||
// it is a local md file
|
|
||||||
if (!files.find(f => f.name === fullName)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const file = parts[0] === "" ? current : files.find(f => f.name === fullName);
|
|
||||||
if (!file) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!file.sections.find(s => s.slug === parts[1])) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// adapted from https://medium.com/@mhagemann/the-ultimate-way-to-slugify-a-url-string-in-javascript-b8e4a0d849e1
|
|
||||||
function slugify(str) {
|
|
||||||
const a = "àáäâãåăæçèéëêǵḧìíïîḿńǹñòóöôœøṕŕßśșțùúüûǘẃẍÿź·_,:;";
|
|
||||||
const b = "aaaaaaaaceeeeghiiiimnnnooooooprssstuuuuuwxyz-----";
|
|
||||||
const p = new RegExp(a.split("").join("|"), "g");
|
|
||||||
return str
|
|
||||||
.toString()
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/\//g, "") // remove /
|
|
||||||
.replace(/\s+/g, "-") // Replace spaces with -
|
|
||||||
.replace(p, c => b.charAt(a.indexOf(c))) // Replace special characters
|
|
||||||
.replace(/&/g, "-and-") // Replace & with ‘and’
|
|
||||||
.replace(/[^\w\-]+/g, "") // Remove all non-word characters
|
|
||||||
.replace(/\-\-+/g, "-") // Replace multiple - with single -
|
|
||||||
.replace(/^-+/, "") // Trim - from start of text
|
|
||||||
.replace(/-+$/, ""); // Trim - from end of text
|
|
||||||
}
|
|
||||||
|
|
||||||
function readDocData(files: string[]): FileData[] {
|
|
||||||
const result: FileData[] = [];
|
|
||||||
|
|
||||||
for (let file of files) {
|
|
||||||
const fileData: FileData = {
|
|
||||||
name: file,
|
|
||||||
links: [],
|
|
||||||
sections: []
|
|
||||||
};
|
|
||||||
const content = fs.readFileSync(file, { encoding: "utf8" });
|
|
||||||
let m;
|
|
||||||
// get links info
|
|
||||||
do {
|
|
||||||
m = LINK_REGEXP.exec(content);
|
|
||||||
if (m) {
|
|
||||||
fileData.links.push({ name: m[0], link: m[2] });
|
|
||||||
}
|
|
||||||
} while (m);
|
|
||||||
// get sections info
|
|
||||||
do {
|
|
||||||
m = HEADING_REGEXP.exec(content);
|
|
||||||
if (m) {
|
|
||||||
fileData.sections.push({ name: m[0], slug: slugify(m[2]) });
|
|
||||||
}
|
|
||||||
} while (m);
|
|
||||||
|
|
||||||
result.push(fileData);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
+21
-5
@@ -1,10 +1,18 @@
|
|||||||
import { Env } from "../src/component/component";
|
import { Env } from "../src/component/component";
|
||||||
|
import { scheduler } from "../src/component/scheduler";
|
||||||
import { EvalContext, QWeb } from "../src/qweb/qweb";
|
import { EvalContext, QWeb } from "../src/qweb/qweb";
|
||||||
|
import { CompilationContext } from "../src/qweb/compilation_context";
|
||||||
import { patch } from "../src/vdom";
|
import { patch } from "../src/vdom";
|
||||||
import "../src/qweb/base_directives";
|
import "../src/qweb/base_directives";
|
||||||
import "../src/qweb/extensions";
|
import "../src/qweb/extensions";
|
||||||
import "../src/component/directive";
|
import "../src/component/directive";
|
||||||
|
|
||||||
|
// modifies scheduler to make it faster to test components
|
||||||
|
scheduler.requestAnimationFrame = function(callback: FrameRequestCallback) {
|
||||||
|
setTimeout(callback, 1);
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
// Some static cleanup
|
// Some static cleanup
|
||||||
let nextSlotId;
|
let nextSlotId;
|
||||||
let slots;
|
let slots;
|
||||||
@@ -13,6 +21,7 @@ let TEMPLATES;
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
nextSlotId = QWeb.nextSlotId;
|
nextSlotId = QWeb.nextSlotId;
|
||||||
|
CompilationContext.nextID = 1;
|
||||||
slots = Object.assign({}, QWeb.slots);
|
slots = Object.assign({}, QWeb.slots);
|
||||||
nextId = QWeb.nextId;
|
nextId = QWeb.nextId;
|
||||||
TEMPLATES = Object.assign({}, QWeb.TEMPLATES);
|
TEMPLATES = Object.assign({}, QWeb.TEMPLATES);
|
||||||
@@ -30,8 +39,10 @@ export function nextMicroTick(): Promise<void> {
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function nextTick(): Promise<void> {
|
export async function nextTick(): Promise<void> {
|
||||||
return new Promise(resolve => setTimeout(resolve));
|
return new Promise(function(resolve) {
|
||||||
|
setTimeout(() => scheduler.requestAnimationFrame(() => resolve()));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeTestFixture() {
|
export function makeTestFixture() {
|
||||||
@@ -99,10 +110,15 @@ export function renderToDOM(
|
|||||||
* Note that the result of renderToString is guaranteed to be the same as the
|
* Note that the result of renderToString is guaranteed to be the same as the
|
||||||
* one from QWeb.
|
* one from QWeb.
|
||||||
*/
|
*/
|
||||||
export function renderToString(qweb: QWeb, t: string, context: EvalContext = {}): string {
|
export function renderToString(
|
||||||
const node = renderToDOM(qweb, t, context);
|
qweb: QWeb,
|
||||||
|
t: string,
|
||||||
|
context: EvalContext = {},
|
||||||
|
extra?: any
|
||||||
|
): string {
|
||||||
|
const node = renderToDOM(qweb, t, context, extra);
|
||||||
const result = node instanceof Text ? node.textContent! : node.outerHTML;
|
const result = node instanceof Text ? node.textContent! : node.outerHTML;
|
||||||
if (result !== qweb.renderToString(t, context)) {
|
if (result !== qweb.renderToString(t, context, extra)) {
|
||||||
throw new Error("HTML string returned by renderToString helper does not match QWeb render");
|
throw new Error("HTML string returned by renderToString helper does not match QWeb render");
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -0,0 +1,595 @@
|
|||||||
|
import { makeTestEnv, makeTestFixture, nextTick } from "./helpers";
|
||||||
|
import { Component, Env } from "../src/component/component";
|
||||||
|
import {
|
||||||
|
useState,
|
||||||
|
onMounted,
|
||||||
|
onWillUnmount,
|
||||||
|
useRef,
|
||||||
|
onPatched,
|
||||||
|
onWillPatch,
|
||||||
|
onWillStart,
|
||||||
|
onWillUpdateProps,
|
||||||
|
useSubEnv
|
||||||
|
} from "../src/hooks";
|
||||||
|
import { xml } from "../src/tags";
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Setup and helpers
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// We create before each test:
|
||||||
|
// - fixture: a div, appended to the DOM, intended to be the target of dom
|
||||||
|
// manipulations. Note that it is removed after each test.
|
||||||
|
// - env: a WEnv, necessary to create new components
|
||||||
|
|
||||||
|
let fixture: HTMLElement;
|
||||||
|
let env: Env;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = makeTestFixture();
|
||||||
|
env = makeTestEnv();
|
||||||
|
Component.env = env;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fixture.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Tests
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("hooks", () => {
|
||||||
|
test("can use a state hook", async () => {
|
||||||
|
class Counter extends Component<any, any> {
|
||||||
|
static template = xml`<div><t t-esc="counter.value"/></div>`;
|
||||||
|
counter = useState({ value: 42 });
|
||||||
|
}
|
||||||
|
const counter = new Counter();
|
||||||
|
await counter.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div>42</div>");
|
||||||
|
counter.counter.value = 3;
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("<div>3</div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can use onMounted, onWillUnmount", async () => {
|
||||||
|
const steps: string[] = [];
|
||||||
|
function useMyHook() {
|
||||||
|
onMounted(() => {
|
||||||
|
steps.push("mounted");
|
||||||
|
});
|
||||||
|
onWillUnmount(() => {
|
||||||
|
steps.push("willunmount");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
class MyComponent extends Component<any, any> {
|
||||||
|
static template = xml`<div>hey</div>`;
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
useMyHook();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const component = new MyComponent();
|
||||||
|
await component.mount(fixture);
|
||||||
|
expect(component).not.toHaveProperty("mounted");
|
||||||
|
expect(component).not.toHaveProperty("willUnmount");
|
||||||
|
expect(fixture.innerHTML).toBe("<div>hey</div>");
|
||||||
|
expect(steps).toEqual(["mounted"]);
|
||||||
|
component.unmount();
|
||||||
|
expect(fixture.innerHTML).toBe("");
|
||||||
|
expect(steps).toEqual(["mounted", "willunmount"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can use onMounted, onWillUnmount, part 2", async () => {
|
||||||
|
const steps: string[] = [];
|
||||||
|
function useMyHook() {
|
||||||
|
onMounted(() => {
|
||||||
|
steps.push("mounted");
|
||||||
|
});
|
||||||
|
onWillUnmount(() => {
|
||||||
|
steps.push("willunmount");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
class MyComponent extends Component<any, any> {
|
||||||
|
static template = xml`<div>hey</div>`;
|
||||||
|
constructor(parent, props) {
|
||||||
|
super(parent, props);
|
||||||
|
useMyHook();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static template = xml`<div><MyComponent t-if="state.flag"/></div>`;
|
||||||
|
static components = { MyComponent };
|
||||||
|
state = useState({ flag: true });
|
||||||
|
}
|
||||||
|
const parent = new Parent();
|
||||||
|
await parent.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><div>hey</div></div>");
|
||||||
|
expect(steps).toEqual(["mounted"]);
|
||||||
|
|
||||||
|
parent.state.flag = false;
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("<div></div>");
|
||||||
|
expect(steps).toEqual(["mounted", "willunmount"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("mounted, willUnmount, onMounted, onWillUnmount order", async () => {
|
||||||
|
const steps: string[] = [];
|
||||||
|
function useMyHook() {
|
||||||
|
onMounted(() => {
|
||||||
|
steps.push("hook:mounted");
|
||||||
|
});
|
||||||
|
onWillUnmount(() => {
|
||||||
|
steps.push("hook:willunmount");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
class MyComponent extends Component<any, any> {
|
||||||
|
static template = xml`<div>hey</div>`;
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
useMyHook();
|
||||||
|
}
|
||||||
|
mounted() {
|
||||||
|
steps.push("comp:mounted");
|
||||||
|
}
|
||||||
|
willUnmount() {
|
||||||
|
steps.push("comp:willunmount");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const component = new MyComponent();
|
||||||
|
await component.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div>hey</div>");
|
||||||
|
component.unmount();
|
||||||
|
expect(fixture.innerHTML).toBe("");
|
||||||
|
expect(steps).toEqual(["comp:mounted", "hook:mounted", "hook:willunmount", "comp:willunmount"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("mounted, willUnmount, onMounted, onWillUnmount order, part 2", async () => {
|
||||||
|
const steps: string[] = [];
|
||||||
|
function useMyHook() {
|
||||||
|
onMounted(() => {
|
||||||
|
steps.push("hook:mounted");
|
||||||
|
});
|
||||||
|
onWillUnmount(() => {
|
||||||
|
steps.push("hook:willunmount");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
class MyComponent extends Component<any, any> {
|
||||||
|
static template = xml`<div>hey</div>`;
|
||||||
|
constructor(parent, props) {
|
||||||
|
super(parent, props);
|
||||||
|
useMyHook();
|
||||||
|
}
|
||||||
|
mounted() {
|
||||||
|
steps.push("comp:mounted");
|
||||||
|
}
|
||||||
|
willUnmount() {
|
||||||
|
steps.push("comp:willunmount");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static template = xml`<div><MyComponent t-if="state.flag"/></div>`;
|
||||||
|
static components = { MyComponent };
|
||||||
|
state = useState({ flag: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = new Parent();
|
||||||
|
await parent.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><div>hey</div></div>");
|
||||||
|
parent.state.flag = false;
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("<div></div>");
|
||||||
|
|
||||||
|
expect(steps).toEqual(["comp:mounted", "hook:mounted", "hook:willunmount", "comp:willunmount"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("two different call to mounted/willunmount should work", async () => {
|
||||||
|
const steps: string[] = [];
|
||||||
|
function useMyHook(i) {
|
||||||
|
onMounted(() => {
|
||||||
|
steps.push("hook:mounted" + i);
|
||||||
|
});
|
||||||
|
onWillUnmount(() => {
|
||||||
|
steps.push("hook:willunmount" + i);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
class MyComponent extends Component<any, any> {
|
||||||
|
static template = xml`<div>hey</div>`;
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
useMyHook(1);
|
||||||
|
useMyHook(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const component = new MyComponent();
|
||||||
|
await component.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div>hey</div>");
|
||||||
|
component.unmount();
|
||||||
|
expect(fixture.innerHTML).toBe("");
|
||||||
|
expect(steps).toEqual([
|
||||||
|
"hook:mounted1",
|
||||||
|
"hook:mounted2",
|
||||||
|
"hook:willunmount2",
|
||||||
|
"hook:willunmount1"
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("useRef hook", async () => {
|
||||||
|
class Counter extends Component<any, any> {
|
||||||
|
static template = xml`<div><button t-ref="button"><t t-esc="value"/></button></div>`;
|
||||||
|
button = useRef("button");
|
||||||
|
value = 0;
|
||||||
|
increment() {
|
||||||
|
this.value++;
|
||||||
|
(this.button.el as HTMLButtonElement).innerHTML = String(this.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const counter = new Counter();
|
||||||
|
expect(counter.button.el).toBe(null);
|
||||||
|
await counter.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><button>0</button></div>");
|
||||||
|
expect(counter.button.el).not.toBe(null);
|
||||||
|
expect(counter.button.el).toBe(fixture.querySelector("button"));
|
||||||
|
counter.increment();
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("<div><button>1</button></div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("useRef hook is null if ref is removed ", async () => {
|
||||||
|
expect.assertions(4);
|
||||||
|
class TestRef extends Component<any, any> {
|
||||||
|
static template = xml`<div><span t-if="state.flag" t-ref="span">owl</span></div>`;
|
||||||
|
spanRef = useRef("span");
|
||||||
|
state = useState({ flag: true });
|
||||||
|
willPatch() {
|
||||||
|
expect(this.spanRef.el).not.toBeNull();
|
||||||
|
}
|
||||||
|
patched() {
|
||||||
|
expect(this.spanRef.el).toBeNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const component = new TestRef();
|
||||||
|
await component.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>owl</span></div>");
|
||||||
|
component.state.flag = false;
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("<div></div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("t-refs on widget are components", async () => {
|
||||||
|
class WidgetB extends Component<any, any> {
|
||||||
|
static template = xml`<div>b</div>`;
|
||||||
|
}
|
||||||
|
class WidgetC extends Component<any, any> {
|
||||||
|
static template = xml`<div class="outer-div">Hello<WidgetB t-ref="mywidgetb" /></div>`;
|
||||||
|
static components = { WidgetB };
|
||||||
|
ref = useRef("mywidgetb");
|
||||||
|
}
|
||||||
|
|
||||||
|
const widget = new WidgetC();
|
||||||
|
expect(widget.ref.comp).toBe(null);
|
||||||
|
expect(widget.ref.el).toBe(null);
|
||||||
|
await widget.mount(fixture);
|
||||||
|
expect(widget.ref.comp).toBeInstanceOf(WidgetB);
|
||||||
|
expect(widget.ref.el).toEqual(fixture.querySelector(".outer-div > div"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("t-refs are bound at proper timing", async () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
class Widget extends Component<any, any> {
|
||||||
|
static template = xml`<div>widget</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ParentWidget extends Component<any, any> {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<t t-foreach="state.list" t-as="elem" t-ref="child" t-key="elem" t-component="Widget"/>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
static components = { Widget };
|
||||||
|
state = useState({ list: <any>[] });
|
||||||
|
child = useRef("child");
|
||||||
|
willPatch() {
|
||||||
|
expect(this.child.comp).toBeNull();
|
||||||
|
}
|
||||||
|
patched() {
|
||||||
|
expect(this.child.comp).not.toBeNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = new ParentWidget();
|
||||||
|
await parent.mount(fixture);
|
||||||
|
parent.state.list.push(1);
|
||||||
|
await nextTick();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("t-refs are bound at proper timing (2)", async () => {
|
||||||
|
expect.assertions(10);
|
||||||
|
class Widget extends Component<any, any> {
|
||||||
|
static template = xml`<div>widget</div>`;
|
||||||
|
}
|
||||||
|
class ParentWidget extends Component<any, any> {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<t t-if="state.child1" t-ref="child1" t-component="Widget"/>
|
||||||
|
<t t-if="state.child2" t-ref="child2" t-component="Widget"/>
|
||||||
|
</div>`;
|
||||||
|
static components = { Widget };
|
||||||
|
state = useState({ child1: true, child2: false });
|
||||||
|
child1 = useRef("child1");
|
||||||
|
child2 = useRef("child2");
|
||||||
|
count = 0;
|
||||||
|
mounted() {
|
||||||
|
expect(this.child1.comp).toBeDefined();
|
||||||
|
expect(this.child2.comp).toBeNull();
|
||||||
|
}
|
||||||
|
willPatch() {
|
||||||
|
if (this.count === 0) {
|
||||||
|
expect(this.child1.comp).toBeDefined();
|
||||||
|
expect(this.child2.comp).toBeNull();
|
||||||
|
}
|
||||||
|
if (this.count === 1) {
|
||||||
|
expect(this.child1.comp).toBeDefined();
|
||||||
|
expect(this.child2.comp).toBeDefined();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
patched() {
|
||||||
|
if (this.count === 0) {
|
||||||
|
expect(this.child1.comp).toBeDefined();
|
||||||
|
expect(this.child2.comp).toBeDefined();
|
||||||
|
}
|
||||||
|
if (this.count === 1) {
|
||||||
|
expect(this.child1.comp).toBeNull();
|
||||||
|
expect(this.child2.comp).toBeDefined();
|
||||||
|
}
|
||||||
|
this.count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = new ParentWidget();
|
||||||
|
await parent.mount(fixture);
|
||||||
|
parent.state.child2 = true;
|
||||||
|
await nextTick();
|
||||||
|
parent.state.child1 = false;
|
||||||
|
await nextTick();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can use onPatched, onWillPatch", async () => {
|
||||||
|
const steps: string[] = [];
|
||||||
|
function useMyHook() {
|
||||||
|
onWillPatch(() => {
|
||||||
|
steps.push("willPatch");
|
||||||
|
});
|
||||||
|
onPatched(() => {
|
||||||
|
steps.push("patched");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyComponent extends Component<any, any> {
|
||||||
|
static template = xml`<div><t t-if="state.flag">hey</t></div>`;
|
||||||
|
state = useState({ flag: true });
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
useMyHook();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const component = new MyComponent();
|
||||||
|
await component.mount(fixture);
|
||||||
|
expect(component).not.toHaveProperty("patched");
|
||||||
|
expect(component).not.toHaveProperty("willPatch");
|
||||||
|
expect(steps).toEqual([]);
|
||||||
|
|
||||||
|
expect(fixture.innerHTML).toBe("<div>hey</div>");
|
||||||
|
component.state.flag = false;
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("<div></div>");
|
||||||
|
|
||||||
|
expect(steps).toEqual(["willPatch", "patched"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("patched, willPatch, onPatched, onWillPatch order", async () => {
|
||||||
|
const steps: string[] = [];
|
||||||
|
function useMyHook() {
|
||||||
|
onPatched(() => {
|
||||||
|
steps.push("hook:patched");
|
||||||
|
});
|
||||||
|
onWillPatch(() => {
|
||||||
|
steps.push("hook:willPatch");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
class MyComponent extends Component<any, any> {
|
||||||
|
static template = xml`<div><t t-if="state.flag">hey</t></div>`;
|
||||||
|
state = useState({ flag: true });
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
useMyHook();
|
||||||
|
}
|
||||||
|
willPatch() {
|
||||||
|
steps.push("comp:willPatch");
|
||||||
|
}
|
||||||
|
patched() {
|
||||||
|
steps.push("comp:patched");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const component = new MyComponent();
|
||||||
|
await component.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div>hey</div>");
|
||||||
|
component.state.flag = false;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(steps).toEqual(["hook:willPatch", "comp:willPatch", "comp:patched", "hook:patched"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("two different call to willPatch/patched should work", async () => {
|
||||||
|
const steps: string[] = [];
|
||||||
|
function useMyHook(i) {
|
||||||
|
onPatched(() => {
|
||||||
|
steps.push("hook:patched" + i);
|
||||||
|
});
|
||||||
|
onWillPatch(() => {
|
||||||
|
steps.push("hook:willPatch" + i);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
class MyComponent extends Component<any, any> {
|
||||||
|
static template = xml`<div>hey<t t-esc="state.value"/></div>`;
|
||||||
|
state = useState({ value: 1 });
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
useMyHook(1);
|
||||||
|
useMyHook(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const component = new MyComponent();
|
||||||
|
await component.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div>hey1</div>");
|
||||||
|
component.state.value++;
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("<div>hey2</div>");
|
||||||
|
|
||||||
|
expect(steps).toEqual(["hook:willPatch2", "hook:willPatch1", "hook:patched1", "hook:patched2"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("autofocus hook", () => {
|
||||||
|
function useAutofocus(name) {
|
||||||
|
let ref = useRef(name);
|
||||||
|
let isInDom = false;
|
||||||
|
function updateFocus() {
|
||||||
|
if (!isInDom && ref.el) {
|
||||||
|
isInDom = true;
|
||||||
|
ref.el.focus();
|
||||||
|
} else if (isInDom && !ref.el) {
|
||||||
|
isInDom = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onPatched(updateFocus);
|
||||||
|
onMounted(updateFocus);
|
||||||
|
}
|
||||||
|
|
||||||
|
test("simple input", async () => {
|
||||||
|
class SomeComponent extends Component<any, any> {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<input t-ref="input1"/>
|
||||||
|
<input t-ref="input2"/>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
useAutofocus("input2");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const component = new SomeComponent();
|
||||||
|
await component.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><input><input></div>");
|
||||||
|
const input2 = fixture.querySelectorAll("input")[1];
|
||||||
|
expect(input2).toBe(document.activeElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("input in a t-if", async () => {
|
||||||
|
class SomeComponent extends Component<any, any> {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<input t-ref="input1"/>
|
||||||
|
<t t-if="state.flag"><input t-ref="input2"/></t>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
state = useState({ flag: false });
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
useAutofocus("input2");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const component = new SomeComponent();
|
||||||
|
await component.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><input></div>");
|
||||||
|
expect(document.activeElement).toBe(document.body);
|
||||||
|
|
||||||
|
component.state.flag = true;
|
||||||
|
await nextTick();
|
||||||
|
const input2 = fixture.querySelectorAll("input")[1];
|
||||||
|
expect(input2).toBe(document.activeElement);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can use sub env", async () => {
|
||||||
|
class TestComponent extends Component<any, any> {
|
||||||
|
static template = xml`<div><t t-esc="env.val"/></div>`;
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
useSubEnv({ val: 3 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const component = new TestComponent();
|
||||||
|
await component.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div>3</div>");
|
||||||
|
expect(env).not.toHaveProperty("val");
|
||||||
|
expect(component.env).toHaveProperty("val");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parent and child env", async () => {
|
||||||
|
class Child extends Component<any, any> {
|
||||||
|
static template = xml`<div><t t-esc="env.val"/></div>`;
|
||||||
|
constructor(parent, props) {
|
||||||
|
super(parent, props);
|
||||||
|
useSubEnv({ val: 5 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static template = xml`<div><t t-esc="env.val"/><Child/></div>`;
|
||||||
|
static components = { Child };
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
useSubEnv({ val: 3 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const component = new Parent();
|
||||||
|
await component.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div>3<div>5</div></div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can use onWillStart, onWillUpdateProps", async () => {
|
||||||
|
const steps: string[] = [];
|
||||||
|
function useMyHook() {
|
||||||
|
onWillStart(() => {
|
||||||
|
steps.push("onWillStart");
|
||||||
|
});
|
||||||
|
onWillUpdateProps(nextProps => {
|
||||||
|
expect(nextProps).toEqual({ value: 2 });
|
||||||
|
steps.push("onWillUpdateProps");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
class MyComponent extends Component<any, any> {
|
||||||
|
static template = xml`<span><t t-esc="props.value"/></span>`;
|
||||||
|
constructor(parent, props) {
|
||||||
|
super(parent, props);
|
||||||
|
useMyHook();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class App extends Component<any, any> {
|
||||||
|
static template = xml`<div><MyComponent value="state.value"/></div>`;
|
||||||
|
static components = { MyComponent };
|
||||||
|
state = useState({ value: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = new App();
|
||||||
|
await app.mount(fixture);
|
||||||
|
expect(app).not.toHaveProperty("willStart");
|
||||||
|
expect(app).not.toHaveProperty("willUpdateProps");
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
|
||||||
|
expect(steps).toEqual(["onWillStart"]);
|
||||||
|
|
||||||
|
app.state.value = 2;
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>2</span></div>");
|
||||||
|
expect(steps).toEqual(["onWillStart", "onWillUpdateProps"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
import { AsyncRoot } from "../../src/misc/async_root";
|
||||||
|
import { useState } from "../../src/hooks";
|
||||||
|
import { xml } from "../../src/tags";
|
||||||
|
import { makeDeferred, makeTestFixture, makeTestEnv, nextTick } from "../helpers";
|
||||||
|
import { Component } from "../../src/component/component";
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Setup and helpers
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// We create before each test:
|
||||||
|
// - fixture: a div, appended to the DOM, intended to be the target of dom
|
||||||
|
// manipulations. Note that it is removed after each test.
|
||||||
|
// - a test env, necessary to create components, that is set as env
|
||||||
|
|
||||||
|
let fixture: HTMLElement;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = makeTestFixture();
|
||||||
|
Component.env = makeTestEnv();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fixture.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Asyncroot", () => {
|
||||||
|
test("delayed component with AsyncRoot component", async () => {
|
||||||
|
let def;
|
||||||
|
class Child extends Component<any, any> {
|
||||||
|
static template = xml`<span><t t-esc="props.val"/></span>`;
|
||||||
|
}
|
||||||
|
class AsyncChild extends Child {
|
||||||
|
willUpdateProps() {
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<button t-on-click="updateApp">Update App State</button>
|
||||||
|
<div class="children">
|
||||||
|
<Child val="state.val"/>
|
||||||
|
<AsyncRoot>
|
||||||
|
<AsyncChild val="state.val"/>
|
||||||
|
</AsyncRoot>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
static components = { Child, AsyncChild, AsyncRoot };
|
||||||
|
state = useState({ val: 0 });
|
||||||
|
|
||||||
|
updateApp() {
|
||||||
|
this.state.val++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = new Parent();
|
||||||
|
await parent.mount(fixture);
|
||||||
|
|
||||||
|
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>0</span><span>0</span>");
|
||||||
|
|
||||||
|
// click on button to increment Parent counter
|
||||||
|
def = makeDeferred();
|
||||||
|
fixture.querySelector("button")!.click();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1</span><span>0</span>");
|
||||||
|
|
||||||
|
def.resolve();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1</span><span>1</span>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("fast component with AsyncRoot", async () => {
|
||||||
|
let def;
|
||||||
|
class Child extends Component<any, any> {
|
||||||
|
static template = xml`<span><t t-esc="props.val"/></span>`;
|
||||||
|
}
|
||||||
|
class AsyncChild extends Child {
|
||||||
|
willUpdateProps() {
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<button t-on-click="updateApp">Update App State</button>
|
||||||
|
<div class="children">
|
||||||
|
<AsyncRoot>
|
||||||
|
<Child val="state.val"/>
|
||||||
|
</AsyncRoot>
|
||||||
|
<AsyncChild val="state.val"/>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
static components = { Child, AsyncChild, AsyncRoot };
|
||||||
|
state = useState({ val: 0 });
|
||||||
|
|
||||||
|
updateApp() {
|
||||||
|
this.state.val++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = new Parent();
|
||||||
|
await parent.mount(fixture);
|
||||||
|
|
||||||
|
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>0</span><span>0</span>");
|
||||||
|
|
||||||
|
// click on button to increment Parent counter
|
||||||
|
def = makeDeferred();
|
||||||
|
fixture.querySelector("button")!.click();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1</span><span>0</span>");
|
||||||
|
|
||||||
|
def.resolve();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1</span><span>1</span>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("asyncroot component: mixed re-renderings", async () => {
|
||||||
|
let def;
|
||||||
|
class Child extends Component<any, any> {
|
||||||
|
static template = xml`
|
||||||
|
<span t-on-click="increment">
|
||||||
|
<t t-esc="state.val"/>/<t t-esc="props.val"/>
|
||||||
|
</span>`;
|
||||||
|
state = useState({ val: 0 });
|
||||||
|
|
||||||
|
increment() {
|
||||||
|
this.state.val++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class AsyncChild extends Child {
|
||||||
|
willUpdateProps() {
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<button t-on-click="updateApp">Update App State</button>
|
||||||
|
<div class="children">
|
||||||
|
<Child val="state.val"/>
|
||||||
|
<AsyncRoot>
|
||||||
|
<AsyncChild val="state.val"/>
|
||||||
|
</AsyncRoot>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
static components = { Child, AsyncChild, AsyncRoot };
|
||||||
|
state = useState({ val: 0 });
|
||||||
|
|
||||||
|
updateApp() {
|
||||||
|
this.state.val++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = new Parent();
|
||||||
|
await parent.mount(fixture);
|
||||||
|
|
||||||
|
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>0/0</span><span>0/0</span>");
|
||||||
|
|
||||||
|
// click on button to increment Parent counter
|
||||||
|
def = makeDeferred();
|
||||||
|
fixture.querySelector("button")!.click();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>0/1</span><span>0/0</span>");
|
||||||
|
|
||||||
|
// click on each Child to increment their local counter
|
||||||
|
const children = parent.el!.querySelectorAll("span");
|
||||||
|
children[0]!.click();
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1/1</span><span>0/0</span>");
|
||||||
|
|
||||||
|
children[1]!.click();
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1/1</span><span>0/0</span>");
|
||||||
|
|
||||||
|
// finalize first re-rendering (coming from the props update)
|
||||||
|
def.resolve();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1/1</span><span>1/1</span>");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,828 @@
|
|||||||
|
import { Portal } from "../../src/misc/portal";
|
||||||
|
import { xml } from "../../src/tags";
|
||||||
|
import { makeTestFixture, makeTestEnv, nextTick } from "../helpers";
|
||||||
|
import { Component } from "../../src/component/component";
|
||||||
|
import { useState } from "../../src/hooks";
|
||||||
|
import { QWeb } from "../../src/qweb";
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Setup and helpers
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// We create before each test:
|
||||||
|
// - fixture: a div, appended to the DOM, intended to be the target of dom
|
||||||
|
// manipulations. Note that it is removed after each test.
|
||||||
|
// - outside: a div with id #outside appended into fixture, meant to be used as
|
||||||
|
// target by Portal component
|
||||||
|
// - a test env, necessary to create components, that is set on Component
|
||||||
|
|
||||||
|
let fixture: HTMLElement;
|
||||||
|
let outside: HTMLElement;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = makeTestFixture();
|
||||||
|
outside = document.createElement("div");
|
||||||
|
outside.setAttribute("id", "outside");
|
||||||
|
fixture.appendChild(outside);
|
||||||
|
|
||||||
|
Component.env = makeTestEnv();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fixture.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Portal: Props validation", () => {
|
||||||
|
test("target is mandatory", async () => {
|
||||||
|
const dev = QWeb.dev;
|
||||||
|
QWeb.dev = true;
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static components = { Portal };
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<Portal>
|
||||||
|
<div>2</div>
|
||||||
|
</Portal>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
let error;
|
||||||
|
try {
|
||||||
|
const parent = new Parent();
|
||||||
|
await parent.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(error.message).toBe(`Missing props 'target' (component 'Portal')`);
|
||||||
|
|
||||||
|
QWeb.dev = dev;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("target is not list", async () => {
|
||||||
|
const dev = QWeb.dev;
|
||||||
|
QWeb.dev = true;
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static components = { Portal };
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<Portal target="['body']">
|
||||||
|
<div>2</div>
|
||||||
|
</Portal>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
let error;
|
||||||
|
try {
|
||||||
|
const parent = new Parent();
|
||||||
|
await parent.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(error.message).toBe(`Invalid Prop 'target' in component 'Portal'`);
|
||||||
|
|
||||||
|
QWeb.dev = dev;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Portal: Basic use and DOM placement", () => {
|
||||||
|
test("basic use of portal", async () => {
|
||||||
|
const dev = QWeb.dev;
|
||||||
|
QWeb.dev = true;
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static components = { Portal };
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<span>1</span>
|
||||||
|
<Portal target="'#outside'">
|
||||||
|
<div>2</div>
|
||||||
|
</Portal>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
let error;
|
||||||
|
let parent;
|
||||||
|
try {
|
||||||
|
parent = new Parent();
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeUndefined();
|
||||||
|
await parent.mount(fixture);
|
||||||
|
expect(outside.innerHTML).toBe("<div>2</div>");
|
||||||
|
expect(parent.el!.outerHTML).toBe("<div><span>1</span><portal></portal></div>");
|
||||||
|
QWeb.dev = dev;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("conditional use of Portal", async () => {
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static components = { Portal };
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<span>1</span>
|
||||||
|
<Portal target="'#outside'" t-if="state.hasPortal">
|
||||||
|
<div>2</div>
|
||||||
|
</Portal>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
state = useState({ hasPortal: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = new Parent();
|
||||||
|
await parent.mount(fixture);
|
||||||
|
expect(outside.innerHTML).toBe("");
|
||||||
|
expect(parent.el!.outerHTML).toBe("<div><span>1</span></div>");
|
||||||
|
|
||||||
|
parent.state.hasPortal = true;
|
||||||
|
await nextTick();
|
||||||
|
expect(outside.innerHTML).toBe("<div>2</div>");
|
||||||
|
expect(parent.el!.outerHTML).toBe("<div><span>1</span><portal></portal></div>");
|
||||||
|
|
||||||
|
parent.state.hasPortal = false;
|
||||||
|
await nextTick();
|
||||||
|
expect(outside.innerHTML).toBe("");
|
||||||
|
expect(parent.el!.outerHTML).toBe("<div><span>1</span></div>");
|
||||||
|
|
||||||
|
parent.state.hasPortal = true;
|
||||||
|
await nextTick();
|
||||||
|
expect(outside.innerHTML).toBe("<div>2</div>");
|
||||||
|
expect(parent.el!.outerHTML).toBe("<div><span>1</span><portal></portal></div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("conditional use of Portal (with sub Component)", async () => {
|
||||||
|
class Child extends Component<any, any> {
|
||||||
|
static template = xml`<div><t t-esc="props.val"/></div>`;
|
||||||
|
}
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static components = { Portal, Child };
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<span>1</span>
|
||||||
|
<Portal t-if="state.hasPortal" target="'#outside'">
|
||||||
|
<Child val="state.val"/>
|
||||||
|
</Portal>
|
||||||
|
</div>`;
|
||||||
|
state = useState({ hasPortal: false, val: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = new Parent();
|
||||||
|
await parent.mount(fixture);
|
||||||
|
expect(outside.innerHTML).toBe("");
|
||||||
|
expect(parent.el!.outerHTML).toBe("<div><span>1</span></div>");
|
||||||
|
|
||||||
|
parent.state.hasPortal = true;
|
||||||
|
await nextTick();
|
||||||
|
expect(outside.innerHTML).toBe("<div>1</div>");
|
||||||
|
expect(parent.el!.outerHTML).toBe("<div><span>1</span><portal></portal></div>");
|
||||||
|
|
||||||
|
parent.state.hasPortal = false;
|
||||||
|
await nextTick();
|
||||||
|
expect(outside.innerHTML).toBe("");
|
||||||
|
expect(parent.el!.outerHTML).toBe("<div><span>1</span></div>");
|
||||||
|
|
||||||
|
parent.state.val = 2;
|
||||||
|
await nextTick();
|
||||||
|
expect(outside.innerHTML).toBe("");
|
||||||
|
expect(parent.el!.outerHTML).toBe("<div><span>1</span></div>");
|
||||||
|
|
||||||
|
parent.state.hasPortal = true;
|
||||||
|
await nextTick();
|
||||||
|
expect(outside.innerHTML).toBe("<div>2</div>");
|
||||||
|
expect(parent.el!.outerHTML).toBe("<div><span>1</span><portal></portal></div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("with target in template (before portal)", async () => {
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static components = { Portal };
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<div id="local-target"></div>
|
||||||
|
<span>1</span>
|
||||||
|
<Portal target="'#local-target'">
|
||||||
|
<p>2</p>
|
||||||
|
</Portal>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = new Parent();
|
||||||
|
await parent.mount(fixture);
|
||||||
|
expect(parent.el!.innerHTML).toBe(
|
||||||
|
'<div id="local-target"><p>2</p></div><span>1</span><portal></portal>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("with target in template (after portal)", async () => {
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static components = { Portal };
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<span>1</span>
|
||||||
|
<Portal target="'#local-target'">
|
||||||
|
<p>2</p>
|
||||||
|
</Portal>
|
||||||
|
<div id="local-target"></div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = new Parent();
|
||||||
|
await parent.mount(fixture);
|
||||||
|
expect(parent.el!.innerHTML).toBe(
|
||||||
|
'<span>1</span><portal></portal><div id="local-target"><p>2</p></div>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("portal with target not in dom", async () => {
|
||||||
|
const consoleError = console.error;
|
||||||
|
console.error = jest.fn(() => {});
|
||||||
|
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static components = { Portal };
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<Portal target="'#does-not-exist'">
|
||||||
|
<div>2</div>
|
||||||
|
</Portal>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = new Parent();
|
||||||
|
let error;
|
||||||
|
try {
|
||||||
|
await parent.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(error.message).toBe('Could not find any match for "#does-not-exist"');
|
||||||
|
expect(console.error).toBeCalledTimes(0);
|
||||||
|
expect(fixture.innerHTML).toBe(`<div id="outside"></div>`);
|
||||||
|
console.error = consoleError;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("portal with child and props", async () => {
|
||||||
|
const steps: string[] = [];
|
||||||
|
class Child extends Component<any, any> {
|
||||||
|
static template = xml`<span><t t-esc="props.val"/></span>`;
|
||||||
|
mounted() {
|
||||||
|
steps.push("mounted");
|
||||||
|
expect(outside.innerHTML).toBe("<span>1</span>");
|
||||||
|
}
|
||||||
|
patched() {
|
||||||
|
steps.push("patched");
|
||||||
|
expect(outside.innerHTML).toBe("<span>2</span>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static components = { Portal, Child };
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<Portal target="'#outside'">
|
||||||
|
<Child val="state.val"/>
|
||||||
|
</Portal>
|
||||||
|
</div>`;
|
||||||
|
state = useState({ val: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = new Parent();
|
||||||
|
await parent.mount(fixture);
|
||||||
|
expect(outside.innerHTML).toBe("<span>1</span>");
|
||||||
|
expect(parent.el!.innerHTML).toBe("<portal></portal>");
|
||||||
|
|
||||||
|
parent.state.val = 2;
|
||||||
|
await nextTick();
|
||||||
|
expect(outside.innerHTML).toBe("<span>2</span>");
|
||||||
|
expect(parent.el!.innerHTML).toBe("<portal></portal>");
|
||||||
|
expect(steps).toEqual(["mounted", "patched"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("portal with only text as content", async () => {
|
||||||
|
const consoleError = console.error;
|
||||||
|
console.error = jest.fn(() => {});
|
||||||
|
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static components = { Portal };
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<Portal target="'#outside'">
|
||||||
|
<t t-esc="'only text'"/>
|
||||||
|
</Portal>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = new Parent();
|
||||||
|
let error;
|
||||||
|
try {
|
||||||
|
await parent.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(error.message).toBe("Portal must have exactly one non-text child (has 0)");
|
||||||
|
expect(console.error).toBeCalledTimes(0);
|
||||||
|
expect(fixture.innerHTML).toBe(`<div id="outside"></div>`);
|
||||||
|
console.error = consoleError;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("portal with no content", async () => {
|
||||||
|
const consoleError = console.error;
|
||||||
|
console.error = jest.fn(() => {});
|
||||||
|
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static components = { Portal };
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<Portal target="'#outside'">
|
||||||
|
<t t-if="false" t-esc="'ABC'"/>
|
||||||
|
</Portal>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = new Parent();
|
||||||
|
let error;
|
||||||
|
try {
|
||||||
|
await parent.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(error.message).toBe("Portal must have exactly one non-text child (has 0)");
|
||||||
|
expect(console.error).toBeCalledTimes(0);
|
||||||
|
expect(fixture.innerHTML).toBe(`<div id="outside"></div>`);
|
||||||
|
console.error = consoleError;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("portal with many children", async () => {
|
||||||
|
const consoleError = console.error;
|
||||||
|
console.error = jest.fn(() => {});
|
||||||
|
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static components = { Portal };
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<Portal target="'#outside'">
|
||||||
|
<div>1</div>
|
||||||
|
<p>2</p>
|
||||||
|
</Portal>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
const parent = new Parent();
|
||||||
|
let error;
|
||||||
|
try {
|
||||||
|
await parent.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(error.message).toBe("Portal must have exactly one non-text child (has 2)");
|
||||||
|
expect(console.error).toBeCalledTimes(0);
|
||||||
|
expect(fixture.innerHTML).toBe(`<div id="outside"></div>`);
|
||||||
|
console.error = consoleError;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("portal with dynamic body", async () => {
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static components = { Portal };
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<Portal target="'#outside'">
|
||||||
|
<span t-if="state.val" t-esc="state.val"/>
|
||||||
|
<div t-else=""/>
|
||||||
|
</Portal>
|
||||||
|
</div>`;
|
||||||
|
state = useState({ val: "ab" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = new Parent();
|
||||||
|
await parent.mount(fixture);
|
||||||
|
|
||||||
|
expect(outside.innerHTML).toBe(`<span>ab</span>`);
|
||||||
|
|
||||||
|
parent.state.val = "";
|
||||||
|
await nextTick();
|
||||||
|
expect(outside.innerHTML).toBe(`<div></div>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("portal could have dynamically no content", async () => {
|
||||||
|
const consoleError = console.error;
|
||||||
|
console.error = jest.fn(() => {});
|
||||||
|
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static components = { Portal };
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<Portal target="'#outside'">
|
||||||
|
<span t-if="state.val" t-esc="state.val"/>
|
||||||
|
</Portal>
|
||||||
|
</div>`;
|
||||||
|
state = { val: "ab" };
|
||||||
|
}
|
||||||
|
const parent = new Parent();
|
||||||
|
await parent.mount(fixture);
|
||||||
|
|
||||||
|
expect(outside.innerHTML).toBe(`<span>ab</span>`);
|
||||||
|
|
||||||
|
let error;
|
||||||
|
try {
|
||||||
|
parent.state.val = "";
|
||||||
|
await parent.render();
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(outside.innerHTML).toBe(``);
|
||||||
|
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(error.message).toBe("Portal must have exactly one non-text child (has 0)");
|
||||||
|
|
||||||
|
expect(console.error).toBeCalledTimes(0);
|
||||||
|
console.error = consoleError;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("lifecycle hooks of portal sub component are properly called", async () => {
|
||||||
|
const steps: any[] = [];
|
||||||
|
|
||||||
|
class Child extends Component<any, any> {
|
||||||
|
static template = xml`<span t-esc="props.val"/>`;
|
||||||
|
mounted() {
|
||||||
|
steps.push("child:mounted");
|
||||||
|
}
|
||||||
|
willPatch() {
|
||||||
|
steps.push("child:willPatch");
|
||||||
|
}
|
||||||
|
patched() {
|
||||||
|
steps.push("child:patched");
|
||||||
|
}
|
||||||
|
willUnmount() {
|
||||||
|
steps.push("child:willUnmount");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static components = { Portal, Child };
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<Portal t-if="state.hasChild" target="'#outside'">
|
||||||
|
<Child val="state.val"/>
|
||||||
|
</Portal>
|
||||||
|
</div>`;
|
||||||
|
state = useState({ hasChild: false, val: 1 });
|
||||||
|
mounted() {
|
||||||
|
steps.push("parent:mounted");
|
||||||
|
}
|
||||||
|
willPatch() {
|
||||||
|
steps.push("parent:willPatch");
|
||||||
|
}
|
||||||
|
patched() {
|
||||||
|
steps.push("parent:patched");
|
||||||
|
}
|
||||||
|
willUnmount() {
|
||||||
|
steps.push("parent:willUnmount");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const parent = new Parent();
|
||||||
|
await parent.mount(fixture);
|
||||||
|
expect(steps).toEqual(["parent:mounted"]);
|
||||||
|
|
||||||
|
parent.state.hasChild = true;
|
||||||
|
await nextTick();
|
||||||
|
expect(steps).toEqual([
|
||||||
|
"parent:mounted",
|
||||||
|
"parent:willPatch",
|
||||||
|
"child:mounted",
|
||||||
|
"parent:patched"
|
||||||
|
]);
|
||||||
|
|
||||||
|
parent.state.val = 2;
|
||||||
|
await nextTick();
|
||||||
|
expect(steps).toEqual([
|
||||||
|
"parent:mounted",
|
||||||
|
"parent:willPatch",
|
||||||
|
"child:mounted",
|
||||||
|
"parent:patched",
|
||||||
|
"parent:willPatch",
|
||||||
|
"child:willPatch",
|
||||||
|
"child:patched",
|
||||||
|
"parent:patched"
|
||||||
|
]);
|
||||||
|
|
||||||
|
parent.state.hasChild = false;
|
||||||
|
await nextTick();
|
||||||
|
expect(steps).toEqual([
|
||||||
|
"parent:mounted",
|
||||||
|
"parent:willPatch",
|
||||||
|
"child:mounted",
|
||||||
|
"parent:patched",
|
||||||
|
"parent:willPatch",
|
||||||
|
"child:willPatch",
|
||||||
|
"child:patched",
|
||||||
|
"parent:patched",
|
||||||
|
"parent:willPatch",
|
||||||
|
"child:willUnmount",
|
||||||
|
"parent:patched"
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("portal destroys on crash", async () => {
|
||||||
|
class Child extends Component<any, any> {
|
||||||
|
static template = xml`<span t-esc="props.error and this.will.crash" />`;
|
||||||
|
state = {};
|
||||||
|
}
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static components = { Portal, Child };
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<Portal target="'#outside'" >
|
||||||
|
<Child error="state.error"/>
|
||||||
|
</Portal>
|
||||||
|
</div>`;
|
||||||
|
state = { error: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = new Parent();
|
||||||
|
await parent.mount(fixture);
|
||||||
|
parent.state.error = true;
|
||||||
|
|
||||||
|
let error;
|
||||||
|
try {
|
||||||
|
await parent.render();
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(error.message).toBe("Cannot read property 'crash' of undefined");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Portal: Events handling", () => {
|
||||||
|
test("events triggered on movable pure node are handled", async () => {
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static components = { Portal };
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<Portal target="'#outside'">
|
||||||
|
<span id="trigger-me" t-on-custom="_onCustom" t-esc="state.val"/>
|
||||||
|
</Portal>
|
||||||
|
</div>`;
|
||||||
|
state = useState({ val: "ab" });
|
||||||
|
|
||||||
|
_onCustom() {
|
||||||
|
this.state.val = "triggered";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const parent = new Parent();
|
||||||
|
await parent.mount(fixture);
|
||||||
|
|
||||||
|
expect(outside.innerHTML).toBe(`<span id="trigger-me">ab</span>`);
|
||||||
|
outside.querySelector("#trigger-me")!.dispatchEvent(new Event("custom"));
|
||||||
|
await nextTick();
|
||||||
|
expect(outside.innerHTML).toBe(`<span id="trigger-me">triggered</span>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("events triggered on movable owl components are redirected", async () => {
|
||||||
|
let childInst: Component<any, any> | null = null;
|
||||||
|
class Child extends Component<any, any> {
|
||||||
|
static template = xml`
|
||||||
|
<span t-on-custom="_onCustom" t-esc="props.val"/>`;
|
||||||
|
|
||||||
|
constructor(parent, props) {
|
||||||
|
super(parent, props);
|
||||||
|
childInst = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onCustom() {
|
||||||
|
this.trigger("custom-portal");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static components = { Portal, Child };
|
||||||
|
static template = xml`
|
||||||
|
<div t-on-custom-portal="_onCustomPortal">
|
||||||
|
<Portal target="'#outside'">
|
||||||
|
<Child val="state.val"/>
|
||||||
|
</Portal>
|
||||||
|
</div>`;
|
||||||
|
state = useState({ val: "ab" });
|
||||||
|
|
||||||
|
_onCustomPortal() {
|
||||||
|
this.state.val = "triggered";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const parent = new Parent();
|
||||||
|
await parent.mount(fixture);
|
||||||
|
|
||||||
|
expect(outside.innerHTML).toBe(`<span>ab</span>`);
|
||||||
|
childInst!.trigger("custom");
|
||||||
|
await nextTick();
|
||||||
|
expect(outside.innerHTML).toBe(`<span>triggered</span>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("events triggered on contained movable owl components are redirected", async () => {
|
||||||
|
const steps: string[] = [];
|
||||||
|
let childInst: Component<any, any> | null = null;
|
||||||
|
class Child extends Component<any, any> {
|
||||||
|
static template = xml`
|
||||||
|
<span t-on-custom="_onCustom"/>`;
|
||||||
|
|
||||||
|
constructor(parent, props) {
|
||||||
|
super(parent, props);
|
||||||
|
childInst = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onCustom() {
|
||||||
|
this.trigger("custom-portal");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static components = { Portal, Child };
|
||||||
|
static template = xml`
|
||||||
|
<div t-on-custom="_handled" t-on-custom-portal="_handled">
|
||||||
|
<Portal target="'#outside'">
|
||||||
|
<div>
|
||||||
|
<Child/>
|
||||||
|
</div>
|
||||||
|
</Portal>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
_handled(ev) {
|
||||||
|
steps.push(ev.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const parent = new Parent();
|
||||||
|
await parent.mount(fixture);
|
||||||
|
|
||||||
|
childInst!.trigger("custom");
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// This is expected because trigger is synchronous
|
||||||
|
expect(steps).toMatchObject(["custom-portal", "custom"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Dom events are not mapped", async () => {
|
||||||
|
let childInst: Component<any, any> | null = null;
|
||||||
|
const steps: string[] = [];
|
||||||
|
class Child extends Component<any, any> {
|
||||||
|
static template = xml`
|
||||||
|
<button>child</button>`;
|
||||||
|
|
||||||
|
constructor(parent, props) {
|
||||||
|
super(parent, props);
|
||||||
|
childInst = this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static components = { Portal, Child };
|
||||||
|
static template = xml`
|
||||||
|
<div t-on-click="_handled">
|
||||||
|
<Portal target="'#outside'">
|
||||||
|
<Child />
|
||||||
|
</Portal>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
_handled(ev) {
|
||||||
|
steps.push(ev.type as string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const bodyListener = ev => {
|
||||||
|
steps.push(`body: ${ev.type}`);
|
||||||
|
};
|
||||||
|
document.body.addEventListener("click", bodyListener);
|
||||||
|
|
||||||
|
const parent = new Parent();
|
||||||
|
await parent.mount(fixture);
|
||||||
|
childInst!.el!.click();
|
||||||
|
|
||||||
|
expect(steps).toEqual(["body: click"]);
|
||||||
|
document.body.removeEventListener("click", bodyListener);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Nested portals event propagation", async () => {
|
||||||
|
const outside2 = document.createElement("div");
|
||||||
|
outside2.setAttribute("id", "outside2");
|
||||||
|
fixture.appendChild(outside2);
|
||||||
|
|
||||||
|
const steps: Array<string> = [];
|
||||||
|
let childInst: Component<any, any> | null = null;
|
||||||
|
class Child2 extends Component<any, any> {
|
||||||
|
static template = xml`<div>child2</div>`;
|
||||||
|
constructor(parent, props) {
|
||||||
|
super(parent, props);
|
||||||
|
childInst = this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Child extends Component<any, any> {
|
||||||
|
static components = { Portal, Child2 };
|
||||||
|
static template = xml`
|
||||||
|
<Portal target="'#outside2'">
|
||||||
|
<Child2 />
|
||||||
|
</Portal>`;
|
||||||
|
}
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static components = { Portal, Child };
|
||||||
|
static template = xml`
|
||||||
|
<div t-on-custom='_handled'>
|
||||||
|
<Portal target="'#outside'">
|
||||||
|
<Child/>
|
||||||
|
</Portal>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
_handled(ev) {
|
||||||
|
steps.push(`${ev.type} from ${ev.originalComponent.constructor.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = new Parent();
|
||||||
|
await parent.mount(fixture);
|
||||||
|
|
||||||
|
childInst!.trigger("custom");
|
||||||
|
expect(steps).toEqual(["custom from Child2"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("portal's parent's env is not polluted", async () => {
|
||||||
|
class Child extends Component<any, any> {
|
||||||
|
static template = xml`
|
||||||
|
<button>child</button>`;
|
||||||
|
}
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static components = { Portal, Child };
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<Portal target="'#outside'">
|
||||||
|
<Child />
|
||||||
|
</Portal>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
const parent = new Parent();
|
||||||
|
const parentEnv = Object.assign({}, parent.env);
|
||||||
|
await parent.mount(fixture);
|
||||||
|
expect(parentEnv).toStrictEqual(parent.env);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Portal composed with t-slot", async () => {
|
||||||
|
const steps: Array<string> = [];
|
||||||
|
let childInst: Component<any, any> | null = null;
|
||||||
|
class Child2 extends Component<any, any> {
|
||||||
|
static template = xml`<div>child2</div>`;
|
||||||
|
constructor(parent, props) {
|
||||||
|
super(parent, props);
|
||||||
|
childInst = this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Child extends Component<any, any> {
|
||||||
|
static components = { Portal, Child2 };
|
||||||
|
static template = xml`
|
||||||
|
<Portal target="'#outside'">
|
||||||
|
<t t-slot="default"/>
|
||||||
|
</Portal>`;
|
||||||
|
}
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static components = { Child, Child2 };
|
||||||
|
static template = xml`
|
||||||
|
<div t-on-custom='_handled'>
|
||||||
|
<Child>
|
||||||
|
<Child2/>
|
||||||
|
</Child>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
_handled(ev) {
|
||||||
|
steps.push(ev.type as string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = new Parent();
|
||||||
|
await parent.mount(fixture);
|
||||||
|
|
||||||
|
childInst!.trigger("custom");
|
||||||
|
expect(steps).toEqual(["custom"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Portal: UI/UX", () => {
|
||||||
|
test("focus is kept across re-renders", async () => {
|
||||||
|
class Child extends Component<any, any> {
|
||||||
|
static template = xml`
|
||||||
|
<input id="target-me" t-att-placeholder="props.val"/>`;
|
||||||
|
}
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static components = { Portal, Child };
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<Portal target="'#outside'">
|
||||||
|
<Child val="state.val"/>
|
||||||
|
</Portal>
|
||||||
|
</div>`;
|
||||||
|
state = useState({ val: "ab" });
|
||||||
|
}
|
||||||
|
const parent = new Parent();
|
||||||
|
await parent.mount(fixture);
|
||||||
|
const input = document.querySelector("#target-me");
|
||||||
|
expect(input!.nodeName).toBe("INPUT");
|
||||||
|
expect((input as HTMLInputElement).placeholder).toBe("ab");
|
||||||
|
|
||||||
|
(input as HTMLInputElement).focus();
|
||||||
|
expect(document.activeElement === input).toBeTruthy();
|
||||||
|
|
||||||
|
parent.state.val = "bc";
|
||||||
|
await nextTick();
|
||||||
|
const inputReRendered = document.querySelector("#target-me");
|
||||||
|
expect(inputReRendered!.nodeName).toBe("INPUT");
|
||||||
|
expect((inputReRendered as HTMLInputElement).placeholder).toBe("bc");
|
||||||
|
expect(document.activeElement === inputReRendered).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
File diff suppressed because it is too large
Load Diff
+389
-20
@@ -1,5 +1,6 @@
|
|||||||
import { QWeb } from "../../src/qweb/index";
|
import { QWeb } from "../../src/qweb/index";
|
||||||
import { normalize, renderToDOM, renderToString, trim, nextTick } from "../helpers";
|
import { nextTick, normalize, renderToDOM, renderToString, trim } from "../helpers";
|
||||||
|
import { patch } from "../../src/vdom";
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Setup and helpers
|
// Setup and helpers
|
||||||
@@ -49,6 +50,11 @@ describe("static templates", () => {
|
|||||||
qweb.addTemplate("test", "<div><span>word</span></div>");
|
qweb.addTemplate("test", "<div><span>word</span></div>");
|
||||||
expect(renderToString(qweb, "test")).toBe("<div><span>word</span></div>");
|
expect(renderToString(qweb, "test")).toBe("<div><span>word</span></div>");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("properly handle comments", () => {
|
||||||
|
qweb.addTemplate("test", "<div>hello <!-- comment-->owl</div>");
|
||||||
|
expect(renderToString(qweb, "test")).toBe("<div>hello <!-- comment-->owl</div>");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("error handling", () => {
|
describe("error handling", () => {
|
||||||
@@ -74,19 +80,12 @@ describe("error handling", () => {
|
|||||||
expect(() => qweb.addTemplate("test", "<div/>")).toThrow("already defined");
|
expect(() => qweb.addTemplate("test", "<div/>")).toThrow("already defined");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("loadTemplates throw if parser error", () => {
|
test("addTemplates throw if parser error", () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
qweb.addTemplates("<templates><abc>></templates>");
|
qweb.addTemplates("<templates><abc>></templates>");
|
||||||
}).toThrow("Invalid XML in template");
|
}).toThrow("Invalid XML in template");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("nice error when t-on-directive is evaluated with a missing handler", () => {
|
|
||||||
qweb.addTemplate("templatename", `<div t-on-click="somemethod"></div>`);
|
|
||||||
expect(() => qweb.render("templatename", {}, { handlers: [] })).toThrow(
|
|
||||||
"Missing handler 'somemethod' when evaluating template 'templatename'"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("nice error when t-on is evaluated with a missing event", () => {
|
test("nice error when t-on is evaluated with a missing event", () => {
|
||||||
qweb.addTemplate("templatename", `<div t-on="somemethod"></div>`);
|
qweb.addTemplate("templatename", `<div t-on="somemethod"></div>`);
|
||||||
expect(() => qweb.render("templatename", { someMethod() {} }, { handlers: [] })).toThrow(
|
expect(() => qweb.render("templatename", { someMethod() {} }, { handlers: [] })).toThrow(
|
||||||
@@ -243,6 +242,32 @@ describe("t-set", () => {
|
|||||||
);
|
);
|
||||||
expect(renderToString(qweb, "test", { somevariable: 43 })).toBe("<div>45</div>");
|
expect(renderToString(qweb, "test", { somevariable: 43 })).toBe("<div>45</div>");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("t-set, t-if, and mix of expression/body lookup, 1", () => {
|
||||||
|
qweb.addTemplate(
|
||||||
|
"test",
|
||||||
|
`<div>
|
||||||
|
<t t-if="flag" t-set="ourvar">1</t>
|
||||||
|
<t t-else="" t-set="ourvar" t-value="0"></t>
|
||||||
|
<t t-esc="ourvar"/>
|
||||||
|
</div>`
|
||||||
|
);
|
||||||
|
expect(renderToString(qweb, "test", { flag: true })).toBe("<div>1</div>");
|
||||||
|
expect(renderToString(qweb, "test", { flag: false })).toBe("<div>0</div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("t-set, t-if, and mix of expression/body lookup, 2", () => {
|
||||||
|
qweb.addTemplate(
|
||||||
|
"test",
|
||||||
|
`<div>
|
||||||
|
<t t-if="flag" t-set="ourvar" t-value="1"></t>
|
||||||
|
<t t-else="" t-set="ourvar">0</t>
|
||||||
|
<t t-esc="ourvar"/>
|
||||||
|
</div>`
|
||||||
|
);
|
||||||
|
expect(renderToString(qweb, "test", { flag: true })).toBe("<div>1</div>");
|
||||||
|
expect(renderToString(qweb, "test", { flag: false })).toBe("<div>0</div>");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("t-if", () => {
|
describe("t-if", () => {
|
||||||
@@ -323,6 +348,61 @@ describe("t-if", () => {
|
|||||||
};
|
};
|
||||||
expect(normalize(renderToString(qweb, "test", context))).toBe("<div>andormgtnlt</div>");
|
expect(normalize(renderToString(qweb, "test", context))).toBe("<div>andormgtnlt</div>");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("t-esc with t-if", () => {
|
||||||
|
qweb.addTemplate("test", `<div><t t-if="true" t-esc="'x'"/></div>`);
|
||||||
|
expect(renderToString(qweb, "test")).toBe("<div>x</div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("t-esc with t-elif", () => {
|
||||||
|
qweb.addTemplate("test", `<div><t t-if="false">abc</t><t t-else="1" t-esc="'x'"/></div>`);
|
||||||
|
expect(renderToString(qweb, "test")).toBe("<div>x</div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("t-set, then t-if", () => {
|
||||||
|
qweb.addTemplate(
|
||||||
|
"test",
|
||||||
|
`
|
||||||
|
<div>
|
||||||
|
<t t-set="title" t-value="'test'"/>
|
||||||
|
<t t-if="title"><t t-esc="title"/></t>
|
||||||
|
</div>`
|
||||||
|
);
|
||||||
|
const result = renderToString(qweb, "test");
|
||||||
|
const expected = `<div>test</div>`;
|
||||||
|
expect(result).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("t-set, then t-if, part 2", () => {
|
||||||
|
qweb.addTemplate(
|
||||||
|
"test",
|
||||||
|
`
|
||||||
|
<div>
|
||||||
|
<t t-set="y" t-value="true"/>
|
||||||
|
<t t-set="x" t-value="y"/>
|
||||||
|
<span t-if="x">COUCOU</span>
|
||||||
|
</div>`
|
||||||
|
);
|
||||||
|
const result = renderToString(qweb, "test");
|
||||||
|
const expected = `<div><span>COUCOU</span></div>`;
|
||||||
|
expect(result).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("t-set, then t-elif, part 3", () => {
|
||||||
|
qweb.addTemplate(
|
||||||
|
"test",
|
||||||
|
`
|
||||||
|
<div>
|
||||||
|
<t t-set="y" t-value="false"/>
|
||||||
|
<t t-set="x" t-value="y"/>
|
||||||
|
<span t-if="x">AAA</span>
|
||||||
|
<span t-elif="!x">BBB</span>
|
||||||
|
</div>`
|
||||||
|
);
|
||||||
|
const result = renderToString(qweb, "test");
|
||||||
|
const expected = `<div><span>BBB</span></div>`;
|
||||||
|
expect(result).toBe(expected);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("attributes", () => {
|
describe("attributes", () => {
|
||||||
@@ -593,6 +673,40 @@ describe("t-call (template calling", () => {
|
|||||||
expect(renderToString(qweb, "main")).toBe(expected);
|
expect(renderToString(qweb, "main")).toBe(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("cascading t-call t-raw='0'", () => {
|
||||||
|
qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="finalTemplate">
|
||||||
|
<span>cascade 2</span>
|
||||||
|
<t t-raw="0"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div t-name="subSubTemplate">
|
||||||
|
<t t-call="finalTemplate">
|
||||||
|
<span>cascade 1</span>
|
||||||
|
<t t-raw="0"/>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div t-name="SubTemplate">
|
||||||
|
<t t-call="subSubTemplate">
|
||||||
|
<span>cascade 0</span>
|
||||||
|
<t t-raw="0"/>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div t-name="main">
|
||||||
|
<t t-call="SubTemplate">
|
||||||
|
<span>hey</span> <span>yay</span>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
const expected =
|
||||||
|
"<div><div><div><div><span>cascade 2</span><span>cascade 1</span><span>cascade 0</span><span>hey</span> <span>yay</span></div></div></div></div>";
|
||||||
|
expect(renderToString(qweb, "main")).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
test("recursive template, part 1", () => {
|
test("recursive template, part 1", () => {
|
||||||
qweb.addTemplates(`
|
qweb.addTemplates(`
|
||||||
<templates>
|
<templates>
|
||||||
@@ -606,7 +720,7 @@ describe("t-call (template calling", () => {
|
|||||||
`);
|
`);
|
||||||
const expected = "<div><span>hey</span></div>";
|
const expected = "<div><span>hey</span></div>";
|
||||||
expect(renderToString(qweb, "recursive")).toBe(expected);
|
expect(renderToString(qweb, "recursive")).toBe(expected);
|
||||||
const recursiveFn = Object.values(qweb.recursiveFns)[0];
|
const recursiveFn = Object.values(qweb.recursiveFns)[0] as any;
|
||||||
expect(recursiveFn.toString()).toMatchSnapshot();
|
expect(recursiveFn.toString()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -631,8 +745,10 @@ describe("t-call (template calling", () => {
|
|||||||
`);
|
`);
|
||||||
const root = { val: "a", children: [{ val: "b" }, { val: "c" }] };
|
const root = { val: "a", children: [{ val: "b" }, { val: "c" }] };
|
||||||
const expected = "<div><div><p>a</p><div><p>b</p></div><div><p>c</p></div></div></div>";
|
const expected = "<div><div><p>a</p><div><p>b</p></div><div><p>c</p></div></div></div>";
|
||||||
expect(renderToString(qweb, "Parent", { root })).toBe(expected);
|
expect(renderToString(qweb, "Parent", { root }, { fiber: { vars: {}, scope: {} } })).toBe(
|
||||||
const recursiveFn = Object.values(qweb.recursiveFns)[0];
|
expected
|
||||||
|
);
|
||||||
|
const recursiveFn = Object.values(qweb.recursiveFns)[0] as any;
|
||||||
expect(recursiveFn.toString()).toMatchSnapshot();
|
expect(recursiveFn.toString()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -658,10 +774,17 @@ describe("t-call (template calling", () => {
|
|||||||
const root = { val: "a", children: [{ val: "b", children: [{ val: "d" }] }, { val: "c" }] };
|
const root = { val: "a", children: [{ val: "b", children: [{ val: "d" }] }, { val: "c" }] };
|
||||||
const expected =
|
const expected =
|
||||||
"<div><div><p>a</p><div><p>b</p><div><p>d</p></div></div><div><p>c</p></div></div></div>";
|
"<div><div><p>a</p><div><p>b</p><div><p>d</p></div></div><div><p>c</p></div></div></div>";
|
||||||
expect(renderToString(qweb, "Parent", { root })).toBe(expected);
|
expect(renderToString(qweb, "Parent", { root }, { fiber: {} })).toBe(expected);
|
||||||
const recursiveFn = Object.values(qweb.recursiveFns)[0];
|
const recursiveFn = Object.values(qweb.recursiveFns)[0] as any;
|
||||||
expect(recursiveFn.toString()).toMatchSnapshot();
|
expect(recursiveFn.toString()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("t-call, global templates", () => {
|
||||||
|
QWeb.registerTemplate("abcd", '<div><t t-call="john"/></div>');
|
||||||
|
qweb.addTemplate("john", `<span>desk</span>`);
|
||||||
|
const expected = "<div><span>desk</span></div>";
|
||||||
|
expect(trim(renderToString(qweb, "abcd"))).toBe(expected);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("foreach", () => {
|
describe("foreach", () => {
|
||||||
@@ -735,6 +858,23 @@ describe("foreach", () => {
|
|||||||
expect(Object.keys(context).length).toBe(0);
|
expect(Object.keys(context).length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("t-foreach in t-forach", () => {
|
||||||
|
qweb.addTemplate(
|
||||||
|
"test",
|
||||||
|
`<div>
|
||||||
|
<t t-foreach="numbers" t-as="number">
|
||||||
|
<t t-foreach="letters" t-as="letter">
|
||||||
|
[<t t-esc="number"/><t t-esc="letter"/>]
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</div>`
|
||||||
|
);
|
||||||
|
const context = { numbers: [1, 2, 3], letters: ["a", "b"] };
|
||||||
|
expect(renderToString(qweb, "test", context)).toBe(
|
||||||
|
"<div> [1a] [1b] [2a] [2b] [3a] [3b] </div>"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("throws error if invalid loop expression", () => {
|
test("throws error if invalid loop expression", () => {
|
||||||
qweb.addTemplate(
|
qweb.addTemplate(
|
||||||
"test",
|
"test",
|
||||||
@@ -758,13 +898,16 @@ describe("foreach", () => {
|
|||||||
);
|
);
|
||||||
renderToString(qweb, "test");
|
renderToString(qweb, "test");
|
||||||
expect(console.warn).toHaveBeenCalledTimes(1);
|
expect(console.warn).toHaveBeenCalledTimes(1);
|
||||||
|
expect(console.warn).toHaveBeenCalledWith(
|
||||||
|
"Directive t-foreach should always be used with a t-key! (in template: 'test')"
|
||||||
|
);
|
||||||
console.warn = consoleWarn;
|
console.warn = consoleWarn;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("misc", () => {
|
describe("misc", () => {
|
||||||
test("global", () => {
|
test("global", () => {
|
||||||
qweb.addTemplate("_callee-asc", `<Año t-att-falló="'agüero'" t-raw="0"/>`);
|
qweb.addTemplate("_callee-asc", `<año t-att-falló="'agüero'" t-raw="0"/>`);
|
||||||
qweb.addTemplate("_callee-uses-foo", `<span t-esc="foo">foo default</span>`);
|
qweb.addTemplate("_callee-uses-foo", `<span t-esc="foo">foo default</span>`);
|
||||||
qweb.addTemplate("_callee-asc-toto", `<div t-raw="toto">toto default</div>`);
|
qweb.addTemplate("_callee-asc-toto", `<div t-raw="toto">toto default</div>`);
|
||||||
qweb.addTemplate(
|
qweb.addTemplate(
|
||||||
@@ -961,6 +1104,35 @@ describe("t-on", () => {
|
|||||||
(<HTMLElement>node).click();
|
(<HTMLElement>node).click();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("t-on with inline statement", () => {
|
||||||
|
qweb.addTemplate("test", `<button t-on-click="state.counter++">Click</button>`);
|
||||||
|
let owner = {
|
||||||
|
state: {
|
||||||
|
counter: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
|
||||||
|
expect(owner.state.counter).toBe(0);
|
||||||
|
(<HTMLElement>node).click();
|
||||||
|
expect(owner.state.counter).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("t-on with inline statement (function call)", () => {
|
||||||
|
qweb.addTemplate("test", `<button t-on-click="state.incrementCounter(2)">Click</button>`);
|
||||||
|
let owner = {
|
||||||
|
state: {
|
||||||
|
counter: 0,
|
||||||
|
incrementCounter: inc => {
|
||||||
|
owner.state.counter += inc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
|
||||||
|
expect(owner.state.counter).toBe(0);
|
||||||
|
(<HTMLElement>node).click();
|
||||||
|
expect(owner.state.counter).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
test("t-on with prevent and/or stop modifiers", async () => {
|
test("t-on with prevent and/or stop modifiers", async () => {
|
||||||
expect.assertions(7);
|
expect.assertions(7);
|
||||||
qweb.addTemplate(
|
qweb.addTemplate(
|
||||||
@@ -1072,20 +1244,113 @@ describe("t-on", () => {
|
|||||||
|
|
||||||
expect(steps).toEqual([true, true]);
|
expect(steps).toEqual([true, true]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("t-on with prevent modifier in t-foreach", async () => {
|
||||||
|
expect.assertions(5);
|
||||||
|
qweb.addTemplate(
|
||||||
|
"test",
|
||||||
|
`<div>
|
||||||
|
<t t-foreach="projects" t-as="project">
|
||||||
|
<a href="#" t-key="project" t-on-click.prevent="onEdit(project.id)">
|
||||||
|
Edit <t t-esc="project.name"/>
|
||||||
|
</a>
|
||||||
|
</t>
|
||||||
|
</div>`
|
||||||
|
);
|
||||||
|
const steps: string[] = [];
|
||||||
|
const owner = {
|
||||||
|
projects: [
|
||||||
|
{ id: 1, name: "Project 1" },
|
||||||
|
{ id: 2, name: "Project 2" }
|
||||||
|
],
|
||||||
|
|
||||||
|
onEdit(projectId, ev) {
|
||||||
|
expect(ev.defaultPrevented).toBe(true);
|
||||||
|
steps.push(projectId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const node = <HTMLElement>renderToDOM(qweb, "test", owner, { handlers: [] });
|
||||||
|
expect(node.outerHTML).toBe(
|
||||||
|
`<div><a href="#"> Edit Project 1</a><a href="#"> Edit Project 2</a></div>`
|
||||||
|
);
|
||||||
|
|
||||||
|
const links = node.querySelectorAll("a")!;
|
||||||
|
links[0].click();
|
||||||
|
links[1].click();
|
||||||
|
|
||||||
|
expect(steps).toEqual([1, 2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("t-on with empty handler (only modifiers)", () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
qweb.addTemplate(
|
||||||
|
"test",
|
||||||
|
`<div>
|
||||||
|
<button t-on-click.prevent="">Button</button>
|
||||||
|
</div>`
|
||||||
|
);
|
||||||
|
const node = renderToDOM(qweb, "test", {}, { handlers: [] });
|
||||||
|
|
||||||
|
node.addEventListener("click", e => {
|
||||||
|
expect(e.defaultPrevented).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
const button = (<HTMLElement>node).getElementsByTagName("button")[0];
|
||||||
|
button.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("t-on combined with t-esc", async () => {
|
||||||
|
expect.assertions(3);
|
||||||
|
qweb.addTemplate("test", `<div><button t-on-click="onClick" t-esc="text"/></div>`);
|
||||||
|
const steps: string[] = [];
|
||||||
|
const owner = {
|
||||||
|
text: "Click here",
|
||||||
|
onClick() {
|
||||||
|
steps.push("onClick");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const node = <HTMLElement>renderToDOM(qweb, "test", owner, { handlers: [] });
|
||||||
|
expect(node.outerHTML).toBe(`<div><button>Click here</button></div>`);
|
||||||
|
|
||||||
|
node.querySelector("button")!.click();
|
||||||
|
|
||||||
|
expect(steps).toEqual(["onClick"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("t-on combined with t-raw", async () => {
|
||||||
|
expect.assertions(3);
|
||||||
|
qweb.addTemplate("test", `<div><button t-on-click="onClick" t-raw="html"/></div>`);
|
||||||
|
const steps: string[] = [];
|
||||||
|
const owner = {
|
||||||
|
html: "Click <b>here</b>",
|
||||||
|
onClick() {
|
||||||
|
steps.push("onClick");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const node = <HTMLElement>renderToDOM(qweb, "test", owner, { handlers: [] });
|
||||||
|
expect(node.outerHTML).toBe(`<div><button>Click <b>here</b></button></div>`);
|
||||||
|
|
||||||
|
node.querySelector("button")!.click();
|
||||||
|
|
||||||
|
expect(steps).toEqual(["onClick"]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("t-ref", () => {
|
describe("t-ref", () => {
|
||||||
test("can get a ref on a node", () => {
|
test("can get a ref on a node", () => {
|
||||||
qweb.addTemplate("test", `<div><span t-ref="myspan"/></div>`);
|
qweb.addTemplate("test", `<div><span t-ref="myspan"/></div>`);
|
||||||
let refs: any = {};
|
let refs: any = {};
|
||||||
renderToDOM(qweb, "test", { refs });
|
renderToDOM(qweb, "test", { __owl__: { refs } });
|
||||||
expect(refs.myspan.tagName).toBe("SPAN");
|
expect(refs.myspan.tagName).toBe("SPAN");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can get a dynamic ref on a node", () => {
|
test("can get a dynamic ref on a node", () => {
|
||||||
qweb.addTemplate("test", `<div><span t-ref="myspan{{id}}"/></div>`);
|
qweb.addTemplate("test", `<div><span t-ref="myspan{{id}}"/></div>`);
|
||||||
let refs: any = {};
|
let refs: any = {};
|
||||||
renderToDOM(qweb, "test", { refs, id: 3 });
|
renderToDOM(qweb, "test", { id: 3, __owl__: { refs } });
|
||||||
expect(refs.myspan3.tagName).toBe("SPAN");
|
expect(refs.myspan3.tagName).toBe("SPAN");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1100,19 +1365,19 @@ describe("t-ref", () => {
|
|||||||
</div>`
|
</div>`
|
||||||
);
|
);
|
||||||
let refs: any = {};
|
let refs: any = {};
|
||||||
renderToDOM(qweb, "test", { refs, items: [1, 2, 3] });
|
renderToDOM(qweb, "test", { items: [1, 2, 3], __owl__: { refs } });
|
||||||
expect(Object.keys(refs)).toEqual(["1", "2", "3"]);
|
expect(Object.keys(refs)).toEqual(["1", "2", "3"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("loading templates", () => {
|
describe("loading templates", () => {
|
||||||
test("can initialize qweb with a string", () => {
|
test("can initialize qweb with a string", () => {
|
||||||
const data = `
|
const templates = `
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<templates id="template" xml:space="preserve">
|
<templates id="template" xml:space="preserve">
|
||||||
<div t-name="hey">jupiler</div>
|
<div t-name="hey">jupiler</div>
|
||||||
</templates>`;
|
</templates>`;
|
||||||
const qweb = new QWeb(data);
|
const qweb = new QWeb({ templates });
|
||||||
expect(renderToString(qweb, "hey")).toBe("<div>jupiler</div>");
|
expect(renderToString(qweb, "hey")).toBe("<div>jupiler</div>");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1298,3 +1563,107 @@ describe("global template registration", () => {
|
|||||||
expect((vnode as any).children[0].text).toBe("global");
|
expect((vnode as any).children[0].text).toBe("global");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("properly support svg", () => {
|
||||||
|
test("add proper namespace to svg", () => {
|
||||||
|
qweb.addTemplate(
|
||||||
|
"test",
|
||||||
|
`<svg width="100px" height="90px"><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/> </svg>`
|
||||||
|
);
|
||||||
|
expect(renderToString(qweb, "test")).toBe(
|
||||||
|
`<svg width=\"100px\" height=\"90px\"><circle cx=\"50\" cy=\"50\" r=\"4\" stroke=\"green\" stroke-width=\"1\" fill=\"yellow\"></circle> </svg>`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("add proper namespace to g tags", () => {
|
||||||
|
// this is necessary if one wants to use components in a svg
|
||||||
|
qweb.addTemplate(
|
||||||
|
"test",
|
||||||
|
`<g><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/> </g>`
|
||||||
|
);
|
||||||
|
expect(renderToString(qweb, "test")).toBe(
|
||||||
|
`<g><circle cx=\"50\" cy=\"50\" r=\"4\" stroke=\"green\" stroke-width=\"1\" fill=\"yellow\"></circle> </g>`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("translation support", () => {
|
||||||
|
test("can translate node content", () => {
|
||||||
|
const translations = {
|
||||||
|
word: "mot"
|
||||||
|
};
|
||||||
|
const translateFn = expr => translations[expr] || expr;
|
||||||
|
const qweb = new QWeb({ translateFn });
|
||||||
|
qweb.addTemplate("test", "<div>word</div>");
|
||||||
|
expect(renderToString(qweb, "test")).toBe("<div>mot</div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not translate node content if disabled", () => {
|
||||||
|
const translations = {
|
||||||
|
word: "mot"
|
||||||
|
};
|
||||||
|
const translateFn = expr => translations[expr] || expr;
|
||||||
|
const qweb = new QWeb({ translateFn });
|
||||||
|
qweb.addTemplate(
|
||||||
|
"test",
|
||||||
|
`
|
||||||
|
<div>
|
||||||
|
<span>word</span>
|
||||||
|
<span t-translation="off">word</span>
|
||||||
|
</div>`
|
||||||
|
);
|
||||||
|
expect(renderToString(qweb, "test")).toBe("<div><span>mot</span><span>word</span></div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("some attributes are translated", () => {
|
||||||
|
const translations = {
|
||||||
|
word: "mot"
|
||||||
|
};
|
||||||
|
const translateFn = expr => translations[expr] || expr;
|
||||||
|
const qweb = new QWeb({ translateFn });
|
||||||
|
qweb.addTemplate(
|
||||||
|
"test",
|
||||||
|
`
|
||||||
|
<div>
|
||||||
|
<p label="word">word</p>
|
||||||
|
<p title="word">word</p>
|
||||||
|
<p placeholder="word">word</p>
|
||||||
|
<p alt="word">word</p>
|
||||||
|
<p something="word">word</p>
|
||||||
|
</div>`
|
||||||
|
);
|
||||||
|
expect(renderToString(qweb, "test")).toBe(
|
||||||
|
'<div><p label="mot">mot</p><p title="mot">mot</p><p placeholder="mot">mot</p><p alt="mot">mot</p><p something="word">mot</p></div>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("t-key tests", () => {
|
||||||
|
test("t-key on t-foreach", async () => {
|
||||||
|
qweb.addTemplate(
|
||||||
|
"test",
|
||||||
|
`
|
||||||
|
<div>
|
||||||
|
<t t-foreach="things" t-as="thing" t-key="thing">
|
||||||
|
<span/>
|
||||||
|
</t>
|
||||||
|
</div>`
|
||||||
|
);
|
||||||
|
|
||||||
|
let vnode = qweb.render("test", { things: [1, 2] });
|
||||||
|
vnode = patch(document.createElement("div"), vnode);
|
||||||
|
let elm = vnode.elm as HTMLElement;
|
||||||
|
|
||||||
|
expect(elm.outerHTML).toBe("<div><span></span><span></span></div>");
|
||||||
|
|
||||||
|
const first = elm.querySelectorAll("span")[0];
|
||||||
|
const second = elm.querySelectorAll("span")[1];
|
||||||
|
|
||||||
|
patch(vnode, qweb.render("test", { things: [2, 1] }));
|
||||||
|
|
||||||
|
expect(elm.outerHTML).toBe("<div><span></span><span></span></div>");
|
||||||
|
|
||||||
|
expect(first).toBe(elm.querySelectorAll("span")[1]);
|
||||||
|
expect(second).toBe(elm.querySelectorAll("span")[0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ describe("tokenizer", () => {
|
|||||||
{ type: "VALUE", value: "2" },
|
{ type: "VALUE", value: "2" },
|
||||||
{ type: "RIGHT_BRACE", value: "}" }
|
{ type: "RIGHT_BRACE", value: "}" }
|
||||||
]);
|
]);
|
||||||
expect(tokenize("a,")).toEqual([{ type: "SYMBOL", value: "a" }, { type: "COMMA", value: "," }]);
|
expect(tokenize("a,")).toEqual([
|
||||||
|
{ type: "SYMBOL", value: "a" },
|
||||||
|
{ type: "COMMA", value: "," }
|
||||||
|
]);
|
||||||
expect(tokenize("][")).toEqual([
|
expect(tokenize("][")).toEqual([
|
||||||
{ type: "RIGHT_BRACKET", value: "]" },
|
{ type: "RIGHT_BRACKET", value: "]" },
|
||||||
{ type: "LEFT_BRACKET", value: "[" }
|
{ type: "LEFT_BRACKET", value: "[" }
|
||||||
@@ -43,6 +46,10 @@ describe("tokenizer", () => {
|
|||||||
{ type: "OPERATOR", value: "!==" },
|
{ type: "OPERATOR", value: "!==" },
|
||||||
{ type: "OPERATOR", value: "!=" }
|
{ type: "OPERATOR", value: "!=" }
|
||||||
]);
|
]);
|
||||||
|
expect(tokenize("typeof a")).toEqual([
|
||||||
|
{ type: "OPERATOR", value: "typeof " },
|
||||||
|
{ type: "SYMBOL", value: "a" }
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("strings", () => {
|
test("strings", () => {
|
||||||
@@ -112,6 +119,7 @@ describe("expression evaluation", () => {
|
|||||||
expect(compileExpr("!flag", {})).toBe("!context['flag']");
|
expect(compileExpr("!flag", {})).toBe("!context['flag']");
|
||||||
expect(compileExpr("-3", {})).toBe("-3");
|
expect(compileExpr("-3", {})).toBe("-3");
|
||||||
expect(compileExpr("-a", {})).toBe("-context['a']");
|
expect(compileExpr("-a", {})).toBe("-context['a']");
|
||||||
|
expect(compileExpr("typeof a", {})).toBe("typeof context['a']");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("various binary operators", () => {
|
test("various binary operators", () => {
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`Link component can render simple cases 1`] = `
|
|
||||||
"function anonymous(context,extra
|
|
||||||
) {
|
|
||||||
let utils = this.constructor.utils;
|
|
||||||
let owner = context;
|
|
||||||
var h = this.h;
|
|
||||||
let _1 = utils.toObj({'router-link-active':context['isActive']});
|
|
||||||
var _2 = context['href'];
|
|
||||||
let c3 = [], p3 = {key:3,attrs:{href: _2},class:_1,on:{}};
|
|
||||||
var vn3 = h('a', p3, c3);
|
|
||||||
result = vn3;
|
|
||||||
if (!context['navigate']) {
|
|
||||||
throw new Error('Missing handler \\\\'' + 'navigate' + \`\\\\' when evaluating template '__template__1'\`)
|
|
||||||
}
|
|
||||||
extra.handlers['click' + 3] = extra.handlers['click' + 3] || context['navigate'].bind(owner);
|
|
||||||
p3.on['click'] = extra.handlers['click' + 3];
|
|
||||||
const slot4 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
|
|
||||||
if (slot4) {
|
|
||||||
slot4.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c3, vars: extra.vars, parent: owner}));
|
|
||||||
}
|
|
||||||
return vn3;
|
|
||||||
}"
|
|
||||||
`;
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`RouteComponent can render simple cases 1`] = `
|
|
||||||
"function anonymous(context,extra
|
|
||||||
) {
|
|
||||||
let utils = this.constructor.utils;
|
|
||||||
let QWeb = this.constructor;
|
|
||||||
let parent = context;
|
|
||||||
let owner = context;
|
|
||||||
let result;
|
|
||||||
context = Object.create(context);
|
|
||||||
var h = this.h;
|
|
||||||
var _1 = context['routes'];
|
|
||||||
if (!_1) { throw new Error('QWeb error: Invalid loop expression')}
|
|
||||||
var _2 = _3 = _1;
|
|
||||||
if (!(_1 instanceof Array)) {
|
|
||||||
_2 = Object.keys(_1);
|
|
||||||
_3 = Object.values(_1);
|
|
||||||
}
|
|
||||||
var _length2 = _2.length;
|
|
||||||
for (let i = 0; i < _length2; i++) {
|
|
||||||
context.route_first = i === 0;
|
|
||||||
context.route_last = i === _length2 - 1;
|
|
||||||
context.route_index = i;
|
|
||||||
context.route = _2[i];
|
|
||||||
context.route_value = _3[i];
|
|
||||||
if (context['env'].router.currentRouteName===context['route'].name) {
|
|
||||||
//COMPONENT
|
|
||||||
let def5;
|
|
||||||
let templateId7 = String(-6 - i);
|
|
||||||
let w6 = templateId7 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId7]] : false;
|
|
||||||
let vn8 = {};
|
|
||||||
result = vn8;
|
|
||||||
let props6 = Object.assign({}, context['env'].router.currentParams);
|
|
||||||
if (w6 && w6.__owl__.renderPromise && !w6.__owl__.vnode) {
|
|
||||||
if (utils.shallowEqual(props6, w6.__owl__.renderProps)) {
|
|
||||||
def5 = w6.__owl__.renderPromise;
|
|
||||||
} else {
|
|
||||||
w6.destroy();
|
|
||||||
w6 = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!w6) {
|
|
||||||
let componentKey6 = (context['route'].component);
|
|
||||||
let W6 = context.constructor.components[componentKey6] || QWeb.components[componentKey6];
|
|
||||||
if (!W6) {throw new Error('Cannot find the definition of component \\"' + componentKey6 + '\\"')}
|
|
||||||
w6 = new W6(parent, props6);
|
|
||||||
parent.__owl__.cmap[templateId7] = w6.__owl__.id;
|
|
||||||
def5 = w6.__prepare();
|
|
||||||
def5 = def5.then(vnode=>{if (w6.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: templateId7, hook: {insert(vn) {let nvn=w6.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w6.destroy();}}});utils.defineProxy(vn8, pvnode);w6.__owl__.pvnode = pvnode;});
|
|
||||||
} else {
|
|
||||||
def5 = def5 || w6.__updateProps(props6, extra.forceUpdate, extra.patchQueue);
|
|
||||||
def5 = def5.then(()=>{if (w6.__owl__.isDestroyed) {return};let pvnode=w6.__owl__.pvnode;utils.defineProxy(vn8, pvnode);});
|
|
||||||
}
|
|
||||||
extra.promises.push(def5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}"
|
|
||||||
`;
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Link component can render simple cases 1`] = `
|
||||||
|
"function anonymous(context,extra
|
||||||
|
) {
|
||||||
|
let utils = this.constructor.utils;
|
||||||
|
let owner = context;
|
||||||
|
var h = this.h;
|
||||||
|
let _5 = utils.toObj({'router-link-active':context['isActive']});
|
||||||
|
var _6 = context['href'];
|
||||||
|
let c7 = [], p7 = {key:7,attrs:{href: _6},class:_5,on:{}};
|
||||||
|
var vn7 = h('a', p7, c7);
|
||||||
|
extra.handlers['click' + 7] = extra.handlers['click' + 7] || function (e) {const fn = context['navigate'];if (fn) { fn.call(owner, e); } else { context.navigate; }};
|
||||||
|
p7.on['click'] = extra.handlers['click' + 7];
|
||||||
|
const slot8 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
|
||||||
|
if (slot8) {
|
||||||
|
slot8.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c7, vars: extra.vars, parent: extra.parent || owner}));
|
||||||
|
}
|
||||||
|
return vn7;
|
||||||
|
}"
|
||||||
|
`;
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`RouteComponent can render simple cases 1`] = `
|
||||||
|
"function anonymous(context,extra
|
||||||
|
) {
|
||||||
|
let utils = this.constructor.utils;
|
||||||
|
let QWeb = this.constructor;
|
||||||
|
let parent = context;
|
||||||
|
let owner = context;
|
||||||
|
let result;
|
||||||
|
var h = this.h;
|
||||||
|
if (context['routeComponent']) {
|
||||||
|
const nodeKey5 = context['env'].router.currentRouteName;
|
||||||
|
//COMPONENT
|
||||||
|
let k7 = \`__8__\` + nodeKey5;
|
||||||
|
let w6 = k7 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k7]] : false;
|
||||||
|
let vn9 = {};
|
||||||
|
result = vn9;
|
||||||
|
let props6 = Object.assign({}, context['env'].router.currentParams);
|
||||||
|
if (w6 && w6.__owl__.currentFiber && !w6.__owl__.vnode) {
|
||||||
|
w6.destroy();
|
||||||
|
w6 = false;
|
||||||
|
}
|
||||||
|
if (w6) {
|
||||||
|
w6.__updateProps(props6, extra.fiber, undefined, undefined);
|
||||||
|
let pvnode = w6.__owl__.pvnode;
|
||||||
|
utils.defineProxy(vn9, pvnode);
|
||||||
|
} else {
|
||||||
|
let componentKey6 = \`routeComponent\`;
|
||||||
|
let W6 = context.constructor.components[componentKey6] || QWeb.components[componentKey6]|| context['routeComponent'];
|
||||||
|
if (!W6) {throw new Error('Cannot find the definition of component \\"' + componentKey6 + '\\"')}
|
||||||
|
w6 = new W6(parent, props6);
|
||||||
|
parent.__owl__.cmap[k7] = w6.__owl__.id;
|
||||||
|
let fiber = w6.__prepare(extra.fiber, undefined, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
|
||||||
|
let pvnode = h('dummy', {key: k7, hook: {remove() {},destroy(vn) {w6.destroy();}}});
|
||||||
|
utils.defineProxy(vn9, pvnode);
|
||||||
|
w6.__owl__.pvnode = pvnode;
|
||||||
|
}
|
||||||
|
w6.__owl__.parentLastFiberId = extra.fiber.id;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}"
|
||||||
|
`;
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Component } from "../../src/component/component";
|
import { Component } from "../../src/component/component";
|
||||||
import { Link } from "../../src/router/Link";
|
import { Link } from "../../src/router/link";
|
||||||
import { RouterEnv } from "../../src/router/Router";
|
import { RouterEnv } from "../../src/router/router";
|
||||||
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
|
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
|
||||||
import { TestRouter } from "./TestRouter";
|
import { TestRouter } from "./test_router";
|
||||||
|
|
||||||
describe("Link component", () => {
|
describe("Link component", () => {
|
||||||
let fixture: HTMLElement;
|
let fixture: HTMLElement;
|
||||||
@@ -12,6 +12,7 @@ describe("Link component", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = makeTestFixture();
|
fixture = makeTestFixture();
|
||||||
env = <RouterEnv>makeTestEnv();
|
env = <RouterEnv>makeTestEnv();
|
||||||
|
Component.env = env;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -30,15 +31,18 @@ describe("Link component", () => {
|
|||||||
</div>
|
</div>
|
||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
class App extends Component<any, any, any> {
|
class App extends Component<any, any> {
|
||||||
static components = { Link: Link };
|
static components = { Link: Link };
|
||||||
}
|
}
|
||||||
|
|
||||||
const routes = [{ name: "about", path: "/about" }, { name: "users", path: "/users" }];
|
const routes = [
|
||||||
|
{ name: "about", path: "/about" },
|
||||||
|
{ name: "users", path: "/users" }
|
||||||
|
];
|
||||||
|
|
||||||
router = new TestRouter(env, routes, { mode: "history" });
|
router = new TestRouter(env, routes, { mode: "history" });
|
||||||
router.navigate({ to: "users" });
|
router.navigate({ to: "users" });
|
||||||
const app = new App(env);
|
const app = new App();
|
||||||
await app.mount(fixture);
|
await app.mount(fixture);
|
||||||
expect(fixture.innerHTML).toBe('<div><a href="/about">About</a></div>');
|
expect(fixture.innerHTML).toBe('<div><a href="/about">About</a></div>');
|
||||||
|
|
||||||
@@ -61,15 +65,18 @@ describe("Link component", () => {
|
|||||||
</div>
|
</div>
|
||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
class App extends Component<any, any, any> {
|
class App extends Component<any, any> {
|
||||||
static components = { Link: Link };
|
static components = { Link: Link };
|
||||||
}
|
}
|
||||||
|
|
||||||
const routes = [{ name: "about", path: "/about" }, { name: "users", path: "/users" }];
|
const routes = [
|
||||||
|
{ name: "about", path: "/about" },
|
||||||
|
{ name: "users", path: "/users" }
|
||||||
|
];
|
||||||
|
|
||||||
router = new TestRouter(env, routes, { mode: "history" });
|
router = new TestRouter(env, routes, { mode: "history" });
|
||||||
router.navigate({ to: "users" });
|
router.navigate({ to: "users" });
|
||||||
const app = new App(env);
|
const app = new App();
|
||||||
await app.mount(fixture);
|
await app.mount(fixture);
|
||||||
|
|
||||||
expect(window.location.pathname).toBe("/users");
|
expect(window.location.pathname).toBe("/users");
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Component } from "../../src/component/component";
|
import { Component } from "../../src/component/component";
|
||||||
import { RouterEnv } from "../../src/router/Router";
|
import { RouterEnv } from "../../src/router/router";
|
||||||
import { RouteComponent } from "../../src/router/RouteComponent";
|
import { RouteComponent } from "../../src/router/route_component";
|
||||||
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
|
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
|
||||||
import { TestRouter } from "./TestRouter";
|
import { TestRouter } from "./test_router";
|
||||||
|
|
||||||
describe("RouteComponent", () => {
|
describe("RouteComponent", () => {
|
||||||
let fixture: HTMLElement;
|
let fixture: HTMLElement;
|
||||||
@@ -12,6 +12,7 @@ describe("RouteComponent", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = makeTestFixture();
|
fixture = makeTestFixture();
|
||||||
env = <RouterEnv>makeTestEnv();
|
env = <RouterEnv>makeTestEnv();
|
||||||
|
Component.env = env;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -32,9 +33,9 @@ describe("RouteComponent", () => {
|
|||||||
<span t-name="Users">Users</span>
|
<span t-name="Users">Users</span>
|
||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
class About extends Component<any, any, any> {}
|
class About extends Component<any, any> {}
|
||||||
class Users extends Component<any, any, any> {}
|
class Users extends Component<any, any> {}
|
||||||
class App extends Component<any, any, any> {
|
class App extends Component<any, any> {
|
||||||
static components = { RouteComponent };
|
static components = { RouteComponent };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +46,7 @@ describe("RouteComponent", () => {
|
|||||||
|
|
||||||
router = new TestRouter(env, routes, { mode: "history" });
|
router = new TestRouter(env, routes, { mode: "history" });
|
||||||
await router.navigate({ to: "about" });
|
await router.navigate({ to: "about" });
|
||||||
const app = new App(env);
|
const app = new App();
|
||||||
await app.mount(fixture);
|
await app.mount(fixture);
|
||||||
expect(fixture.innerHTML).toBe("<div><span>About</span></div>");
|
expect(fixture.innerHTML).toBe("<div><span>About</span></div>");
|
||||||
|
|
||||||
@@ -64,15 +65,15 @@ describe("RouteComponent", () => {
|
|||||||
<span t-name="Book">Book <t t-esc="props.title"/></span>
|
<span t-name="Book">Book <t t-esc="props.title"/></span>
|
||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
class Book extends Component<any, any, any> {}
|
class Book extends Component<any, any> {}
|
||||||
class App extends Component<any, any, any> {
|
class App extends Component<any, any> {
|
||||||
static components = { RouteComponent };
|
static components = { RouteComponent };
|
||||||
}
|
}
|
||||||
|
|
||||||
const routes = [{ name: "book", path: "/book/{{title}}", component: Book }];
|
const routes = [{ name: "book", path: "/book/{{title}}", component: Book }];
|
||||||
router = new TestRouter(env, routes, { mode: "history" });
|
router = new TestRouter(env, routes, { mode: "history" });
|
||||||
await router.navigate({ to: "book", params: { title: "1984" } });
|
await router.navigate({ to: "book", params: { title: "1984" } });
|
||||||
const app = new App(env);
|
const app = new App();
|
||||||
await app.mount(fixture);
|
await app.mount(fixture);
|
||||||
expect(fixture.innerHTML).toBe("<div><span>Book 1984</span></div>");
|
expect(fixture.innerHTML).toBe("<div><span>Book 1984</span></div>");
|
||||||
});
|
});
|
||||||
@@ -86,19 +87,19 @@ describe("RouteComponent", () => {
|
|||||||
<span t-name="Book">Book <t t-esc="props.title"/>|<t t-esc="incVal"/></span>
|
<span t-name="Book">Book <t t-esc="props.title"/>|<t t-esc="incVal"/></span>
|
||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
class Book extends Component<any, any, any> {
|
class Book extends Component<any, any> {
|
||||||
get incVal() {
|
get incVal() {
|
||||||
return this.props.val + 1;
|
return this.props.val + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class App extends Component<any, any, any> {
|
class App extends Component<any, any> {
|
||||||
static components = { RouteComponent };
|
static components = { RouteComponent };
|
||||||
}
|
}
|
||||||
|
|
||||||
const routes = [{ name: "book", path: "/book/{{title}}/{{val.number}}", component: Book }];
|
const routes = [{ name: "book", path: "/book/{{title}}/{{val.number}}", component: Book }];
|
||||||
router = new TestRouter(env, routes, { mode: "history" });
|
router = new TestRouter(env, routes, { mode: "history" });
|
||||||
await router.navigate({ to: "book", params: { title: "1984", val: "123" } });
|
await router.navigate({ to: "book", params: { title: "1984", val: "123" } });
|
||||||
const app = new App(env);
|
const app = new App();
|
||||||
await app.mount(fixture);
|
await app.mount(fixture);
|
||||||
expect(fixture.innerHTML).toBe("<div><span>Book 1984|124</span></div>");
|
expect(fixture.innerHTML).toBe("<div><span>Book 1984|124</span></div>");
|
||||||
});
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Destination, RouterEnv, Route } from "../../src/router/Router";
|
import { Destination, RouterEnv, Route } from "../../src/router/router";
|
||||||
import { makeTestEnv, nextTick } from "../helpers";
|
import { makeTestEnv, nextTick } from "../helpers";
|
||||||
import { TestRouter } from "./TestRouter";
|
import { TestRouter } from "./test_router";
|
||||||
|
|
||||||
let env: RouterEnv;
|
let env: RouterEnv;
|
||||||
let router: TestRouter | null = null;
|
let router: TestRouter | null = null;
|
||||||
@@ -52,6 +52,14 @@ describe("router miscellaneous", () => {
|
|||||||
expect(window.location.hash).toBe("#/users/5");
|
expect(window.location.hash).toBe("#/users/5");
|
||||||
expect(env.qweb.forceUpdate).toHaveBeenCalledTimes(2);
|
expect(env.qweb.forceUpdate).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("navigate in hash mode preserve location", async () => {
|
||||||
|
router = new TestRouter(env, [{ name: "users", path: "/users/{{id}}" }], { mode: "hash" });
|
||||||
|
window.history.pushState({}, "title", window.location.origin + "/test.html");
|
||||||
|
expect(window.location.href).toBe("http://localhost/test.html");
|
||||||
|
await router.navigate({ to: "users", params: { id: 3 } });
|
||||||
|
expect(window.location.href).toBe("http://localhost/test.html#/users/3");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("routeToPath", () => {
|
describe("routeToPath", () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Router } from "../../src/router/Router";
|
import { Router } from "../../src/router/router";
|
||||||
import { QWeb } from "../../src/qweb/index";
|
import { QWeb } from "../../src/qweb/index";
|
||||||
|
|
||||||
export class TestRouter extends Router {
|
export class TestRouter extends Router {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Env } from "../../src/component/component";
|
import { Env } from "../src/component/component";
|
||||||
import { Store, Getter } from "../../src/store/store";
|
import { Store, Getter } from "../src/store";
|
||||||
import { nextTick, nextMicroTick } from "../helpers";
|
import { nextTick, nextMicroTick } from "./helpers";
|
||||||
|
|
||||||
describe("basic use", () => {
|
describe("basic use", () => {
|
||||||
test("dispatch an action", () => {
|
test("dispatch an action", () => {
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`connecting a component to store connecting a component to a local store 1`] = `"<div></div>"`;
|
|
||||||
|
|
||||||
exports[`connecting a component to store connecting a component to a local store 2`] = `"<div><span>hello</span></div>"`;
|
|
||||||
|
|
||||||
exports[`connecting a component to store connecting a component works 1`] = `"<div></div>"`;
|
|
||||||
|
|
||||||
exports[`connecting a component to store connecting a component works 2`] = `"<div><span>hello</span></div>"`;
|
|
||||||
|
|
||||||
exports[`connecting a component to store deep and shallow connecting a component 1`] = `"<div><span>Kasteel</span></div>"`;
|
|
||||||
|
|
||||||
exports[`connecting a component to store deep and shallow connecting a component 2`] = `"<div><span>Kasteel</span></div>"`;
|
|
||||||
|
|
||||||
exports[`connecting a component to store deep and shallow connecting a component 3`] = `"<div><span>Bertinchamps</span></div>"`;
|
|
||||||
|
|
||||||
exports[`connecting a component to store deep and shallow connecting a component 4`] = `"<div><span>Kasteel</span></div>"`;
|
|
||||||
|
|
||||||
exports[`various scenarios scenarios with async store updates and some components events 1`] = `"<div><button>Do stuff</button><div><span>Attachment 100</span><span>Name: text.txt</span></div></div>"`;
|
|
||||||
|
|
||||||
exports[`various scenarios scenarios with async store updates and some components events 2`] = `"<div><button>Do stuff</button></div>"`;
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* We can only make one test per file, since the debug tool modify in place
|
||||||
|
* the owl object in a way that is difficult to undo.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { debugOwl } from "../../tools/debug";
|
||||||
|
import * as owl from "../../src/index";
|
||||||
|
|
||||||
|
import { Component, Env } from "../../src/component/component";
|
||||||
|
import { xml } from "../../src/tags";
|
||||||
|
import { useState } from "../../src/hooks";
|
||||||
|
import { makeTestFixture, makeTestEnv, nextTick } from "../helpers";
|
||||||
|
|
||||||
|
let fixture: HTMLElement = makeTestFixture();
|
||||||
|
let env: Env = makeTestEnv();
|
||||||
|
Component.env = env;
|
||||||
|
|
||||||
|
debugOwl(owl, {});
|
||||||
|
|
||||||
|
test("can log full lifecycle", async () => {
|
||||||
|
const steps: string[] = [];
|
||||||
|
const log = console.log;
|
||||||
|
console.log = arg => steps.push(arg);
|
||||||
|
|
||||||
|
class Child extends Component<any, any> {
|
||||||
|
static template = xml`<div>child</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static template = xml`<div><Child t-if="state.flag"/></div>`;
|
||||||
|
static components = { Child };
|
||||||
|
state = useState({ flag: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = new Parent(null, {});
|
||||||
|
await parent.mount(fixture);
|
||||||
|
|
||||||
|
parent.state.flag = true;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(steps).toEqual([
|
||||||
|
"[OWL_DEBUG] Parent<id=1> constructor, props={}",
|
||||||
|
"[OWL_DEBUG] Parent<id=1> mount",
|
||||||
|
"[OWL_DEBUG] Parent<id=1> willStart",
|
||||||
|
"[OWL_DEBUG] Parent<id=1> rendering template",
|
||||||
|
"[OWL_DEBUG] Parent<id=1> mounted",
|
||||||
|
"[OWL_DEBUG] Parent<id=1> render",
|
||||||
|
"[OWL_DEBUG] Parent<id=1> rendering template",
|
||||||
|
"[OWL_DEBUG] Child<id=2> constructor, props={}",
|
||||||
|
"[OWL_DEBUG] Child<id=2> willStart",
|
||||||
|
"[OWL_DEBUG] Child<id=2> rendering template",
|
||||||
|
"[OWL_DEBUG] Parent<id=1> willPatch",
|
||||||
|
"[OWL_DEBUG] Child<id=2> mounted",
|
||||||
|
"[OWL_DEBUG] Parent<id=1> patched"
|
||||||
|
]);
|
||||||
|
console.log = log;
|
||||||
|
});
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* We can only make one test per file, since the debug tool modify in place
|
||||||
|
* the owl object in a way that is difficult to undo.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { debugOwl } from "../../tools/debug";
|
||||||
|
import * as owl from "../../src/index";
|
||||||
|
|
||||||
|
import { Component, Env } from "../../src/component/component";
|
||||||
|
import { xml } from "../../src/tags";
|
||||||
|
import { makeTestFixture, makeTestEnv } from "../helpers";
|
||||||
|
|
||||||
|
let fixture: HTMLElement = makeTestFixture();
|
||||||
|
let env: Env = makeTestEnv();
|
||||||
|
Component.env = env;
|
||||||
|
|
||||||
|
debugOwl(owl, { logScheduler: true });
|
||||||
|
|
||||||
|
test("can log scheduler start and stop", async () => {
|
||||||
|
const steps: string[] = [];
|
||||||
|
const log = console.log;
|
||||||
|
console.log = arg => steps.push(arg);
|
||||||
|
|
||||||
|
class Child extends Component<any, any> {
|
||||||
|
static template = xml`<div>child</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Component<any, any> {
|
||||||
|
static template = xml`<div><Child /></div>`;
|
||||||
|
static components = { Child };
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = new Parent(null, {});
|
||||||
|
await parent.mount(fixture);
|
||||||
|
|
||||||
|
expect(steps).toEqual([
|
||||||
|
"[OWL_DEBUG] Parent<id=1> constructor, props={}",
|
||||||
|
"[OWL_DEBUG] Parent<id=1> mount",
|
||||||
|
"[OWL_DEBUG] Parent<id=1> willStart",
|
||||||
|
"[OWL_DEBUG] scheduler: start running tasks queue",
|
||||||
|
"[OWL_DEBUG] Parent<id=1> rendering template",
|
||||||
|
"[OWL_DEBUG] Child<id=2> constructor, props={}",
|
||||||
|
"[OWL_DEBUG] Child<id=2> willStart",
|
||||||
|
"[OWL_DEBUG] Child<id=2> rendering template",
|
||||||
|
"[OWL_DEBUG] Child<id=2> mounted",
|
||||||
|
"[OWL_DEBUG] Parent<id=1> mounted",
|
||||||
|
"[OWL_DEBUG] scheduler: stop running tasks queue"
|
||||||
|
]);
|
||||||
|
console.log = log;
|
||||||
|
});
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
/**
|
||||||
|
* Doc Link Checker
|
||||||
|
*
|
||||||
|
* We define here a test to make sure that there are no dead link in the Owl
|
||||||
|
* documentation.
|
||||||
|
*/
|
||||||
|
import * as fs from "fs";
|
||||||
|
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
|
interface MarkDownLink {
|
||||||
|
name: string;
|
||||||
|
link: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MarkDownSection {
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FileData {
|
||||||
|
name: string;
|
||||||
|
path: string[];
|
||||||
|
fullName: string;
|
||||||
|
links: MarkDownLink[];
|
||||||
|
sections: MarkDownSection[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const LINK_REGEXP = /\[([^\[]+)\]\(([^\)]+)\)/g;
|
||||||
|
const HEADING_REGEXP = /\n(#+\s*)(.*)/g;
|
||||||
|
|
||||||
|
export function addMardownData(fileData): void {
|
||||||
|
const sep = fileData.path.length > 0 ? "/" : "";
|
||||||
|
const fullName = fileData.path.join("/") + sep + fileData.name;
|
||||||
|
const content = fs.readFileSync(fullName, { encoding: "utf8" });
|
||||||
|
let m;
|
||||||
|
// get links info
|
||||||
|
do {
|
||||||
|
m = LINK_REGEXP.exec(content);
|
||||||
|
if (m) {
|
||||||
|
fileData.links.push({ name: m[0], link: m[2] });
|
||||||
|
}
|
||||||
|
} while (m);
|
||||||
|
// get sections info
|
||||||
|
do {
|
||||||
|
m = HEADING_REGEXP.exec(content);
|
||||||
|
if (m) {
|
||||||
|
fileData.sections.push({ name: m[0], slug: slugify(m[2]) });
|
||||||
|
}
|
||||||
|
} while (m);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of FileData corresponding to all files that need to be
|
||||||
|
* validated.
|
||||||
|
*/
|
||||||
|
function getFiles(path: string[] = []): FileData[] {
|
||||||
|
if (path.length === 0) {
|
||||||
|
const baseFiles: FileData[] = [
|
||||||
|
{ name: "README.md", path: [], links: [], sections: [], fullName: "README.md" },
|
||||||
|
{ name: "roadmap.md", path: [], links: [], sections: [], fullName: "roadmap.md" }
|
||||||
|
];
|
||||||
|
const rest = getFiles(["doc"]);
|
||||||
|
const result = baseFiles.concat(rest);
|
||||||
|
result.forEach(addMardownData);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const files = fs.readdirSync(path.join("/"), { withFileTypes: true }).map(f => {
|
||||||
|
if (f.isDirectory()) {
|
||||||
|
return getFiles(path.concat(f.name));
|
||||||
|
}
|
||||||
|
const fullName = path.join("/") + (path.length > 0 ? "/" : "") + f.name;
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: f.name,
|
||||||
|
path,
|
||||||
|
links: [],
|
||||||
|
sections: [],
|
||||||
|
fullName
|
||||||
|
}
|
||||||
|
];
|
||||||
|
});
|
||||||
|
return Array.prototype.concat(...files);
|
||||||
|
}
|
||||||
|
|
||||||
|
const LOCAL_FILES = ["LICENSE", "tools/debug.js"];
|
||||||
|
export function isLinkValid(link: MarkDownLink, current: FileData, files: FileData[]): boolean {
|
||||||
|
if (link.link.startsWith("http")) {
|
||||||
|
// no check on external links
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Step 1: extract path, name, hash
|
||||||
|
// path = ['doc', 'architecture]
|
||||||
|
// name = 'rendering.md'
|
||||||
|
// hash = 'blabla' (or '' if no hash)
|
||||||
|
|
||||||
|
const parts = link.link.split("#");
|
||||||
|
const hash = parts[1] || "";
|
||||||
|
let name;
|
||||||
|
let path;
|
||||||
|
if (parts[0]) {
|
||||||
|
let temp = parts[0].split("/");
|
||||||
|
name = temp[temp.length - 1];
|
||||||
|
temp.splice(-1);
|
||||||
|
path = current.path.slice();
|
||||||
|
for (let elem of temp) {
|
||||||
|
if (elem === "..") {
|
||||||
|
path.splice(-1);
|
||||||
|
} else if (elem !== ".") {
|
||||||
|
path.push(elem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// there are no file name, so this is a relative link to the current file
|
||||||
|
name = current.name;
|
||||||
|
path = current.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: build normalized link file name
|
||||||
|
const linkFullName = path.join("/") + (path.length > 0 ? "/" : "") + name;
|
||||||
|
|
||||||
|
// Step 3: check link name against white list of local files
|
||||||
|
if (LOCAL_FILES.includes(linkFullName)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: check if there is a matching file
|
||||||
|
let target: FileData | undefined = files.find(f => f.fullName === linkFullName);
|
||||||
|
if (!target) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 5: if necessary, check if there is a corresponding link inside the target
|
||||||
|
// link name
|
||||||
|
if (hash) {
|
||||||
|
if (!target.sections.find(s => s.slug === hash)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// adapted from https://medium.com/@mhagemann/the-ultimate-way-to-slugify-a-url-string-in-javascript-b8e4a0d849e1
|
||||||
|
function slugify(str) {
|
||||||
|
const a = "àáäâãåăæçèéëêǵḧìíïîḿńǹñòóöôœøṕŕßśșțùúüûǘẃẍÿź·_,:;";
|
||||||
|
const b = "aaaaaaaaceeeeghiiiimnnnooooooprssstuuuuuwxyz-----";
|
||||||
|
const p = new RegExp(a.split("").join("|"), "g");
|
||||||
|
return str
|
||||||
|
.toString()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\//g, "") // remove /
|
||||||
|
.replace(/\s+/g, "-") // Replace spaces with -
|
||||||
|
.replace(p, c => b.charAt(a.indexOf(c))) // Replace special characters
|
||||||
|
.replace(/&/g, "-and-") // Replace & with ‘and’
|
||||||
|
.replace(/[^\w\-]+/g, "") // Remove all non-word characters
|
||||||
|
.replace(/\-\-+/g, "-") // Replace multiple - with single -
|
||||||
|
.replace(/^-+/, "") // Trim - from start of text
|
||||||
|
.replace(/-+$/, ""); // Trim - from end of text
|
||||||
|
}
|
||||||
|
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
// Test
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
|
test("All markdown links work", () => {
|
||||||
|
let linkNumber = 0;
|
||||||
|
let invalidLinkNumber = 0;
|
||||||
|
const data = getFiles();
|
||||||
|
for (let file of data) {
|
||||||
|
for (let link of file.links) {
|
||||||
|
linkNumber++;
|
||||||
|
if (!isLinkValid(link, file, data)) {
|
||||||
|
console.warn(`Invalid Link: "${link.name}" in "${file.name}"`);
|
||||||
|
invalidLinkNumber++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(invalidLinkNumber).toBe(0);
|
||||||
|
expect(linkNumber).toBeGreaterThan(10);
|
||||||
|
});
|
||||||
+57
-59
@@ -1,5 +1,6 @@
|
|||||||
import { h, patch } from "../src/vdom";
|
import { h, patch } from "../src/vdom";
|
||||||
import { init } from "../src/vdom/vdom";
|
import { htmlToVDOM } from "../src/vdom/html_to_vdom";
|
||||||
|
import { init, addNS } from "../src/vdom/vdom";
|
||||||
|
|
||||||
function map(list, fn) {
|
function map(list, fn) {
|
||||||
var ret: any[] = [];
|
var ret: any[] = [];
|
||||||
@@ -184,20 +185,6 @@ describe("snabbdom", function() {
|
|||||||
expect(elm.tagName).toBe("DIV");
|
expect(elm.tagName).toBe("DIV");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("has different tag and id", function() {
|
|
||||||
let elm: any = document.createElement("div");
|
|
||||||
vnode0.appendChild(elm);
|
|
||||||
const vnode1 = h("span#id");
|
|
||||||
elm = patch(elm, vnode1).elm;
|
|
||||||
expect(elm.tagName).toBe("SPAN");
|
|
||||||
expect(elm.id).toBe("id");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("has id", function() {
|
|
||||||
elm = patch(vnode0, h("div", [h("div#unique")])).elm;
|
|
||||||
expect(elm.firstChild.id).toBe("unique");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("has correct namespace", function() {
|
test("has correct namespace", function() {
|
||||||
const SVGNamespace = "http://www.w3.org/2000/svg";
|
const SVGNamespace = "http://www.w3.org/2000/svg";
|
||||||
const XHTMLNamespace = "http://www.w3.org/1999/xhtml";
|
const XHTMLNamespace = "http://www.w3.org/1999/xhtml";
|
||||||
@@ -206,33 +193,15 @@ describe("snabbdom", function() {
|
|||||||
expect(elm.firstChild.namespaceURI).toBe(SVGNamespace);
|
expect(elm.firstChild.namespaceURI).toBe(SVGNamespace);
|
||||||
|
|
||||||
// verify that svg tag automatically gets svg namespace
|
// verify that svg tag automatically gets svg namespace
|
||||||
elm = patch(vnode0, h("svg", [h("foreignObject", [h("div", ["I am HTML embedded in SVG"])])]))
|
const vnode = h("svg", [h("foreignObject", [h("div", ["I am HTML embedded in SVG"])])]);
|
||||||
.elm;
|
// need to add namespace manually. it is usually done by the template
|
||||||
|
// compiler
|
||||||
|
addNS(vnode.data, (vnode as any).children, vnode.sel);
|
||||||
|
|
||||||
|
elm = patch(vnode0, vnode).elm;
|
||||||
expect(elm.namespaceURI).toBe(SVGNamespace);
|
expect(elm.namespaceURI).toBe(SVGNamespace);
|
||||||
expect(elm.firstChild.namespaceURI).toBe(SVGNamespace);
|
expect(elm.firstChild.namespaceURI).toBe(SVGNamespace);
|
||||||
expect(elm.firstChild.firstChild.namespaceURI).toBe(XHTMLNamespace);
|
expect(elm.firstChild.firstChild.namespaceURI).toBe(XHTMLNamespace);
|
||||||
|
|
||||||
// verify that svg tag with extra selectors gets svg namespace
|
|
||||||
elm = patch(vnode0, h("svg#some-id")).elm;
|
|
||||||
expect(elm.namespaceURI).toBe(SVGNamespace);
|
|
||||||
|
|
||||||
// verify that non-svg tag beginning with 'svg' does NOT get namespace
|
|
||||||
elm = patch(vnode0, h("svg-custom-el")).elm;
|
|
||||||
expect(elm.namespaceURI).not.toBe(SVGNamespace);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("receives classes in selector", function() {
|
|
||||||
elm = patch(vnode0, h("div", [h("i.am.a.class")])).elm;
|
|
||||||
expect(elm.firstChild.classList.contains("am")).toBeTruthy();
|
|
||||||
expect(elm.firstChild.classList.contains("a")).toBeTruthy();
|
|
||||||
expect(elm.firstChild.classList.contains("class")).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("receives classes in selector when namespaced", function() {
|
|
||||||
elm = patch(vnode0, h("svg", [h("g.am.a.class.too")])).elm;
|
|
||||||
expect(elm.firstChild.classList.contains("am")).toBeTruthy();
|
|
||||||
expect(elm.firstChild.classList.contains("a")).toBeTruthy();
|
|
||||||
expect(elm.firstChild.classList.contains("class")).toBeTruthy();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can create elements with text content", function() {
|
test("can create elements with text content", function() {
|
||||||
@@ -271,26 +240,6 @@ describe("snabbdom", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("patching an element", function() {
|
describe("patching an element", function() {
|
||||||
test("changes the elements classes", function() {
|
|
||||||
const vnode1 = h("i.i.am.horse");
|
|
||||||
const vnode2 = h("i.i.am");
|
|
||||||
patch(vnode0, vnode1);
|
|
||||||
elm = patch(vnode1, vnode2).elm;
|
|
||||||
expect(elm.classList.contains("i")).toBeTruthy();
|
|
||||||
expect(elm.classList.contains("am")).toBeTruthy();
|
|
||||||
expect(!elm.classList.contains("horse")).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("removes missing classes", function() {
|
|
||||||
const vnode1 = h("i.i.am.horse");
|
|
||||||
const vnode2 = h("i.i.am");
|
|
||||||
patch(vnode0, vnode1);
|
|
||||||
elm = patch(vnode1, vnode2).elm;
|
|
||||||
expect(elm.classList.contains("i")).toBeTruthy();
|
|
||||||
expect(elm.classList.contains("am")).toBeTruthy();
|
|
||||||
expect(!elm.classList.contains("horse")).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("changes an elements props", function() {
|
test("changes an elements props", function() {
|
||||||
const vnode1 = h("a", { props: { src: "http://other/" } });
|
const vnode1 = h("a", { props: { src: "http://other/" } });
|
||||||
const vnode2 = h("a", { props: { src: "http://localhost/" } });
|
const vnode2 = h("a", { props: { src: "http://localhost/" } });
|
||||||
@@ -1108,3 +1057,52 @@ describe("snabbdom", function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Html to vdom
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
describe("html to vdom", function() {
|
||||||
|
let elm, vnode0;
|
||||||
|
beforeEach(function() {
|
||||||
|
elm = document.createElement("div");
|
||||||
|
vnode0 = elm;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("empty strings return empty list", function() {
|
||||||
|
expect(htmlToVDOM("")).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("just text", function() {
|
||||||
|
const nodeList = htmlToVDOM("simple text");
|
||||||
|
expect(nodeList).toHaveLength(1);
|
||||||
|
expect(nodeList[0]).toEqual({ text: "simple text" });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("empty tag", function() {
|
||||||
|
const nodeList = htmlToVDOM("<span></span>");
|
||||||
|
expect(nodeList).toHaveLength(1);
|
||||||
|
elm = patch(vnode0, nodeList[0]).elm;
|
||||||
|
expect(elm.outerHTML).toEqual("<span></span>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("tag with text", function() {
|
||||||
|
const nodeList = htmlToVDOM("<span>abc</span>");
|
||||||
|
expect(nodeList).toHaveLength(1);
|
||||||
|
elm = patch(vnode0, nodeList[0]).elm;
|
||||||
|
expect(elm.outerHTML).toEqual("<span>abc</span>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("tag with attribute", function() {
|
||||||
|
const nodeList = htmlToVDOM(`<span a="1" b="2">abc</span>`);
|
||||||
|
expect(nodeList).toHaveLength(1);
|
||||||
|
elm = patch(vnode0, nodeList[0]).elm;
|
||||||
|
expect(elm.outerHTML).toEqual(`<span a="1" b="2">abc</span>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("misc", function() {
|
||||||
|
const nodeList = htmlToVDOM(`<span a="1" b="2">abc<div>1</div></span>`);
|
||||||
|
expect(nodeList).toHaveLength(1);
|
||||||
|
elm = patch(vnode0, nodeList[0]).elm;
|
||||||
|
expect(elm.outerHTML).toEqual(`<span a="1" b="2">abc<div>1</div></span>`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Likes Counter Widget
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
class Counter extends owl.Component {
|
||||||
|
state = { counter: 0 };
|
||||||
|
|
||||||
|
increment() {
|
||||||
|
this.state.counter++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Message Widget
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
class Message extends owl.Component {
|
||||||
|
shouldUpdate(nextProps) {
|
||||||
|
return nextProps.message !== this.props.message;
|
||||||
|
}
|
||||||
|
removeMessage() {
|
||||||
|
this.trigger("remove-message", {
|
||||||
|
id: this.props.message.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message.components = { Counter };
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Root Widget
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
class App extends owl.Component {
|
||||||
|
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
benchmark(message, fn, callback) {
|
||||||
|
if (this.state.multipleFlag) {
|
||||||
|
const N = 20;
|
||||||
|
let n = N;
|
||||||
|
let total = 0;
|
||||||
|
let cb = info => {
|
||||||
|
let finalize = () => {
|
||||||
|
n--;
|
||||||
|
total += info.delta;
|
||||||
|
if (n === 0) {
|
||||||
|
const avg = total / N;
|
||||||
|
this.log(`Average: ${formatNumber(avg)}ms`, true);
|
||||||
|
if (callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this._benchmark(message, fn, cb);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.state.clearAfterFlag) {
|
||||||
|
this._benchmark(
|
||||||
|
"clear",
|
||||||
|
() => {
|
||||||
|
this.state.messages = [];
|
||||||
|
},
|
||||||
|
finalize,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
finalize();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this._benchmark(message, fn, cb);
|
||||||
|
} else {
|
||||||
|
this._benchmark(message, fn, callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_benchmark(message, fn, cb, log = true) {
|
||||||
|
setTimeout(() => {
|
||||||
|
startMeasure(message);
|
||||||
|
fn();
|
||||||
|
stopMeasure(info => {
|
||||||
|
if (log) {
|
||||||
|
this.log(info.msg);
|
||||||
|
}
|
||||||
|
if (cb) {
|
||||||
|
cb(info);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
addMessages(n) {
|
||||||
|
this.benchmark("add " + n, () => {
|
||||||
|
const newMessages = buildData(n);
|
||||||
|
this.state.messages.push.apply(this.state.messages, newMessages);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this._benchmark("clear", () => {
|
||||||
|
this.state.messages = [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSomeMessages() {
|
||||||
|
this.benchmark("update every 10th", () => {
|
||||||
|
const messages = this.state.messages;
|
||||||
|
for (let i = 0; i < messages.length; i += 10) {
|
||||||
|
const msg = Object.assign({}, messages[i]);
|
||||||
|
msg.author += "!!!";
|
||||||
|
messages[i] = msg;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeMessage(event) {
|
||||||
|
this.benchmark("remove message", () => {
|
||||||
|
const index = this.state.messages.findIndex(m => m.id === event.detail.id);
|
||||||
|
this.state.messages.splice(index, 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
log(str, isBold) {
|
||||||
|
const div = document.createElement("div");
|
||||||
|
if (isBold) {
|
||||||
|
div.classList.add("bold");
|
||||||
|
}
|
||||||
|
div.textContent = `> ${str}`;
|
||||||
|
this.refs.log.appendChild(div);
|
||||||
|
this.refs.log.scrollTop = this.refs.log.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearLog() {
|
||||||
|
this.refs.log.innerHTML = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
App.components = { Message };
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Application initialization
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
async function start() {
|
||||||
|
const templates = await owl.utils.loadTemplates("templates.xml");
|
||||||
|
const env = {
|
||||||
|
qweb: new owl.QWeb(templates)
|
||||||
|
};
|
||||||
|
const app = new App(env);
|
||||||
|
app.mount(document.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
start();
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>OWL 0.21.0 Benchmark</title>
|
||||||
|
<link href="../shared/main.css" rel="stylesheet"/>
|
||||||
|
<script src='owl.js'></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src='app.js' type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
|||||||
|
<templates>
|
||||||
|
<div t-name="App" class="main">
|
||||||
|
<div class="left-thing">
|
||||||
|
<div class="title">Actions</div>
|
||||||
|
<div class="panel">
|
||||||
|
<button t-on-click="addMessages(100)">Add 100 messages</button>
|
||||||
|
<button t-on-click="addMessages(1000)">Add 1k messages</button>
|
||||||
|
<button t-on-click="addMessages(10000)">Add 10k messages</button>
|
||||||
|
<button t-on-click="addMessages(30000)">Add 30k messages</button>
|
||||||
|
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
|
||||||
|
<button t-on-click="clear">Clear</button>
|
||||||
|
</div>
|
||||||
|
<div class="flags">
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" id="multipleflag" t-model="multipleFlag"/>
|
||||||
|
<label for="multipleflag">Do it 20x</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" id="clearFlag" t-model="clearAfterFlag" />
|
||||||
|
<label for="clearFlag">Clear after</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
|
||||||
|
<hr/>
|
||||||
|
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
|
||||||
|
<div class="log">
|
||||||
|
<div class="log-content" t-ref="log"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="right-thing">
|
||||||
|
<div class="content" t-on-remove-message="removeMessage">
|
||||||
|
<t t-foreach="state.messages" t-as="message">
|
||||||
|
<Message t-key="message.id" message="message"/>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div t-name="Message" class="message">
|
||||||
|
<span class="author"><t t-esc="props.message.author"/></span>
|
||||||
|
<span class="msg"><t t-esc="props.message.msg"/></span>
|
||||||
|
<button class="remove" t-on-click="removeMessage">Remove</button>
|
||||||
|
<Counter/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div t-name="Counter">
|
||||||
|
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
|
||||||
|
|
||||||
|
const { useState, useRef } = owl.hooks;
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Likes Counter Widget
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
class Counter extends owl.Component {
|
||||||
|
state = useState({ counter: 0 });
|
||||||
|
|
||||||
|
increment() {
|
||||||
|
this.state.counter++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Message Widget
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
class Message extends owl.Component {
|
||||||
|
static components = { Counter };
|
||||||
|
|
||||||
|
shouldUpdate(nextProps) {
|
||||||
|
return nextProps.message !== this.props.message;
|
||||||
|
}
|
||||||
|
removeMessage() {
|
||||||
|
this.trigger("remove-message", {
|
||||||
|
id: this.props.message.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Root Widget
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
class App extends owl.Component {
|
||||||
|
static components = { Message };
|
||||||
|
state = useState({ messages: [], multipleFlag: false, clearAfterFlag: false });
|
||||||
|
logRef = useRef("log");
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
benchmark(message, fn, callback) {
|
||||||
|
if (this.state.multipleFlag) {
|
||||||
|
const N = 20;
|
||||||
|
let n = N;
|
||||||
|
let total = 0;
|
||||||
|
let cb = info => {
|
||||||
|
let finalize = () => {
|
||||||
|
n--;
|
||||||
|
total += info.delta;
|
||||||
|
if (n === 0) {
|
||||||
|
const avg = total / N;
|
||||||
|
this.log(`Average: ${formatNumber(avg)}ms`, true);
|
||||||
|
if (callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this._benchmark(message, fn, cb);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.state.clearAfterFlag) {
|
||||||
|
this._benchmark(
|
||||||
|
"clear",
|
||||||
|
() => {
|
||||||
|
this.state.messages = [];
|
||||||
|
},
|
||||||
|
finalize,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
finalize();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this._benchmark(message, fn, cb);
|
||||||
|
} else {
|
||||||
|
this._benchmark(message, fn, callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_benchmark(message, fn, cb, log = true) {
|
||||||
|
setTimeout(() => {
|
||||||
|
startMeasure(message);
|
||||||
|
fn();
|
||||||
|
stopMeasure(info => {
|
||||||
|
if (log) {
|
||||||
|
this.log(info.msg);
|
||||||
|
}
|
||||||
|
if (cb) {
|
||||||
|
cb(info);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
addMessages(n) {
|
||||||
|
this.benchmark("add " + n, () => {
|
||||||
|
const newMessages = buildData(n);
|
||||||
|
this.state.messages.push.apply(this.state.messages, newMessages);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this._benchmark("clear", () => {
|
||||||
|
this.state.messages = [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSomeMessages() {
|
||||||
|
this.benchmark("update every 10th", () => {
|
||||||
|
const messages = this.state.messages;
|
||||||
|
for (let i = 0; i < messages.length; i += 10) {
|
||||||
|
const msg = Object.assign({}, messages[i]);
|
||||||
|
msg.author += "!!!";
|
||||||
|
messages[i] = msg;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeMessage(event) {
|
||||||
|
this.benchmark("remove message", () => {
|
||||||
|
const index = this.state.messages.findIndex(m => m.id === event.detail.id);
|
||||||
|
this.state.messages.splice(index, 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
log(str, isBold) {
|
||||||
|
const div = document.createElement("div");
|
||||||
|
if (isBold) {
|
||||||
|
div.classList.add("bold");
|
||||||
|
}
|
||||||
|
div.textContent = `> ${str}`;
|
||||||
|
this.logRef.el.appendChild(div);
|
||||||
|
this.logRef.el.scrollTop = this.logRef.el.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearLog() {
|
||||||
|
this.logRef.el.innerHTML = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Application initialization
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
async function start() {
|
||||||
|
const templates = await owl.utils.loadFile("templates.xml");
|
||||||
|
const env = {
|
||||||
|
qweb: new owl.QWeb(templates)
|
||||||
|
};
|
||||||
|
const app = new App(env);
|
||||||
|
app.mount(document.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
start();
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>OWL v0.24.0 Benchmark</title>
|
||||||
|
<link href="../shared/main.css" rel="stylesheet"/>
|
||||||
|
<script src='owl.js'></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src='app.js' type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user