mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
241 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 14ceb382ef | |||
| 9846b2e997 | |||
| cfc2fb2ba2 | |||
| d57017b6ed | |||
| 5bf7a5c671 | |||
| 9b589af75a | |||
| 471fd49063 | |||
| 4fc3423682 | |||
| ffb3263c79 | |||
| e1acf66143 | |||
| 2a40907e35 | |||
| 1c0dd79955 | |||
| 9fad164c79 | |||
| 53a8841914 | |||
| 19f0863aa3 | |||
| 28f7b44f9e | |||
| fe46a6ccee | |||
| 3c23a7b246 | |||
| c52799de2c | |||
| 4edb90f44b | |||
| 0c1a7aabae | |||
| cfdce29ce7 | |||
| b344d73e07 | |||
| 4cd08d25c8 | |||
| f9861040c8 | |||
| 40d8090232 | |||
| 55be6437f1 | |||
| 6ff4dd74bf | |||
| 3628b49c37 | |||
| f289cb17ec | |||
| e6a3ede4a6 | |||
| 10cfe0b740 | |||
| a3d563ebb4 | |||
| 3b1247d476 | |||
| beb6c2bc82 | |||
| f0cd6f61cd | |||
| c950b385a0 | |||
| c172b31481 | |||
| 9b3a97e732 | |||
| 670c98916c | |||
| c59abef374 | |||
| bb4c2506c0 | |||
| 9843d16585 | |||
| 34e463ad0a | |||
| 2294c1967f | |||
| 4b53e48e91 | |||
| 70a70161e7 | |||
| e690991a6a | |||
| a621a14028 | |||
| 9754cc7521 | |||
| aa268937a7 | |||
| 3db6f0a643 | |||
| 635a7fc72c | |||
| 6cd5ec10f0 | |||
| 0a276fbf1d | |||
| d593131cc1 | |||
| 3c3dcb44a9 | |||
| 6ecd1d6ea2 | |||
| 1a931a2a08 | |||
| bf705fcd99 | |||
| bb46346767 | |||
| cd0762b436 | |||
| ca920a0091 | |||
| e5b1ba24d8 | |||
| eaf912bed7 | |||
| fd0fb5be00 | |||
| f9f730ab7b | |||
| b30b72fc36 | |||
| ab08e84bff | |||
| cd9cac1246 | |||
| f6369f9750 | |||
| f18c73e59f | |||
| 93d0a7dd46 | |||
| b1d022b583 | |||
| 5382e824b1 | |||
| 15a3f59744 | |||
| 4a90ec7ff0 | |||
| 9f9b6b174e | |||
| 545ceefc3d | |||
| 591508e769 | |||
| d9cbd23cd9 | |||
| f4cd111cc8 | |||
| ea015af741 | |||
| bfc7c81c0d | |||
| e24ff8f8aa | |||
| 9f1e64d399 | |||
| 8e1aa71436 | |||
| be2fc965b1 | |||
| 7245ccf8f9 | |||
| 1c8e1af86d | |||
| 60a6cca960 | |||
| ad55b42ccb | |||
| 50c0a4b126 | |||
| 2ca42e7470 | |||
| d381e85d94 | |||
| af7520d869 | |||
| 0095bfa61f | |||
| 1b12cf9b91 | |||
| dbfc7e4acd | |||
| e838e879c0 | |||
| ee63f6bb0f | |||
| 16bbb8bc9f | |||
| 4a5db0c283 | |||
| cc50a2e3bb | |||
| 1e2b204fdd | |||
| 4a889b7b6b | |||
| 34695883c2 | |||
| 3d2e2a1873 | |||
| af6aca83a2 | |||
| 8c8ffb6a6b | |||
| 63a8fcd7e2 | |||
| e6a5934162 | |||
| 5524c2e323 | |||
| 35c1de26b8 | |||
| d1cf6b1b8d | |||
| bfa2c681cd | |||
| 609c108607 | |||
| a56939f13a | |||
| 5ff501f8b8 | |||
| 60917bb102 | |||
| b201ef8162 | |||
| 9cacc78ad7 | |||
| 0d933b8570 | |||
| 72f6daa695 | |||
| 567d78a9ac | |||
| f013c57050 | |||
| b9fae87273 | |||
| 1f0457f69b | |||
| c481a73a76 | |||
| e64e415b3b | |||
| aa406b53e2 | |||
| 57027bd0fa | |||
| 671c662fee | |||
| 0ae70b1d63 | |||
| 2701a7d861 | |||
| 3ebe985c3c | |||
| 1c0d4b5832 | |||
| 2840794fa2 | |||
| 710f42d4e4 | |||
| 45a2b0122d | |||
| 4c41f62364 | |||
| c2ab9774fb | |||
| 22e48e3be0 | |||
| 1ff3c32fe4 | |||
| 9a3e1ed99e | |||
| 438e2fbc2e | |||
| b3db085745 | |||
| b948c59c6d | |||
| 6426fe98f9 | |||
| 4dd9de349e | |||
| e1bcea77b5 | |||
| 2a19aeb6a3 | |||
| b53a34b7d1 | |||
| f881640d3d | |||
| 54c3dd76e4 | |||
| 7b887447da | |||
| 438b21df3f | |||
| d1094f647a | |||
| 49baf0de6e | |||
| b14d069ee7 | |||
| e14a4fe338 | |||
| 1f9ec46236 | |||
| 1464a3631b | |||
| 7d3c374b78 | |||
| 1dd257f33c | |||
| 1206afe6be | |||
| 5842ed51b0 | |||
| d22219f084 | |||
| 0be1a54e03 | |||
| 32921ecc20 | |||
| 1c290c1172 | |||
| 2f9d7ea58f | |||
| fe23e76341 | |||
| 73023d4869 | |||
| ff51443b8e | |||
| f8e07f6d30 | |||
| 00da778753 | |||
| 187eb922c0 | |||
| 466c12a0e6 | |||
| 8dc3ec94bf | |||
| a554436d03 | |||
| 34b5049dee | |||
| 0eccfd08fe | |||
| 98b063fd70 | |||
| e9ba94ae55 | |||
| 4d72a4f240 | |||
| a08deb895d | |||
| 60cea14b01 |
+6
-1
@@ -1,5 +1,6 @@
|
||||
/node_modules
|
||||
/dist
|
||||
|
||||
npm-debug.log
|
||||
|
||||
# misc
|
||||
@@ -14,8 +15,12 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
package-lock.json
|
||||
|
||||
#ide's
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
node_modules
|
||||
|
||||
# Extras temp file
|
||||
/extras/owl.js
|
||||
/tools/owl.js
|
||||
@@ -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,19 +1,71 @@
|
||||
<h1 align="center">🦉 <a href="https://odoo.github.io/owl/">Odoo Web Library</a> 🦉</h1>
|
||||
|
||||
_A web framework for structured, dynamic and maintainable applications_
|
||||
_A no nonsense web framework for structured, dynamic and maintainable applications_
|
||||
|
||||
## Project Overview
|
||||
|
||||
The Odoo Web Library (OWL) is a small
|
||||
UI framework intended to be the basis for the [Odoo](https://www.odoo.com/) Web Client, and hopefully many
|
||||
other Odoo related projects. OWL's main feature is a _declarative component system_, with QWeb as a template engine, asynchronous rendering, and an underlying virtual dom.
|
||||
The Odoo Web Library (OWL) is a smallish (~17kb gzipped) UI framework intended to
|
||||
be the basis for the [Odoo](https://www.odoo.com/) Web Client. Owl is a modern
|
||||
framework, written in Typescript, taking the best ideas from React and Vue in a
|
||||
simple and consistent way. Owl's main features are:
|
||||
|
||||
**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.
|
||||
- a declarative component system,
|
||||
- a reactivity system based on hooks,
|
||||
- a store implementation (for state management),
|
||||
- a small frontend router
|
||||
|
||||
## OWL's design principles
|
||||
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.
|
||||
|
||||
## Example
|
||||
|
||||
Here is a short example to illustrate interactive components:
|
||||
|
||||
```javascript
|
||||
import { Component, QWeb, useState } from "owl";
|
||||
import { xml } from "owl/tags";
|
||||
|
||||
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++;
|
||||
}
|
||||
}
|
||||
|
||||
class App extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<span>Hello Owl</span>
|
||||
<Counter />
|
||||
</div>`;
|
||||
|
||||
static components = { Counter };
|
||||
}
|
||||
|
||||
const app = new App({ qweb: new QWeb() });
|
||||
app.mount(document.body);
|
||||
```
|
||||
|
||||
Note that the counter component is made reactive with the [`useState`](doc/hooks.md#usestate)
|
||||
hook. Also, all examples here uses the `xml` helper to define inline templates.
|
||||
But this is not mandatory, many applications will load templates separately.
|
||||
|
||||
More interesting examples can be found on the
|
||||
[playground](https://odoo.github.io/owl/playground) application.
|
||||
|
||||
## Design Principles
|
||||
|
||||
OWL is designed to be used in highly dynamic applications where changing
|
||||
requirements are common, code needs to be maintained by large teams.
|
||||
requirements are common and code needs to be maintained by large teams.
|
||||
|
||||
- **XML based**: templates are based on the XML format, which allows interesting
|
||||
applications. For example, they could be stored in a database and modified
|
||||
@@ -26,62 +78,186 @@ requirements are common, code needs to be maintained by large teams.
|
||||
for various reasons (security/deployment/dynamic modules/specific assets tools),
|
||||
it is not ok to use standard web tools based on `npm`.
|
||||
|
||||
Owl is not designed to be fast or small (even though it is quite good on those
|
||||
two topics). If you are interested in a comparison with React or Vue, you will
|
||||
Owl is not designed to be fast nor small (even though it is quite good on those
|
||||
two topics). It is a no nonsense framework to build applications. There is only
|
||||
one way to define components (with classes).
|
||||
|
||||
If you are interested in a comparison with React or Vue, you will
|
||||
find some more information [here](doc/comparison.md).
|
||||
|
||||
# Example
|
||||
## Documentation
|
||||
|
||||
Here is a short example to illustrate interactive widgets:
|
||||
A complete documentation for Owl can be found here:
|
||||
|
||||
```javascript
|
||||
class ClickCounter extends owl.Component {
|
||||
state = { value: 0 };
|
||||
- [Main documentation page](doc/readme.md).
|
||||
|
||||
increment() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
The most important sections are:
|
||||
|
||||
const TEMPLATES = `
|
||||
<button t-name="ClickCounter" t-on-click="increment">
|
||||
Click Me! [<t t-esc="state.value"/>]
|
||||
</button>`;
|
||||
- [Quick Start](doc/quick_start.md)
|
||||
- [Component](doc/component.md)
|
||||
- [Hooks](doc/hooks.md)
|
||||
|
||||
const qweb = new owl.QWeb(TEMPLATES);
|
||||
const counter = new ClickCounter({ qweb });
|
||||
counter.mount(document.body);
|
||||
```
|
||||
|
||||
More interesting examples can be found on the [playground](https://odoo.github.io/owl/playground) application.
|
||||
Found an issue in the documentation? A broken link? Some outdated information?
|
||||
Submit a PR!
|
||||
|
||||
## Installing/Building
|
||||
|
||||
If you want to use a simple `<script>` tag, the last release can be downloaded here:
|
||||
|
||||
- [owl-0.12.0.js](https://github.com/odoo/owl/releases/download/v0.12.0/owl.js)
|
||||
- [owl-0.12.0.min.js](https://github.com/odoo/owl/releases/download/v0.12.0/owl.min.js)
|
||||
- [owl-0.23.0.js](https://github.com/odoo/owl/releases/download/v0.23.0/owl.js)
|
||||
- [owl-0.23.0.min.js](https://github.com/odoo/owl/releases/download/v0.23.0/owl.min.js)
|
||||
|
||||
Some npm scripts are available:
|
||||
|
||||
| Command | Description |
|
||||
| ---------------------- | ------------------------------------------------------------------------------- |
|
||||
| `npm install` | install every dependency required for this project |
|
||||
| `npm run build` | build a bundle of _owl_ in the _/dist/_ folder |
|
||||
| `npm run minify` | minify the prebuilt owl.js file |
|
||||
| `npm run test` | run all tests |
|
||||
| `npm run test:watch` | run all tests, and keep a watcher |
|
||||
| `npm run extras` | build extras applications, start a static server (see [here](extras/readme.md)) |
|
||||
| `npm run extras:watch` | same as `extras`, but with a watcher to rebuild owl |
|
||||
| Command | Description |
|
||||
| ---------------- | -------------------------------------------------- |
|
||||
| `npm install` | install every dependency required for this project |
|
||||
| `npm run build` | build a bundle of _owl_ in the _/dist/_ folder |
|
||||
| `npm run minify` | minify the prebuilt owl.js file |
|
||||
| `npm run test` | run all (owl) tests |
|
||||
|
||||
## Documentation
|
||||
## Quick Overview
|
||||
|
||||
The complete documentation can be found [here](doc/readme.md). The most important sections are:
|
||||
Owl components in an application are used to define a (dynamic) tree of components.
|
||||
|
||||
- [Quick Start](doc/quick_start.md)
|
||||
- [Component](doc/component.md)
|
||||
- [QWeb](doc/qweb.md)
|
||||
```
|
||||
Root
|
||||
/ \
|
||||
A B
|
||||
/ \
|
||||
C D
|
||||
```
|
||||
|
||||
**Environment:** the root component is special: it is created with an environment,
|
||||
which should contain a `QWeb` instance. The environment is then automatically
|
||||
propagated to each sub components (and accessible in the `this.env` property).
|
||||
|
||||
```js
|
||||
const env = { qweb: new QWeb() };
|
||||
const app = new App(env);
|
||||
app.mount(document.body);
|
||||
```
|
||||
|
||||
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. Some good use case for that is some configuration keys, session
|
||||
information or 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.
|
||||
|
||||
**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++;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**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/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
|
||||
|
||||
OWL is [GPL licensed](./LICENSE).
|
||||
OWL is [LGPL licensed](./LICENSE).
|
||||
|
||||
+59
-7
@@ -1,16 +1,14 @@
|
||||
# 🦉 Animations 🦉
|
||||
|
||||
|
||||
Animation is a complex topic. There are many different use cases, and many
|
||||
solutions and technologies. Owl only supports some basic use cases.
|
||||
|
||||
## Simple CSS effects
|
||||
|
||||
Sometimes, using pure CSS is enough. For these use cases, Owl is not really
|
||||
Sometimes, using pure CSS is enough. For these use cases, Owl is not really
|
||||
necessary: it just needs to render a DOM element with a specific class. For
|
||||
example:
|
||||
|
||||
|
||||
```xml
|
||||
<a class="btn flash" t-on-click="doSomething">Click</a>
|
||||
```
|
||||
@@ -19,7 +17,7 @@ with the following CSS:
|
||||
|
||||
```css
|
||||
btn {
|
||||
background-color: gray;
|
||||
background-color: gray;
|
||||
}
|
||||
|
||||
.flash {
|
||||
@@ -35,10 +33,64 @@ btn {
|
||||
will produce a nice flash effect whenever the user click (or activate with the
|
||||
keyboard) the button.
|
||||
|
||||
## CSS Transitions (single element)
|
||||
## CSS Transitions
|
||||
|
||||
A more complex situation occurs when we want to transition an element in or out
|
||||
of the page. For example, we may want a fade-in and fade-out effect.
|
||||
of the page. For example, we may want a fade-in and fade-out effect.
|
||||
|
||||
The `t-transition` directive is here to help us (see [QWeb documentation](qweb.md#t-transition-directive)).
|
||||
The `t-transition` directive is here to help us. It works on html elements and
|
||||
on components, by adding and removing some css classes.
|
||||
|
||||
To perform useful transition effects, whenever an element appears or disappears,
|
||||
it is necessary to add/remove some css style or class at some precise moment in
|
||||
the lifetime of a node. Since this is not easy to do by hand, Owl `t-transition`
|
||||
directive is there to help.
|
||||
|
||||
Whenever a node has a `t-transition` directive, with a `name` value, the following
|
||||
will happen:
|
||||
|
||||
At node insertion:
|
||||
|
||||
- the css classes `name-enter` and `name-enter-active` will be added directly
|
||||
when the node is inserted into the DOM,
|
||||
- on the next animation frame: the css class `name-enter` will be removed and the
|
||||
class `name-enter-to` will be added (so they can be used to trigger css
|
||||
transition effects),
|
||||
- the css class `name-enter-active` will be removed whenever a css transition
|
||||
ends.
|
||||
|
||||
At node destruction:
|
||||
|
||||
- the css classes `name-leave` and `name-leave-active` will be added before the
|
||||
node is removed to the DOM,
|
||||
- the css class `name-leave` will be removed on the next animation frame (so it
|
||||
can be used to trigger css transition effects),
|
||||
- the css class `name-leave-active` will be removed whenever a css transition
|
||||
ends. Only then will the element be removed from the DOM.
|
||||
|
||||
For example, a simple fade in/out effect can be done with this:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<div t-if="state.flag" class="square" t-transition="fade">Hello</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
```css
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
```
|
||||
|
||||
The `t-transition` directive can be applied on a node element or on a component.
|
||||
|
||||
Notes:
|
||||
|
||||
- more information on animations are available [here](animations.md).
|
||||
- Owl does not support more than one transition on a single node, so the
|
||||
`t-transition` expression must be a single value (i.e. no space allowed)
|
||||
|
||||
+64
-22
@@ -7,26 +7,40 @@ In this page, we try to highlight some of these differences. Obviously, some
|
||||
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.
|
||||
|
||||
## Content
|
||||
|
||||
- [Size](#size)
|
||||
- [Class Based](#class-based)
|
||||
- [Tooling/Build Step](#toolingbuild-step)
|
||||
- [Templating](#templating)
|
||||
- [Asynchronous rendering](#asynchronous-rendering)
|
||||
- [Reactiveness](#reactiveness)
|
||||
- [State Management](#state-management)
|
||||
|
||||
### Size
|
||||
## Size
|
||||
|
||||
OWL is intended to be small and to work at a slightly lower level of abstraction
|
||||
than React and Vue. Also, jQuery is not the same kind of framework, but it is interesting to compare.
|
||||
|
||||
| Framework | Size (minified) | Size (minified, gzipped) |
|
||||
| ------------------------ | --------------- | ------------------------ |
|
||||
| OWL | 32kb | 11kb |
|
||||
| Vue + VueX | | 30kb |
|
||||
| React + ReactDOM + Redux | | 40kb |
|
||||
| jQuery | 86kb | 30kb |
|
||||
| Framework | Size (minified, gzipped) |
|
||||
| ------------------------ | ------------------------ |
|
||||
| OWL | 16kb |
|
||||
| Vue + VueX | 30kb |
|
||||
| React + ReactDOM + Redux | 40kb |
|
||||
| jQuery | 30kb |
|
||||
|
||||
### Tooling/Build step
|
||||
## Class Based
|
||||
|
||||
Both React and Vue moved away from defining components with classes. They prefer
|
||||
a more functional approach, in particular, with the new `hooks` mechanisms.
|
||||
|
||||
This has some advantages and disadvantages. But the end result is that React
|
||||
and Vue both offers multiple different ways of defining new components. In
|
||||
contrast, Owl has only one mechanism: class-based components. We believe that Owl
|
||||
components are fast enough for all our usecases, and making it as simple as
|
||||
possible for developers is more valuable (for us).
|
||||
|
||||
## Tooling/Build step
|
||||
|
||||
OWL is designed to be easy to use in a standalone way. For various reasons,
|
||||
Odoo does not want to rely on standard web tools (such as webpack), and OWL can
|
||||
@@ -43,7 +57,7 @@ components, which also necessitate a build step.
|
||||
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.
|
||||
|
||||
### Templating
|
||||
## Templating
|
||||
|
||||
OWL uses its own QWeb engine, which compiles templates on the
|
||||
frontend, as they are needed. This is extremely convenient for our use case, in
|
||||
@@ -84,22 +98,22 @@ 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:
|
||||
there is a syntax highlighter for jsx here on github!
|
||||
|
||||
### Asynchronous rendering
|
||||
## Asynchronous Rendering
|
||||
|
||||
This is actually a big difference between OWL and React/Vue: components in OWL
|
||||
are totally asynchronous. They have two asynchronous hooks in their lifecycle:
|
||||
|
||||
- `willStart` (before the widget starts rendering)
|
||||
- `willStart` (before the component starts rendering)
|
||||
- `willUpdateProps` (before new props are set)
|
||||
|
||||
Both these methods can be implemented and return a promise. The rendering will
|
||||
then wait for these promises to be completed before patching the DOM. This is
|
||||
useful for some use cases: for example, a widget may want to fetch an external
|
||||
library (a calendar widget may need a specialized calendar rendering library),
|
||||
useful for some use cases: for example, a component may want to fetch an external
|
||||
library (a calendar component may need a specialized calendar rendering library),
|
||||
in its willStart hook.
|
||||
|
||||
```javascript
|
||||
class MyCalendarWidget extends owl.Component {
|
||||
class MyCalendarComponent extends owl.Component {
|
||||
...
|
||||
|
||||
willStart() {
|
||||
@@ -115,7 +129,7 @@ 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
|
||||
more convoluted.
|
||||
|
||||
### Reactiveness
|
||||
## Reactiveness
|
||||
|
||||
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.
|
||||
@@ -126,10 +140,12 @@ by getters/setters. With that, it can notify components whenever the state that
|
||||
they read was changed.
|
||||
|
||||
Owl is closer to vue: it also tracks magically the state properties, but it does
|
||||
only increment a counter whenever it changes (and a _deep_ counter for each of
|
||||
its parents). This assumes that the state is actually a tree.
|
||||
only increment an internal counter whenever it changes. Note that it is done
|
||||
with a `Proxy`, which means that it is totally transparent to the developers.
|
||||
Adding new keys is supported. Once any part of the state has been changed, a
|
||||
rendering is scheduled in the next microtask tick (promise queue).
|
||||
|
||||
### State Management
|
||||
## State Management
|
||||
|
||||
Managing the state of an application is a tricky issue. Many solutions have
|
||||
been proposed these last few years. It also depends on the kind of application we
|
||||
@@ -188,7 +204,33 @@ keeps track of who get data, and retrigger a render when it was changed.
|
||||
|
||||
**Owl**
|
||||
|
||||
Owl store is a little bit like a mix of redux and vuex: it has mutations and
|
||||
actions, like VueX, it keeps track of the state changes, but it does not notify
|
||||
a component when the state changes. Instead, components need to connect to the
|
||||
store like in redux, with a function that will listen to the relevant state.
|
||||
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
|
||||
not notify a component when the state changes. Instead, components need to connect
|
||||
to the store like in redux, by inheriting the `ConnectedComponent` class.
|
||||
|
||||
```javascript
|
||||
const actions = {
|
||||
increment({ state }, val) {
|
||||
state.counter += val;
|
||||
}
|
||||
};
|
||||
|
||||
const state = {
|
||||
counter: 0
|
||||
};
|
||||
const store = new owl.Store({ state, actions });
|
||||
|
||||
class Counter extends owl.ConnectedComponent {
|
||||
static mapStoreToProps(state) {
|
||||
return {
|
||||
value: state.counter
|
||||
};
|
||||
}
|
||||
increment() {
|
||||
this.env.store.dispatch("increment");
|
||||
}
|
||||
}
|
||||
|
||||
const counter = new Counter({ store, qweb });
|
||||
```
|
||||
|
||||
+809
-141
File diff suppressed because it is too large
Load Diff
+108
@@ -0,0 +1,108 @@
|
||||
# 🦉 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 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.
|
||||
|
||||
## 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 });
|
||||
const env = {
|
||||
qweb: new QWeb(TEMPLATES),
|
||||
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">
|
||||
some more sopthisticated user interface
|
||||
</t>
|
||||
`;
|
||||
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,27 @@
|
||||
# 🦉 Event Bus 🦉
|
||||
|
||||
It is sometimes useful to use a `Bus` to communicate informations between various
|
||||
parts of the code. Owl has a very simple bus class, which manages subscriptions,
|
||||
triggering events, and callbacks.
|
||||
|
||||
```js
|
||||
const bus = new owl.core.EventBus();
|
||||
|
||||
bus.on("some-event", null, function(...args) {
|
||||
console.log(...args);
|
||||
});
|
||||
|
||||
bus.trigger("some-event", 1, 2, 3);
|
||||
// [1,2,3] will be logged to the console
|
||||
```
|
||||
|
||||
Its API is:
|
||||
|
||||
| Method | Description |
|
||||
| -------------------------------- | --------------------------------- |
|
||||
| `on(eventType, owner, callback)` | add a listener |
|
||||
| `off(eventType, owner)` | remove all listeners for an owner |
|
||||
| `trigger(eventType, ...args)` | trigger an event |
|
||||
| `clear` | remove all subscriptions |
|
||||
|
||||
Note that the [`Store`](store.md) is an example of an `EventBus`.
|
||||
+422
@@ -0,0 +1,422 @@
|
||||
# 🦉 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 works beautifully with Owl components: they solve the problems mentioned
|
||||
above, and in particular, they are the perfect way to make your component
|
||||
reactive.
|
||||
|
||||
## Example: 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 have 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 });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Hooks can get a reference to the currently being defined component with the
|
||||
`Component.current` static property. This is why they need to be called in the
|
||||
constructor.
|
||||
|
||||
### `useState`
|
||||
|
||||
The `useState` hook is certainly the most important hooks for Owl components:
|
||||
this is what enables component to be reactive, to react to state change.
|
||||
|
||||
The `useState` hook has to be given an object or an array, and will return
|
||||
an observed version of it (using a `Proxy`).
|
||||
|
||||
```javascript
|
||||
const { useState } = owl.hooks;
|
||||
|
||||
class Counter extends owl.Component {
|
||||
static template = xml`
|
||||
<button t-on-click="increment">
|
||||
Click Me! [<t t-esc="state.value"/>]
|
||||
</button>`;
|
||||
|
||||
state = useState({ value: 0 });
|
||||
|
||||
increment() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `onMounted`
|
||||
|
||||
`onMounted` is not an 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 an 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 an 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 an 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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
As shown by the example above, html elements are accessed by using the `el`
|
||||
key, and components references are accessed with `comp`.
|
||||
|
||||
Note: if used on a component, the reference will be set in the `refs`
|
||||
variable between `willPatch` and `patched`.
|
||||
|
||||
The `t-ref` directive also accepts dynamic values with string interpolation
|
||||
(like the [`t-attf-`](qweb.md#dynamic-attributes) and
|
||||
`t-component` directives). For example,
|
||||
|
||||
```xml
|
||||
<div t-ref="component_{{someCondition ? '1' : '2'}}"/>
|
||||
```
|
||||
|
||||
Here, the references needs 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 component, but not to the whole application.
|
||||
This is where the `useSubEnv` hook may be useful: it let 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 him (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 inject 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.
|
||||
+3
-37
@@ -11,44 +11,10 @@ For example, this code will display `update` in the console:
|
||||
```javascript
|
||||
const observer = new owl.Observer();
|
||||
observer.notifyCB = () => console.log("update");
|
||||
observer.observe(obj);
|
||||
const obj = observer.observe({ a: { b: 1 } });
|
||||
|
||||
const obj = { a: { b: 1 } };
|
||||
obj.a.b = 2;
|
||||
```
|
||||
|
||||
## Technical Limitations
|
||||
|
||||
Since the observer uses getters and setters, it is actually unable to react to
|
||||
changes in two situations:
|
||||
|
||||
- adding a key to an object:
|
||||
|
||||
```javascript
|
||||
const observer = new owl.Observer();
|
||||
const obj = { a: 1 };
|
||||
observer.observe(obj);
|
||||
obj.b = 2; // will do nothing
|
||||
```
|
||||
|
||||
In that case, we need a way to tell the observer that something happened.
|
||||
This can be done by using the `set` method:
|
||||
|
||||
```javascript
|
||||
observer.set(obj, "b", 2);
|
||||
```
|
||||
|
||||
- modifying an array by setting a new value at a given index:
|
||||
|
||||
```javascript
|
||||
const observer = new owl.Observer();
|
||||
const obj = { todos: [{ id: 1, text: "todo" }] };
|
||||
observer.observe(obj);
|
||||
obj[0] = { id: 2, text: "othertodo" }; // will do nothing, and obj[0] is not observed
|
||||
```
|
||||
|
||||
In that case, the solution is the same, we can simply use the `set` method:
|
||||
|
||||
```javascript
|
||||
observer.set(obj, 0, { id: 2, text: "othertodo" });
|
||||
```
|
||||
The observer is implemented with the native `Proxy` object. Note that this
|
||||
means that it will not work on older browsers.
|
||||
|
||||
+9
-7
@@ -1,6 +1,6 @@
|
||||
# 🦉 Quick Start 🦉
|
||||
|
||||
## Static server
|
||||
## Static Server
|
||||
|
||||
Let us assume that we have a static server running somewhere. We could then
|
||||
simply add an html page with a few extra files.
|
||||
@@ -58,25 +58,27 @@ To build an application (or a sub-part of an application), we need two things:
|
||||
need. In practice, it could context some user session information, some
|
||||
configuration keys (for example, isMobile = true/false if we are in mobile mode).
|
||||
|
||||
- a description of the user interface: there should be a root widget, which can
|
||||
have sub widgets
|
||||
- a description of the user interface: there should be a root component, which can
|
||||
have sub components
|
||||
|
||||
Here are a few steps that we may take to get started:
|
||||
|
||||
- get the templates
|
||||
- create a qweb engine, with the templates
|
||||
- create an environment
|
||||
- create an instance of the root widget
|
||||
- mount the root widget to a DOM element
|
||||
- create an instance of the root component
|
||||
- mount the root component to a DOM element
|
||||
|
||||
Let us now add the javascript to make it work, in `app.js`:
|
||||
|
||||
```javascript
|
||||
const useState = owl.hooks.useState;
|
||||
|
||||
class ClickCounter extends owl.Component {
|
||||
static template = "clickcounter";
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.template = "clickcounter";
|
||||
this.state = { value: 0 };
|
||||
this.state = useState({ value: 0 });
|
||||
}
|
||||
|
||||
increment() {
|
||||
|
||||
+258
-403
@@ -3,44 +3,77 @@
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Directives](#directives)
|
||||
- [QWeb Engine](#qweb-engine)
|
||||
- [QWeb Specification](#qweb-specification)
|
||||
|
||||
- [Static html nodes](#static-html-nodes)
|
||||
- [`t-esc` directive](#t-esc-directive)
|
||||
- [`t-raw` directive](#t-raw-directive)
|
||||
- [`t-set` directive](#t-set-directive)
|
||||
- [`t-if` directive](#t-if-directive)
|
||||
- [Expression evaluation](#expression-evaluation)
|
||||
- [Dynamic attributes (`t-att` and `t-attf` directives)](#dynamic-attributes-t-att-and-t-attf-directives)
|
||||
- [`t-call` directive (sub templates)](#t-call-directive-sub-templates)
|
||||
|
||||
- [JS/OWL Specific Extensions](#jsowl-specific-extensions)
|
||||
|
||||
- [`t-on` directive](#t-on-directive)
|
||||
- [Component: `t-widget`, `t-props`](#component-t-widget-t-props)
|
||||
- [`t-ref` directive](#t-ref-directive)
|
||||
- [`t-key` directive](#t-key-directive)
|
||||
- [`t-transition` directive](#t-transition-directive)
|
||||
- [`t-mounted` directive](#t-mounted-directive)
|
||||
- [Debugging (`t-debug` and `t-log`)](#debugging-t-debug-and-t-log)
|
||||
- [White spaces](#white-spaces)
|
||||
- [Root nodes](#root-nodes)
|
||||
- [Reference](#reference)
|
||||
- [White Spaces](#white-spaces)
|
||||
- [Root Nodes](#root-nodes)
|
||||
- [Expression Evaluation](#expression-evaluation)
|
||||
- [Static html Nodes](#static-html-nodes)
|
||||
- [Outputting Data](#outputting-data)
|
||||
- [Setting Variables](#setting-variables)
|
||||
- [Conditionals](#conditionals)
|
||||
- [Dynamic Attributes](#dynamic-attributes)
|
||||
- [Loops](#loops)
|
||||
- [Rendering Sub Templates](#rendering-sub-templates)
|
||||
- [Debugging](#debugging)
|
||||
|
||||
## 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
|
||||
mostly to generate html. In OWL, QWeb templates are compiled into functions that
|
||||
generate a virtual dom representation of the html.
|
||||
mostly to generate HTML. In OWL, QWeb templates are compiled into functions that
|
||||
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.
|
||||
|
||||
The QWeb implementation in the OWL project is slightly different. It compiles
|
||||
templates into functions that output a virtual DOM instead of a string. This is
|
||||
necessary for the component system. In addition, it has a few extra directives
|
||||
(see [OWL Specific Extensions](#owlspecificextensions))
|
||||
```xml
|
||||
<div>
|
||||
<span t-if="somecondition">Some string</span>
|
||||
<ul t-else="1">
|
||||
<li t-foreach="messages" t-as="message">
|
||||
<t t-esc="message">
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
```
|
||||
|
||||
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`, ...
|
||||
|
||||
## Directives
|
||||
|
||||
We present here a list of all standard QWeb directives:
|
||||
|
||||
| Name | Description |
|
||||
| ------------------------------ | ------------------------------------------------------------ |
|
||||
| `t-esc` | [Outputting safely a value](#outputting-data) |
|
||||
| `t-raw` | [Outputting value, without escaping](#outputting-data) |
|
||||
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
|
||||
| `t-if`, `t-elif`, `t-else`, | [conditionally rendering](#conditionals) |
|
||||
| `t-foreach`, `t-as` | [Loops](#loops) |
|
||||
| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
|
||||
| `t-call` | [Rendering sub templates](#rendering-sub-templates) |
|
||||
| `t-debug`, `t-log` | [Debugging](#debugging) |
|
||||
| `t-name` | [Defining a template (not really a directive)](#qweb-engine) |
|
||||
|
||||
The component system in Owl requires additional directives, to express various
|
||||
needs. Here is a list of all Owl specific directives:
|
||||
|
||||
| Name | Description |
|
||||
| ------------------------------------------------------ | ----------------------------------------------------------------------------------- |
|
||||
| `t-component`, `t-props`, `t-keepalive`, `t-asyncroot` | [Defining a sub component](component.md#composition) |
|
||||
| `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-on-*` | [Event handling](component.md#event-handling) |
|
||||
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
|
||||
| `t-slot` | [Rendering a slot](component.md#slots) |
|
||||
| `t-model` | [Form input bindings](component.md#form-input-bindings) |
|
||||
|
||||
## QWeb Engine
|
||||
|
||||
@@ -61,12 +94,14 @@ It's API is quite simple:
|
||||
const qweb = new owl.QWeb(TEMPLATES);
|
||||
```
|
||||
|
||||
- **`addTemplate(name, xmlStr)`**: add a specific template.
|
||||
- **`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).
|
||||
|
||||
@@ -74,7 +109,7 @@ It's API is quite simple:
|
||||
const TEMPLATES = `
|
||||
<templates>
|
||||
<div t-name="App" class="main">main</div>
|
||||
<div t-name="OtherWidget">other widget</div>
|
||||
<div t-name="OtherComponent">other component</div>
|
||||
</templates>`;
|
||||
qweb.addTemplates(TEMPLATES);
|
||||
```
|
||||
@@ -83,30 +118,137 @@ It's API is quite simple:
|
||||
which is a virtual representation of the DOM (see [vdom doc](vdom.md)).
|
||||
|
||||
```js
|
||||
const vnode = qweb.render("App", widget);
|
||||
const vnode = qweb.render("App", component);
|
||||
```
|
||||
|
||||
- **`register(name, Component)`**: static function to register an OWL 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-widget` directive). This is useful for commonly used
|
||||
templates (see the `t-component` directive). This is useful for commonly used
|
||||
components accross the application.
|
||||
|
||||
```js
|
||||
class Dialog extends owl.Component { ... }
|
||||
QWeb.register("Dialog", Dialog);
|
||||
QWeb.registerComponent("Dialog", Dialog);
|
||||
|
||||
...
|
||||
|
||||
class ParentWidget extends owl.Component { ... }
|
||||
qweb.addTemplate("ParentWidget", "<div><t t-widget='Dialog'/></div>");
|
||||
class ParentComponent extends owl.Component { ... }
|
||||
qweb.addTemplate("ParentComponent", "<div><Dialog/></div>");
|
||||
```
|
||||
|
||||
## QWeb Specification
|
||||
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
|
||||
|
||||
We define in this section the specification of how `QWeb` templates should be
|
||||
rendered.
|
||||
rendered. Note that we only document here the standard QWeb specification. Owl
|
||||
specific extensions are documented in various other parts of the documentation.
|
||||
|
||||
### Static html nodes
|
||||
### White Spaces
|
||||
|
||||
White spaces in a templates are handled in a special way:
|
||||
|
||||
- consecutive whitespaces are always condensed to a single whitespace
|
||||
- if a whitespace-only text node contains a linebreak, it is ignored
|
||||
- the previous rules do not apply if we are in a `<pre>` tag
|
||||
|
||||
### Root Nodes
|
||||
|
||||
For many reasons, Owl QWeb templates should have a single root node. More
|
||||
precisely, the result of a template rendering should have a single root node:
|
||||
|
||||
```xml
|
||||
<!–– not ok: two root nodes ––>
|
||||
<t>
|
||||
<div>foo</div>
|
||||
<div>bar</div>
|
||||
</t>
|
||||
|
||||
<!–– ok: result has one single root node ––>
|
||||
<t>
|
||||
<div t-if="someCondition">foo</div>
|
||||
<span t-else="1">bar</span>
|
||||
</t>
|
||||
```
|
||||
|
||||
Extra root nodes will actually be ignored (even though they will be rendered
|
||||
in memory).
|
||||
|
||||
Note: this does not apply to subtemplates (see the `t-call` directive). In that
|
||||
case, they will be inlined in the main template, and can actually have many
|
||||
root nodes.
|
||||
|
||||
### Expression Evaluation
|
||||
|
||||
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
|
||||
component). For example, `a + b.c(d)` will be converted into:
|
||||
|
||||
```js
|
||||
context["a"] + context["b"].c(context["d"]);
|
||||
```
|
||||
|
||||
It is useful to explain the various rules that applies on these expressions:
|
||||
|
||||
1. it should be a simple expression which returns a value. It cannot be a statement.
|
||||
|
||||
```xml
|
||||
<div><p t-if="1 + 2 === 3">ok</p></div>
|
||||
```
|
||||
|
||||
is valid, but the following is not valid:
|
||||
|
||||
```xml
|
||||
<div><p t-if="console.log(1)">NOT valid</p></div>
|
||||
```
|
||||
|
||||
2. it can use anything in the rendering context (typically, the component):
|
||||
|
||||
```xml
|
||||
<p t-if="user.birthday === today()">Happy bithday!</p>
|
||||
```
|
||||
|
||||
is valid, and will read the `user` object from the context, and call the
|
||||
`today` function.
|
||||
|
||||
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.
|
||||
|
||||
| Word | will be replaced by |
|
||||
| ----- | ------------------- |
|
||||
| `and` | `&&` |
|
||||
| `or` | `\|\|` |
|
||||
| `gt` | `>` |
|
||||
| `gte` | `>=` |
|
||||
| `lt` | `<` |
|
||||
| `lte` | `<=` |
|
||||
|
||||
So, one can write this:
|
||||
|
||||
```xml
|
||||
<div><p t-if="10 + 2 gt 5">ok</p></div>
|
||||
```
|
||||
|
||||
### Static Html Nodes
|
||||
|
||||
Normal, regular html nodes are rendered into themselves:
|
||||
|
||||
@@ -114,7 +256,7 @@ Normal, regular html nodes are rendered into themselves:
|
||||
<div>hello</div> <!–– rendered as itself ––>
|
||||
```
|
||||
|
||||
### `t-esc` directive
|
||||
### Outputting Data
|
||||
|
||||
The `t-esc` directive is necessary whenever you want to add a dynamic text
|
||||
expression in a template. The text is escaped to avoid security issues.
|
||||
@@ -129,14 +271,12 @@ rendered with the value `value` set to `42` in the rendering context yields:
|
||||
<p>42</p>
|
||||
```
|
||||
|
||||
### `t-raw` directive
|
||||
|
||||
The `t-raw` directive is almost the same as `t-esc`, but without the escaping.
|
||||
This is mostly useful to inject a raw html string somewhere. Obviously, this
|
||||
is unsafe to do in general, and should only be used for strings known to be safe.
|
||||
|
||||
```xml
|
||||
<p><t t-esc="value"/></p>
|
||||
<p><t t-raw="value"/></p>
|
||||
```
|
||||
|
||||
rendered with the value `value` set to `<span>foo</span>` in the rendering context yields:
|
||||
@@ -145,7 +285,12 @@ rendered with the value `value` set to `<span>foo</span>` in the rendering conte
|
||||
<p><span>foo</span></p>
|
||||
```
|
||||
|
||||
### `t-set` directive
|
||||
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
|
||||
|
||||
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, ...
|
||||
|
||||
@@ -177,7 +322,7 @@ This is done via the `t-set` directive, which takes the name of the variable to
|
||||
The `t-set` directive acts like a regular variable in most programming language.
|
||||
It is lexically scoped (inner nodes are sub scopes), can be shadowed, ...
|
||||
|
||||
### `t-if` directive
|
||||
### Conditionals
|
||||
|
||||
The `t-if` directive is useful to conditionally render something. It evaluates
|
||||
the expression given as attribute value, and then acts accordingly.
|
||||
@@ -227,52 +372,7 @@ Extra conditional branching directives `t-elif` and `t-else` are also available:
|
||||
</div>
|
||||
```
|
||||
|
||||
### Expression evaluation
|
||||
|
||||
It is useful to explain the various rules that applies on QWeb expressions. These
|
||||
expressions are strings that will be converted to a javascript expression at
|
||||
compile time.
|
||||
|
||||
1. it should be a simple expression which returns a value. It cannot be a statement.
|
||||
|
||||
```xml
|
||||
<div><p t-if="1 + 2 === 3">ok</p></div>
|
||||
```
|
||||
|
||||
is valid, but the following is not valid:
|
||||
|
||||
```xml
|
||||
<div><p t-if="console.log(1)">NOT valid</p></div>
|
||||
```
|
||||
|
||||
2. it can use anything in the rendering context:
|
||||
|
||||
```xml
|
||||
<p t-if="user.birthday == today()">Happy bithday!</p>
|
||||
```
|
||||
|
||||
is valid, and will read the `user` object from the context, and call the
|
||||
`today` function.
|
||||
|
||||
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.
|
||||
|
||||
| Word | will be replaced by |
|
||||
| ----- | ------------------- |
|
||||
| `and` | `&&` |
|
||||
| `or` | `\|\|` |
|
||||
| `gt` | `>` |
|
||||
| `gte` | `>=` |
|
||||
| `lt` | `<` |
|
||||
| `lte` | `<=` |
|
||||
|
||||
So, one can write this:
|
||||
|
||||
```xml
|
||||
<div><p t-if="10 + 2 gt 5">ok</p></div>
|
||||
```
|
||||
|
||||
### Dynamic attributes (`t-att` and `t-attf` directives)
|
||||
### Dynamic Attributes
|
||||
|
||||
One can use the `t-att-` directive to add dynamic attributes. Its main use is to
|
||||
evaluate an expression (at rendering time) and bind an attribute to its result:
|
||||
@@ -292,19 +392,77 @@ If an expression evaluates to a falsy value, it will not be set at all:
|
||||
There is another way to format a string attribute: the `t-attf-` directive. With
|
||||
it, you get string interpolation:
|
||||
|
||||
```xml
|
||||
<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> -->
|
||||
```
|
||||
|
||||
For historical reason, there is an alternate form of string interpolation:
|
||||
|
||||
```xml
|
||||
<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> -->
|
||||
```
|
||||
|
||||
### `t-call` directive (sub templates)
|
||||
### Loops
|
||||
|
||||
QWeb has an iteration directive `t-foreach` which take an expression returning the
|
||||
collection to iterate on, and a second parameter `t-as` providing the name to use
|
||||
for the current item of the iteration:
|
||||
|
||||
```xml
|
||||
<t t-foreach="[1, 2, 3]" t-as="i">
|
||||
<p><t t-esc="i"/></p>
|
||||
</t>
|
||||
```
|
||||
|
||||
will be rendered as:
|
||||
|
||||
```xml
|
||||
<p>1</p>
|
||||
<p>2</p>
|
||||
<p>3</p>
|
||||
```
|
||||
|
||||
Like conditions, `t-foreach` applies to the element bearing the directive’s attribute, and
|
||||
|
||||
```xml
|
||||
<p t-foreach="[1, 2, 3]" t-as="i">
|
||||
<t t-esc="i"/>
|
||||
</p>
|
||||
```
|
||||
|
||||
is equivalent to the previous example.
|
||||
|
||||
`t-foreach` can iterate on an array (the current item will be the current value)
|
||||
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
|
||||
variables for various data points (note: `$as` will be replaced by the name
|
||||
passed to `t-as`):
|
||||
|
||||
- `$as_value`: the current iteration value, identical to `$as` for lists and
|
||||
integers, but for objects, it provides the value (where `$as` provides the key)
|
||||
- `$as_index`: the current iteration index (the first item of the iteration has index 0)
|
||||
- `$as_first`: whether the current item is the first of the iteration
|
||||
(equivalent to `$as_index == 0`)
|
||||
- `$as_last`: whether the current item is the last of the iteration
|
||||
(equivalent to `$as_index + 1 == $as_size`), requires the iteratee’s size be
|
||||
available
|
||||
|
||||
These extra variables provided and all new variables created into the `t-foreach`
|
||||
are only available in the scope of the `t-foreach`. If the variable exists outside
|
||||
the context of the `t-foreach`, the value is copied at the end of the foreach
|
||||
into the global context.
|
||||
|
||||
```xml
|
||||
<t t-set="existing_variable" t-value="False"/>
|
||||
<!-- existing_variable now False -->
|
||||
|
||||
<p t-foreach="Array(3)" t-as="i">
|
||||
<t t-set="existing_variable" t-value="True"/>
|
||||
<t t-set="new_variable" t-value="True"/>
|
||||
<!-- existing_variable and new_variable now True -->
|
||||
</p>
|
||||
|
||||
<!-- existing_variable always True -->
|
||||
<!-- new_variable undefined -->
|
||||
```
|
||||
|
||||
### Rendering Sub Templates
|
||||
|
||||
QWeb templates can be used for top level rendering, but they can also be used
|
||||
from within another template (to avoid duplication or give names to parts of
|
||||
@@ -352,276 +510,7 @@ will result in :
|
||||
</div>
|
||||
```
|
||||
|
||||
## JS/OWL Specific Extensions
|
||||
|
||||
### `t-on` directive
|
||||
|
||||
In a component's template, it is useful to be able to register handlers on some
|
||||
elements to some specific events. This
|
||||
is what makes a template _alive_. There are two different use cases.
|
||||
|
||||
1. Register an event handler on a DOM node
|
||||
|
||||
```xml
|
||||
<button t-on-click="someMethod">Do something</button>
|
||||
```
|
||||
|
||||
This will be roughly translated in javascript like this:
|
||||
|
||||
```js
|
||||
button.addEventListener("click", widget.someMethod.bind(widget));
|
||||
```
|
||||
|
||||
The suffix (`click` in this example) is simply the name of the actual DOM
|
||||
event.
|
||||
|
||||
In order to remove the DOM event details from the event handlers (like calls
|
||||
to `event.preventDefault`) and let them focus on data logic, _modifiers_ can
|
||||
be specified as additional suffixes of the `t-on` directive.
|
||||
|
||||
| Modifier | Description |
|
||||
| ---------- | ----------------------------------------------------------------- |
|
||||
| `.stop` | calls `event.stopPropagation()` before calling the method |
|
||||
| `.prevent` | calls `event.preventDefault()` before calling the method |
|
||||
| `.self` | calls the method only if the `event.target` is the element itself |
|
||||
|
||||
```xml
|
||||
<button t-on-click.stop="someMethod">Do something</button>
|
||||
```
|
||||
|
||||
Note that modifiers can be combined (ex: `t-on-click.stop.prevent`), and that
|
||||
the order may matter. For instance `t-on-click.prevent.self` will prevent all
|
||||
clicks while `t-on-click.self.prevent` will only prevent clicks on the
|
||||
element itself.
|
||||
|
||||
2. Register an event handler on a component. This will not capture a DOM event,
|
||||
but rather a _business_ event:
|
||||
|
||||
```xml
|
||||
<t t-widget="MyWidget" t-on-menuLoaded="someMethod"/>
|
||||
```
|
||||
|
||||
```js
|
||||
class MyWidget {
|
||||
someWhere() {
|
||||
const payload = ...;
|
||||
this.trigger('menuLoaded', payload);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Here, the parent widget will receive the payload in its `someMethod` handler,
|
||||
whenever the event is triggered.
|
||||
|
||||
The `t-on` directive also allows to prebind some arguments. For example,
|
||||
|
||||
```xml
|
||||
<button t-on-click="someMethod(expr)">Do something</button>
|
||||
```
|
||||
|
||||
Here, `expr` is a valid Owl expression, so it could be `true` or some variable
|
||||
from the rendering context.
|
||||
|
||||
### Component: `t-widget`, `t-props`
|
||||
|
||||
The `t-widget` and the `t-props` directives are the key to a declarative component
|
||||
system. They allow a template to define where and how a sub widget is created
|
||||
and/or updated. For example:
|
||||
|
||||
```xml
|
||||
<div t-name="ParentWidget">
|
||||
<t t-widget="ChildWidget" t-props="{count: state.val}"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
class ParentWidget {
|
||||
widgets = { ChildWidget };
|
||||
state = { val: 4 };
|
||||
}
|
||||
```
|
||||
|
||||
Whenever the template is rendered, it will automatically create the subwidget
|
||||
`ChildWidget` at the correct place. It needs to find the reference to the
|
||||
actual component class in the special `widgets` key, or the class registered in
|
||||
QWeb's global registry (see `register` function of QWeb). It first looks inside
|
||||
the local `widgets` key, then fallbacks on the global registry.
|
||||
|
||||
In this example, the child widget will receive the object `{count: 4}` in its
|
||||
constructor. This will be assigned to the `props` variable, which can be accessed
|
||||
on the widget (and also, in the template). Whenever the state is updated, then
|
||||
the subwidget will also be updated automatically.
|
||||
|
||||
The `t-widget` directive also accepts dynamic values with string interpolation
|
||||
(like the [`t-attf-`](#dynamic-attributes-t-att-and-t-attf-directives) directive):
|
||||
|
||||
```xml
|
||||
<div t-name="ParentWidget">
|
||||
<t t-widget="ChildWidget#{id}"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
class ParentWidget {
|
||||
widgets = { ChildWidget1, ChildWidget2 };
|
||||
state = { id: 1 };
|
||||
}
|
||||
```
|
||||
|
||||
Similarly to `t-attf-`, there is an alternate form of string interpolation:
|
||||
|
||||
```xml
|
||||
<div t-name="ParentWidget">
|
||||
<t t-widget="ChildWidget{{id}}"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
### `t-ref` directive
|
||||
|
||||
The `t-ref` directive helps a component keep reference to some inside part of it.
|
||||
Like the `t-on` directive, it can work either on a DOM node, or on a component:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<div t-ref="someDiv"/>
|
||||
<t t-widget="SubWidget" t-ref="someWidget"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
In this example, the widget will be able to access the `div` and the component
|
||||
inside the special `refs` variable:
|
||||
|
||||
```js
|
||||
this.refs.someDiv;
|
||||
this.refs.someWidget;
|
||||
```
|
||||
|
||||
This is useful for various usecases: for example, integrating with an external
|
||||
library that needs to render itself inside an actual DOM node. Or for calling
|
||||
some method on a sub widget.
|
||||
|
||||
Note: if used on a component, the reference will be set in the `refs`
|
||||
variable between `willPatch` and `patched`.
|
||||
|
||||
The `t-ref` directive also accepts dynamic values with string interpolation
|
||||
(like the [`t-attf-`](#dynamic-attributes-t-att-and-t-attf-directives) and
|
||||
[`t-widget-`](#component-t-widget-t-props) directives). For example, if we have
|
||||
`id` set to 44 in the rendering context,
|
||||
|
||||
```xml
|
||||
<div t-ref="widget_#{id}"/>
|
||||
```
|
||||
|
||||
```js
|
||||
this.refs.widget_44;
|
||||
```
|
||||
|
||||
Similarly to `t-attf-` and `t-widget`, there is an alternate form of string
|
||||
interpolation:
|
||||
|
||||
```xml
|
||||
<div t-ref="widget_{{id}}"/>
|
||||
```
|
||||
|
||||
### `t-key` directive
|
||||
|
||||
Even though Owl tries to be as declarative as possible, some DOM state is still
|
||||
locked inside the DOM: for example, the scrolling state, the current user selection,
|
||||
the focused element or the state of an input. This is why we use a virtual dom
|
||||
algorithm to keep the actual DOM node as much as possible. However, this is
|
||||
sometimes not enough, and we need to help Owl decide if an element is actually
|
||||
the same, or is different. The `t-key` directive is used to give an identity to an element.
|
||||
|
||||
There are three main use cases:
|
||||
|
||||
- _elements in a list_:
|
||||
|
||||
```xml
|
||||
<span t-foreach="todos" t-as="todo" t-key="todo.id">
|
||||
<t t-esc="todo.text"/>
|
||||
</span>
|
||||
```
|
||||
|
||||
- _`t-if`/`t-else`_
|
||||
|
||||
- _animations_: give a different identity to a component. Ex: thread id with
|
||||
animations on add/remove message.
|
||||
|
||||
### `t-transition` directive
|
||||
|
||||
To perform useful transition effects, whenever an element appears or disappears,
|
||||
it is necessary to add/remove some css style or class at some precise moment in
|
||||
the lifetime of a node. Since this is not easy to do by hand, Owl `t-transition`
|
||||
directive is there to help.
|
||||
|
||||
Whenever a node has a `t-transition` directive, with a `name` value, the following
|
||||
will happen:
|
||||
|
||||
At node insertion:
|
||||
|
||||
- the css classes `name-enter` and `name-enter-active` will be added directly
|
||||
when the node is inserted into the DOM,
|
||||
- on the next animation frame: the css class `name-enter` will be removed and the
|
||||
class `name-enter-to` will be added (so they can be used to trigger css
|
||||
transition effects),
|
||||
- the css class `name-enter-active` will be removed whenever a css transition
|
||||
ends.
|
||||
|
||||
At node destruction:
|
||||
|
||||
- the css classes `name-leave` and `name-leave-active` will be added before the
|
||||
node is removed to the DOM,
|
||||
- the css class `name-leave` will be removed on the next animation frame (so it
|
||||
can be used to trigger css transition effects),
|
||||
- the css class `name-leave-active` will be removed whenever a css transition
|
||||
ends. Only then will the element be removed from the DOM.
|
||||
|
||||
For example, a simple fade in/out effect can be done with this:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<div t-if="state.flag" class="square" t-transition="fade">Hello</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
```css
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
```
|
||||
|
||||
The `t-transition` directive can be combined with `t-widget`.
|
||||
|
||||
Notes:
|
||||
|
||||
- more information on animations are available [here](animations.md).
|
||||
- the value for `t-transition` must be a single class name. Owl does not support
|
||||
more than one transition on a single node.
|
||||
|
||||
### `t-mounted` directive
|
||||
|
||||
The `t-mounted` directive allows to register a callback to execute whenever the node
|
||||
is inserted into the DOM.
|
||||
|
||||
```xml
|
||||
<div><input t-ref="someInput" t-mounted="focusMe"/></div>
|
||||
```
|
||||
|
||||
```js
|
||||
class MyWidget extends owl.Component {
|
||||
...
|
||||
focusMe() {
|
||||
this.refs.someInput.focus();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Debugging (`t-debug` and `t-log`)
|
||||
### Debugging
|
||||
|
||||
The javascript QWeb implementation provides two useful debugging directives:
|
||||
|
||||
@@ -643,37 +532,3 @@ will stop execution if the browser dev tools are open.
|
||||
```
|
||||
|
||||
will print 42 to the console
|
||||
|
||||
### White spaces
|
||||
|
||||
White spaces in a templates are handled in a special way:
|
||||
|
||||
- consecutive whitespaces are always condensed to a single whitespace
|
||||
- if a whitespace-only text node contains a linebreak, it is ignored
|
||||
- the previous rules do not apply if we are in a `<pre>` tag
|
||||
|
||||
### Root nodes
|
||||
|
||||
For many reasons, Owl QWeb templates should have a single root node. More
|
||||
precisely, the result of a template rendering should have a single root node:
|
||||
|
||||
```xml
|
||||
<!–– not ok: two root nodes ––>
|
||||
<t>
|
||||
<div>foo</div>
|
||||
<div>bar</div>
|
||||
</t>
|
||||
|
||||
<!–– ok: result has one single root node ––>
|
||||
<t>
|
||||
<div t-if="someCondition">foo</div>
|
||||
<span t-else="1">bar</span>
|
||||
</t>
|
||||
```
|
||||
|
||||
Extra root nodes will actually be ignored (even though they will be rendered
|
||||
in memory).
|
||||
|
||||
Note: this does not apply to subtemplates (see the `t-call` directive). In that
|
||||
case, they will be inlined in the main template, and can actually have many
|
||||
root nodes.
|
||||
|
||||
+61
-6
@@ -1,17 +1,72 @@
|
||||
# 🦉 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
|
||||
Context
|
||||
QWeb
|
||||
Store
|
||||
useState
|
||||
core
|
||||
EventBus
|
||||
Observer
|
||||
hooks
|
||||
onWillStart
|
||||
onMounted
|
||||
onWillUpdateProps
|
||||
onWillPatch
|
||||
onPatched
|
||||
onWillUnmount
|
||||
useContext
|
||||
useState
|
||||
useRef
|
||||
useSubEnv
|
||||
useStore
|
||||
useDispatch
|
||||
useGetters
|
||||
router
|
||||
Link
|
||||
RouteComponent
|
||||
Router
|
||||
tags
|
||||
xml
|
||||
utils
|
||||
debounce
|
||||
escape
|
||||
loadJS
|
||||
loadTemplates
|
||||
whenReady
|
||||
```
|
||||
|
||||
|
||||
Note that for convenience, the `useState` hook is also exported at the root of the `owl` object.
|
||||
|
||||
## Reference
|
||||
|
||||
- [Animations](animations.md)
|
||||
- [Component](component.md)
|
||||
- [QWeb](qweb.md)
|
||||
- [Store](store.md)
|
||||
- [Context](context.md)
|
||||
- [Event Bus](event_bus.md)
|
||||
- [Hooks](hooks.md)
|
||||
- [Observer](observer.md)
|
||||
- [Virtual DOM](vdom.md)
|
||||
- [QWeb](qweb.md)
|
||||
- [Router](router.md)
|
||||
- [Store](store.md)
|
||||
- [Tags](tags.md)
|
||||
- [Utils](utils.md)
|
||||
- [Virtual DOM](vdom.md)
|
||||
|
||||
## Learning Resources
|
||||
|
||||
- [Quick Start](quick_start.md)
|
||||
|
||||
## Miscellaneous
|
||||
- [Quick Start](quick_start.md)
|
||||
- [Animations](animations.md)
|
||||
- [Tooling](tooling.md)
|
||||
|
||||
- [Comparison with React/Vue](comparison.md)
|
||||
- [Tooling](tooling.md)
|
||||
- [Templates to start Owl applications (external link)](https://github.com/ged-odoo/owl-templates)
|
||||
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
# 🦉 Router 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Example](#example)
|
||||
- [Reference](#reference)
|
||||
- [Route Definition](#route-definition)
|
||||
- [Router](#router)
|
||||
- [Navigation Guards](#navigation-guards)
|
||||
- [RouteComponent](#routecomponent)
|
||||
- [Link](#link)
|
||||
|
||||
## Overview
|
||||
|
||||
It is often useful to organize an application around urls. If the application is
|
||||
a single page application, then we need a way to manage those urls in the browser.
|
||||
This is why there are many different routers for different frameworks. A generic
|
||||
router can do the job just fine, but a specialized router for Owl can give a
|
||||
better developer experience.
|
||||
|
||||
The Owl router support the following features:
|
||||
|
||||
- `history` or `hash` mode
|
||||
- declarative routes
|
||||
- route redirection
|
||||
- navigation guards
|
||||
- parameterized routes
|
||||
- a `<Link/>` component
|
||||
- a `<RouteComponent/>` component
|
||||
|
||||
Note that it is still in early stage of developments, and there are probably
|
||||
still some issues.
|
||||
|
||||
## Example
|
||||
|
||||
To use the Owl router, there are some steps that needs to be done:
|
||||
|
||||
- declare some routes
|
||||
- create a router
|
||||
- add it to the environment
|
||||
|
||||
```js
|
||||
async function protectRoute({ env, to }) {
|
||||
if (!env.session.authUser) {
|
||||
env.session.setNextRoute(to.name);
|
||||
return { to: "SIGN_IN" };
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export const ROUTES = [
|
||||
{ name: "LANDING", path: "/", component: Landing },
|
||||
{ name: "TASK", path: "/tasks/{{id}}", component: Task },
|
||||
{ name: "SIGN_UP", path: "/signup", component: SignUp },
|
||||
{ name: "SIGN_IN", path: "/signin", component: SignIn },
|
||||
{ name: "ADMIN", path: "/admin", component: Admin, beforeRouteEnter: protectRoute },
|
||||
{ name: "ACCOUNT", path: "/account", component: Account, beforeRouteEnter: protectRoute },
|
||||
{ name: "UNKNOWN", path: "*", redirect: { to: "LANDING" } }
|
||||
];
|
||||
|
||||
function makeEnvironment() {
|
||||
...
|
||||
const env = { qweb };
|
||||
env.session = new Session(env);
|
||||
env.router = new owl.router.Router(env, ROUTES);
|
||||
await env.router.start();
|
||||
return env;
|
||||
}
|
||||
```
|
||||
|
||||
Notice that the router needs to be started. This is an asynchronous operation
|
||||
because it needs to apply the potential navigation guards on the current route
|
||||
(which may or may not mean that the application is redirected to another route).
|
||||
|
||||
## Reference
|
||||
|
||||
### Route definition
|
||||
|
||||
A route need to be defined as an object with the following keys:
|
||||
|
||||
- `name` (optional): a (unique) string useful to identify the current route. If not
|
||||
given, it will be assigned an automatic name,
|
||||
- `path`: a string describing the url. It can be static: `/admin` or dynamic: `/users/{{id}}`.
|
||||
It also can be `*`, to catch all remaining routes.
|
||||
- `component` (optional): an Owl component that will be used by the `t-routecomponent`
|
||||
directive if the route is active
|
||||
- `redirect` (optional): should be destination object (with optional keys `path`, `to` and `params`) if given, the application will be redirected to the destination whenever we match this route
|
||||
- `beforeRouteEnter`: defines a [navigation guard](#navigation-guards).
|
||||
|
||||
### `Router`
|
||||
|
||||
The `Router` constructor takes three arguments:
|
||||
|
||||
- `env`: a valid environment,
|
||||
- a list of routes,
|
||||
- an optional object (with the only key `mode` which can be `history` (default
|
||||
value) or `hash`).
|
||||
|
||||
```js
|
||||
const ROUTES = [...];
|
||||
const router = new owl.router.Router(env, ROUTES, {mode: 'history'});
|
||||
```
|
||||
|
||||
Note that the route are defined in a list, and the order matters: the router
|
||||
tries to find a match by going down the list.
|
||||
|
||||
The router needs to be added to the environment in the `router` sub key.
|
||||
|
||||
Once a router is created, it needs to be started. This is necessary to initialize
|
||||
its current state to the current URL (and also, to potentially apply any
|
||||
navigation guards and/or redirecting).
|
||||
|
||||
```js
|
||||
await router.start();
|
||||
```
|
||||
|
||||
Once started, the router will keep track of the current url and reflect its
|
||||
value in two keys:
|
||||
|
||||
- `router.currentRoute`
|
||||
- `router.currentParams`
|
||||
|
||||
The router also has a `navigate` method, useful to programmatically change the
|
||||
application to another state (and the url):
|
||||
|
||||
```js
|
||||
router.navigate({ to: "USER", params: { id: 51 } });
|
||||
```
|
||||
|
||||
### Navigation Guards
|
||||
|
||||
Navigation guards are very useful to be able to execute some business logic/
|
||||
perform some actions or redirect to other routes whenever the application is
|
||||
entering a new route. For example, the following guard checks if there is an
|
||||
authenticated user, and if it is not the case, redirect to the sign in route.
|
||||
|
||||
```js
|
||||
async function protectRoute({ env, to }) {
|
||||
if (!env.session.authUser) {
|
||||
env.session.setNextRoute(to.name);
|
||||
return { to: "SIGN_IN" };
|
||||
}
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
A navigation guard is a function that returns a promise, which either resolves
|
||||
to `true` (the navigation is accepted), or to another destination object.
|
||||
|
||||
### `RouteComponent`
|
||||
|
||||
The `RouteComponent` component directs Owl to render the component associated
|
||||
to the currently active route (if any):
|
||||
|
||||
```xml
|
||||
<div t-name="App">
|
||||
<NavBar />
|
||||
<RouteComponent />
|
||||
</div>
|
||||
```
|
||||
|
||||
### `Link`
|
||||
|
||||
The `Link` component is a Owl component which render as a `<a>` tag with any
|
||||
content. It will compute the proper href from its props, and allow Owl to
|
||||
properly navigate to a given url if clicked on it.
|
||||
|
||||
```xml
|
||||
<Link to="'HOME'">Home</Link>
|
||||
```
|
||||
+205
-64
@@ -5,11 +5,13 @@
|
||||
- [Overview](#overview)
|
||||
- [Example](#example)
|
||||
- [Reference](#reference)
|
||||
- [Public API](#public-api)
|
||||
- [Mutations](#mutations)
|
||||
- [Store](#store)
|
||||
- [Actions](#actions)
|
||||
- [Getters](#getters)
|
||||
- [Connecting a component](#connecting-a-component)
|
||||
- [Connecting a Component](#connecting-a-component)
|
||||
- [`useStore`](#usestore)
|
||||
- [Semantics](#semantics)
|
||||
- [Good Practices](#good-practices)
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -20,88 +22,141 @@ in various parts of the user interface, and then, it is not obvious which
|
||||
component should own which part of the state.
|
||||
|
||||
Owl's solution to this issue is a centralized store. It is a class that owns
|
||||
some state, and let the developer update it in a structured way (through
|
||||
mutations and actions). Owl components can then connect to the store, and will
|
||||
be updated if necessary.
|
||||
some (or all) state, and let the developer update it in a structured way, with
|
||||
`actions`. Owl components can then connect to the store, and will be updated if
|
||||
necessary.
|
||||
|
||||
Note: Owl's store is inspired by React Redux and VueX.
|
||||
Note: Owl store is inspired by React Redux and VueX.
|
||||
|
||||
## Example
|
||||
|
||||
Here is what a simple store look like:
|
||||
Here is what a simple store looks like:
|
||||
|
||||
```js
|
||||
const actions = {
|
||||
addTodo({commit}, message) {
|
||||
commit('addTodo', message);
|
||||
}
|
||||
};
|
||||
|
||||
const mutations = {
|
||||
addTodo({state}, message) {
|
||||
const todo = {
|
||||
id: state.nextId++,
|
||||
message,
|
||||
isCompleted: false,
|
||||
};
|
||||
state.todos.push(todo);
|
||||
},
|
||||
addTodo({ state }, message) {
|
||||
state.todos.push({
|
||||
id: state.nextId++,
|
||||
message,
|
||||
isCompleted: false
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const state = {
|
||||
todos: [],
|
||||
nextId: 1,
|
||||
todos: [],
|
||||
nextId: 1
|
||||
};
|
||||
|
||||
const store = new owl.Store({state, actions, mutations});
|
||||
store.on('update', () => console.log(store.state));
|
||||
const store = new owl.Store({ state, actions });
|
||||
store.on("update", null, () => console.log(store.state));
|
||||
|
||||
// updating the state
|
||||
store.dispatch('addTodo', 'fix all bugs');
|
||||
store.dispatch("addTodo", "fix all bugs");
|
||||
```
|
||||
|
||||
This example shows how a store can be defined and used. Note that in most cases,
|
||||
actions will be dispatched by connected components.
|
||||
|
||||
## Reference
|
||||
|
||||
The store is a simple `owl.EventBus` that triggers `update` events whenever its
|
||||
state is changed. Note that these events are triggered only after a microtask
|
||||
tick, so only one event will be triggered for any number of state changes in a
|
||||
### `Store`
|
||||
|
||||
The store is a simple [`owl.EventBus`](event_bus.md) that triggers `update` events
|
||||
whenever its state is changed. Note that these events are triggered only after a
|
||||
microtask tick, so only one event will be triggered for any number of state changes in a
|
||||
call stack.
|
||||
|
||||
Also, it is important to mention that the state is observed (with a `owl.Observer`),
|
||||
which is the reason why it is able to know if it was changed. This implies that
|
||||
state changes need to be done carefully in some cases (adding a new key to an
|
||||
object, or modifying an array with the `arr[i] = newValue` syntax). See the
|
||||
Also, it is important to mention that the state is observed (with an `owl.Observer`),
|
||||
which is the reason why it is able to know if it was changed. See the
|
||||
[Observer](observer.md)'s documentation for more details.
|
||||
|
||||
### Public API
|
||||
The `Store` class is quite small. It has two public methods:
|
||||
|
||||
1. `constructor`
|
||||
2. `commit`
|
||||
3. `dispatch`
|
||||
- its constructor
|
||||
- `dispatch`
|
||||
|
||||
### Mutations
|
||||
The constructor takes a configuration object with four (optional) keys:
|
||||
|
||||
Mutations are the only way to modify the state. Changing the state outside a
|
||||
mutation is not allowed (and should throw an error). Mutations are synchronous.
|
||||
- the initial state
|
||||
- the actions
|
||||
- the getters
|
||||
- the environment
|
||||
|
||||
```javascript
|
||||
const config = {
|
||||
state,
|
||||
actions,
|
||||
getters,
|
||||
env
|
||||
};
|
||||
const store = new Store(config);
|
||||
```
|
||||
|
||||
### Actions
|
||||
|
||||
Actions are used to coordinate state changes. It is also useful whenever some
|
||||
asynchronous logic is necessary. For example, fetching data should be done
|
||||
in an action.
|
||||
Actions are used to coordinate state changes. It can be used for both synchronous
|
||||
and asynchronous logic.
|
||||
|
||||
```js
|
||||
const actions = {
|
||||
async login({commit}) {
|
||||
commit('setLoginState', 'pending');
|
||||
try {
|
||||
const loginInfo = await doSomeRPC('/login/', 'someinfo');
|
||||
commit('setLoginState', loginInfo);
|
||||
} catch {
|
||||
commit('setLoginState', 'error');
|
||||
}
|
||||
async login({ state }, info) {
|
||||
state.loginState = "pending";
|
||||
try {
|
||||
const loginInfo = await doSomeRPC("/login/", info);
|
||||
state.loginState = loginInfo;
|
||||
} catch (e) {
|
||||
state.loginState = "error";
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
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
|
||||
arbitrary number of arguments.
|
||||
|
||||
```js
|
||||
store.dispatch("login", someInfo);
|
||||
```
|
||||
|
||||
Note that anything returned by an action will also be returned by the `dispatch`
|
||||
call.
|
||||
|
||||
Also, it is important to be aware that we need to be careful with asynchronous
|
||||
logic. Each state change will potentially trigger a rerendering, so we need to
|
||||
make sure that we do not have a partial corrupted state. Here is an example that
|
||||
is likely not a good idea:
|
||||
|
||||
```javascript
|
||||
const actions = {
|
||||
async fetchSomeData({ state }, recordId) {
|
||||
state.recordId = recordId;
|
||||
const data = await doSomeRPC("/read/", recordId);
|
||||
state.recordData = data;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
In the previous example, there is a period of time in which the state has a
|
||||
`recordId` which does not correspond to the `recordData`. It is more likely that
|
||||
we want an atomic update: updating the `recordId` at the same time as the `recordData`
|
||||
values:
|
||||
|
||||
```javascript
|
||||
const actions = {
|
||||
async fetchSomeData({ state }, recordId) {
|
||||
const data = await doSomeRPC("/read/", recordId);
|
||||
state.recordId = recordId;
|
||||
state.recordData = data;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
@@ -128,22 +183,108 @@ transform the data contained in the store.
|
||||
|
||||
```js
|
||||
const getters = {
|
||||
getPost({state}, id) {
|
||||
const post = state.posts.find(p => p.id === id);
|
||||
const author = state.authors.find(a => a.id = post.id);
|
||||
return {
|
||||
id,
|
||||
author,
|
||||
content: post.content
|
||||
};
|
||||
},
|
||||
getPost({ state }, id) {
|
||||
const post = state.posts.find(p => p.id === id);
|
||||
const author = state.authors.find(a => (a.id = post.id));
|
||||
return {
|
||||
id,
|
||||
author,
|
||||
content: post.content
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// somewhere else
|
||||
const post = store.getters.getPost(id);
|
||||
|
||||
```
|
||||
|
||||
### Connecting a component
|
||||
Getters take _at most_ one argument.
|
||||
|
||||
Todo
|
||||
Note that getters are cached if they don't take any argument, or their argument
|
||||
is a string or a number.
|
||||
|
||||
### Connecting a Component
|
||||
|
||||
At some point, we need a way to interact with the store from a component. This
|
||||
can be done with the help of the three store hooks:
|
||||
|
||||
- `useStore` to subscribe a component to some part of the store state,
|
||||
- `useDispatch` to get a reference to a dispatch function
|
||||
- `useGetters` to get a reference to the getters defined in the store.
|
||||
|
||||
|
||||
Assume we have this store:
|
||||
|
||||
```javascript
|
||||
const actions = {
|
||||
increment({ state }, val) {
|
||||
state.counter.value += val;
|
||||
}
|
||||
};
|
||||
|
||||
const state = {
|
||||
counter: {value: 0}
|
||||
};
|
||||
const store = new owl.Store({ state, actions });
|
||||
```
|
||||
|
||||
A counter component can then select this value and dispatch an action like this:
|
||||
|
||||
```js
|
||||
class Counter extends Component {
|
||||
counter = useStore(state => state.counter);
|
||||
dispatch = useDispatch();
|
||||
}
|
||||
|
||||
const counter = new Counter({ store, qweb });
|
||||
```
|
||||
|
||||
```xml
|
||||
<button t-name="Counter" t-on-click="dispatch('increment')">
|
||||
Click Me! [<t t-esc="counter.value"/>]
|
||||
</button>
|
||||
```
|
||||
|
||||
### `useStore``
|
||||
|
||||
The `useStore` hook is used to select some part of the store state. It accepts
|
||||
two arguments:
|
||||
|
||||
- a selector function, which takes the store state as first argument (and the
|
||||
component props as second argument) and returns
|
||||
an object or an array (which will be then observed)
|
||||
- optionally, an object with a `store` key (if we want to override the default
|
||||
store) and an equality function (if we want to specialize the comparison)
|
||||
|
||||
|
||||
If the `useStore` callback selects a sub part of the store state, the component
|
||||
will only be rerendered whenever this part of the state changes. Otherwise, it
|
||||
will perform a strict equality check and updates the component every time this
|
||||
check fails.
|
||||
|
||||
### Semantics
|
||||
|
||||
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:
|
||||
|
||||
- components are always updated in the order of their creation (so, parent
|
||||
before children)
|
||||
- they are updated only if they are in the DOM
|
||||
- if a parent is asynchronous, the system will wait for it to complete its
|
||||
update before updating other components.
|
||||
- in general, updates are not coordinated. This is not a problem for synchronous
|
||||
components, but if there are many asynchronous components, this could lead to
|
||||
a situation where some part of the UI is updated and other parts of the UI is
|
||||
not updated.
|
||||
|
||||
### Good Practices
|
||||
|
||||
- avoid asynchronous components as much as possible. Asynchronous components
|
||||
lead to situations where parts of the UI is not updated immediately.
|
||||
- do not be afraid to connect many components, parent or children if needed. For
|
||||
example, a `MessageList` component could get a list of ids in its `useStore`
|
||||
call and a `Message` component could get the data of its own
|
||||
message
|
||||
- since the `useStore` function is called for each connected component,
|
||||
for each state update, it is important to make sure that these functions are
|
||||
as fast as possible.
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
# 🦉 Tags 🦉
|
||||
|
||||
Tags are very small helper 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.
|
||||
|
||||
## XML tag
|
||||
|
||||
Without tags, creating a standalone component would look like this:
|
||||
|
||||
```js
|
||||
import { Component } from 'owl'
|
||||
|
||||
const name = 'some-unique-name';
|
||||
const template = `
|
||||
<div>
|
||||
<span t-if="somecondition">text</span>
|
||||
<button t-on-click="someMethod">Click</button>
|
||||
</div>
|
||||
`;
|
||||
QWeb.registerTemplate(name, template);
|
||||
|
||||
class MyComponent extends Component {
|
||||
static template = name;
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
With tags, this process is slightly simplified. The name is uniquely generated,
|
||||
and the template is automatically registered:
|
||||
|
||||
```js
|
||||
import { Component } from 'owl'
|
||||
import { xml } from 'owl/tags'
|
||||
|
||||
class MyComponent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<span t-if="somecondition">text</span>
|
||||
<button t-on-click="someMethod">Click</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
+75
-6
@@ -1,18 +1,87 @@
|
||||
# 🦉 Tooling 🦉
|
||||
|
||||
## Development mode
|
||||
## Content
|
||||
|
||||
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
|
||||
- [Overview](#overview)
|
||||
- [Development Mode](#development-mode)
|
||||
- [Playground](#playground)
|
||||
- [Benchmarks](#benchmarks)
|
||||
- [Single File Component](#single-file-component)
|
||||
|
||||
## Overview
|
||||
|
||||
To help work with/improve/learn OWL, there are a few extras tools/settings.
|
||||
|
||||
- development mode: enable better error reporting for the developer
|
||||
- a playground application: a space to experiment and learn Owl.
|
||||
- a benchmarks application: allow comparison with a few common frameworks
|
||||
|
||||
The two applications are available in the `tools/` folder, and can be accessed
|
||||
by using a static http server. A simple python
|
||||
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`).
|
||||
|
||||
## 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
|
||||
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';
|
||||
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.
|
||||
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
|
||||
|
||||
The playground is an important application designed to help learning and
|
||||
experimenting with Owl. The last published version of Owl can be tested [online](https://odoo.github.io/owl/playground/).
|
||||
|
||||
It is an application similar to `jsFiddle`, but specialized for Owl: there are
|
||||
three tabs (`js`, `css` and `xml`), and a simple button `Run` to execute that
|
||||
code in an iframe.
|
||||
|
||||
## Benchmarks
|
||||
|
||||
Note: This is more an internal tool, useful for people working on Owl.
|
||||
|
||||
The benchmarks application is a very small application, implemented in different
|
||||
frameworks, and in different versions of Owl. This is a simple internal tool,
|
||||
useful to compare various performance metrics on some tasks.
|
||||
|
||||
## Single File Component
|
||||
|
||||
If you want to have `xml` syntax highlighting while using the `xml` helper which
|
||||
helps you define inline templates, there is a VS Code addon `Comment tagged template`
|
||||
which, if installed, does exactly that. To enable it, you need to add a comment,
|
||||
like this:
|
||||
|
||||
```js
|
||||
// -----------------------------------------------------------------------------
|
||||
// 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...
|
||||
}
|
||||
```
|
||||
|
||||
+41
-17
@@ -5,35 +5,59 @@ functions are all available in the `owl.utils` namespace.
|
||||
|
||||
## Content
|
||||
|
||||
- [`whenReady`](#whenready)
|
||||
- [`loadJS`](#loadjs)
|
||||
- [`loadTemplates`](#loadtemplates)
|
||||
- [`escape`](#escape)
|
||||
- [`debounce`](#debounce)
|
||||
- [`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` is useful to register some code that need to be executed
|
||||
as soon as the document (page) is ready:
|
||||
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
|
||||
owl.utils.whenReady(function () {
|
||||
const qweb = new owl.QWeb();
|
||||
const app = new App({ qweb });
|
||||
app.mount(document.body);
|
||||
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`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+16
-1
@@ -1,3 +1,18 @@
|
||||
# 🦉 VDom 🦉
|
||||
|
||||
Owl's virtual dom is a fork of [snabbdom](https://github.com/snabbdom/snabbdom).
|
||||
Owl is a declarative component system: we declare the structure of the component
|
||||
tree, and Owl will translate that to a list of imperative operations. This
|
||||
translation is done by a virtual dom. This is the low level layer of Owl, most
|
||||
developer will not need to call directly the virtual dom functions.
|
||||
|
||||
The main idea behind a virtual dom is to keep a in-memory representation of the
|
||||
DOM (called a virtual node), and whenever some change is needed, to regenerate
|
||||
a new representation, compute the difference between the old and the new, then
|
||||
apply the changes.
|
||||
|
||||
`vdom` exports two functions:
|
||||
|
||||
- `h`: create a new virtual node
|
||||
- `patch`: compare two virtual nodes, and apply the difference.
|
||||
|
||||
Note: Owl's virtual dom is a fork of [snabbdom](https://github.com/snabbdom/snabbdom).
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
<templates>
|
||||
<div class="main" t-name="root">
|
||||
<div class="left-thing">
|
||||
<div class="message_count">
|
||||
Number of msg: 0
|
||||
</div>
|
||||
<button class="o_btn_msg 100">Add 100 messages</button>
|
||||
<button class="o_btn_msg 1000">Add 1000 messages</button>
|
||||
<button class="o_btn_msg 10000">Add 10000 messages</button>
|
||||
<button class="o_btn_msg 50000">Add 50000 messages</button>
|
||||
<button class="updateSomeMessages">Update every 10th message</button>
|
||||
<button class="clear">Clear</button>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="message" class="message">
|
||||
<span class="author"><t t-esc="widget.author"/></span>
|
||||
<span class="msg"><t t-esc="widget.msg"/></span>
|
||||
<button class="remove" >Remove</button>
|
||||
</div>
|
||||
|
||||
<div t-name="counter">
|
||||
<button class="o_increment"></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -1,30 +0,0 @@
|
||||
<templates>
|
||||
<div class="main" t-name="root">
|
||||
<div class="left-thing">
|
||||
<div class="message_count">
|
||||
Number of msg: 0
|
||||
</div>
|
||||
<button class="o_btn_msg 100">Add 100 messages</button>
|
||||
<button class="o_btn_msg 1000">Add 1000 messages</button>
|
||||
<button class="o_btn_msg 10000">Add 10000 messages</button>
|
||||
<button class="o_btn_msg 50000">Add 50000 messages</button>
|
||||
<button class="updateSomeMessages">Update every 10th message</button>
|
||||
<button class="clear">Clear</button>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="message" class="message">
|
||||
<span class="author"><t t-esc="widget.author"/></span>
|
||||
<span class="msg"><t t-esc="widget.msg"/></span>
|
||||
<button class="remove" >Remove</button>
|
||||
</div>
|
||||
|
||||
<div t-name="counter">
|
||||
<button class="o_increment"></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -1,84 +0,0 @@
|
||||
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Likes Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Counter extends owl.Component {
|
||||
template = "counter";
|
||||
state = { counter: 0 };
|
||||
|
||||
increment() {
|
||||
this.state.counter++;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Message extends owl.Component {
|
||||
template = "message";
|
||||
widgets = { Counter };
|
||||
|
||||
shouldUpdate(nextProps) {
|
||||
return nextProps !== this.props;
|
||||
}
|
||||
removeMessage() {
|
||||
this.trigger("remove_message", {
|
||||
id: this.props.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class App extends owl.Component {
|
||||
template = "root";
|
||||
widgets = { Message };
|
||||
state = { messages: [] };
|
||||
|
||||
addMessages(n) {
|
||||
startMeasure("add " + n);
|
||||
const newMessages = buildData(n);
|
||||
this.state.messages.push.apply(this.state.messages, newMessages);
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
clear() {
|
||||
startMeasure("clear");
|
||||
this.state.messages = [];
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
updateSomeMessages() {
|
||||
startMeasure("update every 10th");
|
||||
const messages = this.state.messages;
|
||||
for (let i = 0; i < this.state.messages.length; i += 10) {
|
||||
const msg = Object.assign({}, messages[i]);
|
||||
msg.author += '!!!';
|
||||
this.set(messages, i, msg);
|
||||
}
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
removeMessage(data) {
|
||||
startMeasure("remove message");
|
||||
const index = this.state.messages.findIndex(m => m.id === data.id);
|
||||
this.state.messages.splice(index, 1);
|
||||
stopMeasure();
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// 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();
|
||||
@@ -1,32 +0,0 @@
|
||||
<templates>
|
||||
<div t-name="root" class="main">
|
||||
<div class="left-thing">
|
||||
<div>Number of msg: <t t-esc="state.messages.length"/></div>
|
||||
<button t-on-click="addMessages(100)">Add 100 messages</button>
|
||||
<button t-on-click="addMessages(1000)">Add 1000 messages</button>
|
||||
<button t-on-click="addMessages(10000)">Add 10000 messages</button>
|
||||
<button t-on-click="addMessages(50000)">Add 50000 messages</button>
|
||||
<button t-on-click="updateSomeMessages">Update every 10th message</button>
|
||||
<button t-on-click="clear">Clear</button>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content">
|
||||
<t t-foreach="state.messages" t-as="message">
|
||||
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="message" class="message">
|
||||
<span class="author"><t t-esc="props.author"/></span>
|
||||
<span class="msg"><t t-esc="props.msg"/></span>
|
||||
<button class="remove" t-on-click="removeMessage">Remove</button>
|
||||
<t t-widget="Counter"/>
|
||||
</div>
|
||||
|
||||
<div t-name="counter">
|
||||
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -1,84 +0,0 @@
|
||||
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Likes Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Counter extends owl.Component {
|
||||
template = "counter";
|
||||
state = { counter: 0 };
|
||||
|
||||
increment() {
|
||||
this.state.counter++;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Message extends owl.Component {
|
||||
template = "message";
|
||||
widgets = { Counter };
|
||||
|
||||
shouldUpdate(nextProps) {
|
||||
return nextProps !== this.props;
|
||||
}
|
||||
removeMessage() {
|
||||
this.trigger("remove_message", {
|
||||
id: this.props.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class App extends owl.Component {
|
||||
template = "root";
|
||||
widgets = { Message };
|
||||
state = { messages: [] };
|
||||
|
||||
addMessages(n) {
|
||||
startMeasure("add " + n);
|
||||
const newMessages = buildData(n);
|
||||
this.state.messages.push.apply(this.state.messages, newMessages);
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
clear() {
|
||||
startMeasure("clear");
|
||||
this.state.messages = [];
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
updateSomeMessages() {
|
||||
startMeasure("update every 10th");
|
||||
const messages = this.state.messages;
|
||||
for (let i = 0; i < this.state.messages.length; i += 10) {
|
||||
const msg = Object.assign({}, messages[i]);
|
||||
msg.author += '!!!';
|
||||
this.set(messages, i, msg);
|
||||
}
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
removeMessage(data) {
|
||||
startMeasure("remove message");
|
||||
const index = this.state.messages.findIndex(m => m.id === data.id);
|
||||
this.state.messages.splice(index, 1);
|
||||
stopMeasure();
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// 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();
|
||||
@@ -1,32 +0,0 @@
|
||||
<templates>
|
||||
<div t-name="root" class="main">
|
||||
<div class="left-thing">
|
||||
<div>Number of msg: <t t-esc="state.messages.length"/></div>
|
||||
<button t-on-click="addMessages(100)">Add 100 messages</button>
|
||||
<button t-on-click="addMessages(1000)">Add 1000 messages</button>
|
||||
<button t-on-click="addMessages(10000)">Add 10000 messages</button>
|
||||
<button t-on-click="addMessages(50000)">Add 50000 messages</button>
|
||||
<button t-on-click="updateSomeMessages">Update every 10th message</button>
|
||||
<button t-on-click="clear">Clear</button>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content">
|
||||
<t t-foreach="state.messages" t-as="message">
|
||||
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="message" class="message">
|
||||
<span class="author"><t t-esc="props.author"/></span>
|
||||
<span class="msg"><t t-esc="props.msg"/></span>
|
||||
<button class="remove" t-on-click="removeMessage">Remove</button>
|
||||
<t t-widget="Counter"/>
|
||||
</div>
|
||||
|
||||
<div t-name="counter">
|
||||
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -1,85 +0,0 @@
|
||||
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Likes Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Counter extends owl.Component {
|
||||
template = "counter";
|
||||
state = { counter: 0 };
|
||||
|
||||
increment() {
|
||||
this.state.counter++;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Message extends owl.Component {
|
||||
template = "message";
|
||||
widgets = { Counter };
|
||||
|
||||
shouldUpdate(nextProps) {
|
||||
return nextProps !== this.props;
|
||||
}
|
||||
|
||||
removeMessage() {
|
||||
this.trigger("remove_message", {
|
||||
id: this.props.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class App extends owl.Component {
|
||||
template = "root";
|
||||
widgets = { Message };
|
||||
state = { messages: [] };
|
||||
|
||||
addMessages(n) {
|
||||
startMeasure("add " + n);
|
||||
const newMessages = buildData(n);
|
||||
this.state.messages.push.apply(this.state.messages, newMessages);
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
clear() {
|
||||
startMeasure("clear");
|
||||
this.state.messages = [];
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
updateSomeMessages() {
|
||||
startMeasure("update every 10th");
|
||||
const messages = this.state.messages;
|
||||
for (let i = 0; i < this.state.messages.length; i += 10) {
|
||||
const msg = Object.assign({}, messages[i]);
|
||||
msg.author += '!!!';
|
||||
this.set(messages, i, msg);
|
||||
}
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
removeMessage(data) {
|
||||
startMeasure('remove message');
|
||||
const index = this.state.messages.findIndex(m => m.id === data.id);
|
||||
this.state.messages.splice(index, 1);
|
||||
stopMeasure();
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// 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();
|
||||
@@ -1,32 +0,0 @@
|
||||
<templates>
|
||||
<div t-name="root" class="main">
|
||||
<div class="left-thing">
|
||||
<div>Number of msg: <t t-esc="state.messages.length"/></div>
|
||||
<button t-on-click="addMessages(100)">Add 100 messages</button>
|
||||
<button t-on-click="addMessages(1000)">Add 1000 messages</button>
|
||||
<button t-on-click="addMessages(10000)">Add 10000 messages</button>
|
||||
<button t-on-click="addMessages(50000)">Add 50000 messages</button>
|
||||
<button t-on-click="updateSomeMessages">Update every 10th message</button>
|
||||
<button t-on-click="clear">Clear</button>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content">
|
||||
<t t-foreach="state.messages" t-as="message">
|
||||
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="message" class="message">
|
||||
<span class="author"><t t-esc="props.author"/></span>
|
||||
<span class="msg"><t t-esc="props.msg"/></span>
|
||||
<button class="remove" t-on-click="removeMessage">Remove</button>
|
||||
<t t-widget="Counter"/>
|
||||
</div>
|
||||
|
||||
<div t-name="counter">
|
||||
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -1,84 +0,0 @@
|
||||
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Likes Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Counter extends owl.Component {
|
||||
template = "counter";
|
||||
state = { counter: 0 };
|
||||
|
||||
increment() {
|
||||
this.state.counter++;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Message extends owl.Component {
|
||||
template = "message";
|
||||
widgets = { Counter };
|
||||
|
||||
shouldUpdate(nextProps) {
|
||||
return nextProps !== this.props;
|
||||
}
|
||||
removeMessage() {
|
||||
this.trigger("remove_message", {
|
||||
id: this.props.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class App extends owl.Component {
|
||||
template = "root";
|
||||
widgets = { Message };
|
||||
state = { messages: [] };
|
||||
|
||||
addMessages(n) {
|
||||
startMeasure("add " + n);
|
||||
const newMessages = buildData(n);
|
||||
this.state.messages.push.apply(this.state.messages, newMessages);
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
clear() {
|
||||
startMeasure("clear");
|
||||
this.state.messages = [];
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
updateSomeMessages() {
|
||||
startMeasure("update every 10th");
|
||||
const messages = this.state.messages;
|
||||
for (let i = 0; i < this.state.messages.length; i += 10) {
|
||||
const msg = Object.assign({}, messages[i]);
|
||||
msg.author += '!!!';
|
||||
this.set(messages, i, msg);
|
||||
}
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
removeMessage(data) {
|
||||
startMeasure("remove message");
|
||||
const index = this.state.messages.findIndex(m => m.id === data.id);
|
||||
this.state.messages.splice(index, 1);
|
||||
stopMeasure();
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// 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();
|
||||
@@ -1,32 +0,0 @@
|
||||
<templates>
|
||||
<div t-name="root" class="main">
|
||||
<div class="left-thing">
|
||||
<div>Number of msg: <t t-esc="state.messages.length"/></div>
|
||||
<button t-on-click="addMessages(100)">Add 100 messages</button>
|
||||
<button t-on-click="addMessages(1000)">Add 1000 messages</button>
|
||||
<button t-on-click="addMessages(10000)">Add 10000 messages</button>
|
||||
<button t-on-click="addMessages(50000)">Add 50000 messages</button>
|
||||
<button t-on-click="updateSomeMessages">Update every 10th message</button>
|
||||
<button t-on-click="clear">Clear</button>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content">
|
||||
<t t-foreach="state.messages" t-as="message">
|
||||
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="message" class="message">
|
||||
<span class="author"><t t-esc="props.author"/></span>
|
||||
<span class="msg"><t t-esc="props.msg"/></span>
|
||||
<button class="remove" t-on-click="removeMessage">Remove</button>
|
||||
<t t-widget="Counter"/>
|
||||
</div>
|
||||
|
||||
<div t-name="counter">
|
||||
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -1,84 +0,0 @@
|
||||
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Likes Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Counter extends owl.Component {
|
||||
template = "counter";
|
||||
state = { counter: 0 };
|
||||
|
||||
increment() {
|
||||
this.state.counter++;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Message extends owl.Component {
|
||||
template = "message";
|
||||
widgets = { Counter };
|
||||
|
||||
shouldUpdate(nextProps) {
|
||||
return nextProps !== this.props;
|
||||
}
|
||||
removeMessage() {
|
||||
this.trigger("remove_message", {
|
||||
id: this.props.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class App extends owl.Component {
|
||||
template = "root";
|
||||
widgets = { Message };
|
||||
state = { messages: [] };
|
||||
|
||||
addMessages(n) {
|
||||
startMeasure("add " + n);
|
||||
const newMessages = buildData(n);
|
||||
this.state.messages.push.apply(this.state.messages, newMessages);
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
clear() {
|
||||
startMeasure("clear");
|
||||
this.state.messages = [];
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
updateSomeMessages() {
|
||||
startMeasure("update every 10th");
|
||||
const messages = this.state.messages;
|
||||
for (let i = 0; i < this.state.messages.length; i += 10) {
|
||||
const msg = Object.assign({}, messages[i]);
|
||||
msg.author += '!!!';
|
||||
this.set(messages, i, msg);
|
||||
}
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
removeMessage(data) {
|
||||
startMeasure("remove message");
|
||||
const index = this.state.messages.findIndex(m => m.id === data.id);
|
||||
this.state.messages.splice(index, 1);
|
||||
stopMeasure();
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// 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();
|
||||
@@ -1,32 +0,0 @@
|
||||
<templates>
|
||||
<div t-name="root" class="main">
|
||||
<div class="left-thing">
|
||||
<div>Number of msg: <t t-esc="state.messages.length"/></div>
|
||||
<button t-on-click="addMessages(100)">Add 100 messages</button>
|
||||
<button t-on-click="addMessages(1000)">Add 1000 messages</button>
|
||||
<button t-on-click="addMessages(10000)">Add 10000 messages</button>
|
||||
<button t-on-click="addMessages(50000)">Add 50000 messages</button>
|
||||
<button t-on-click="updateSomeMessages">Update every 10th message</button>
|
||||
<button t-on-click="clear">Clear</button>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content">
|
||||
<t t-foreach="state.messages" t-as="message">
|
||||
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="message" class="message">
|
||||
<span class="author"><t t-esc="props.author"/></span>
|
||||
<span class="msg"><t t-esc="props.msg"/></span>
|
||||
<button class="remove" t-on-click="removeMessage">Remove</button>
|
||||
<t t-widget="Counter"/>
|
||||
</div>
|
||||
|
||||
<div t-name="counter">
|
||||
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -1,81 +0,0 @@
|
||||
import { buildData, startMeasure, stopMeasure } 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 {
|
||||
widgets = { Counter };
|
||||
|
||||
shouldUpdate(nextProps) {
|
||||
return nextProps !== this.props;
|
||||
}
|
||||
removeMessage() {
|
||||
this.trigger("remove_message", {
|
||||
id: this.props.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class App extends owl.Component {
|
||||
widgets = { Message };
|
||||
state = { messages: [] };
|
||||
|
||||
addMessages(n) {
|
||||
startMeasure("add " + n);
|
||||
const newMessages = buildData(n);
|
||||
this.state.messages.push.apply(this.state.messages, newMessages);
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
clear() {
|
||||
startMeasure("clear");
|
||||
this.state.messages = [];
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
updateSomeMessages() {
|
||||
startMeasure("update every 10th");
|
||||
const messages = this.state.messages;
|
||||
for (let i = 0; i < this.state.messages.length; i += 10) {
|
||||
const msg = Object.assign({}, messages[i]);
|
||||
msg.author += '!!!';
|
||||
this.set(messages, i, msg);
|
||||
}
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
removeMessage(data) {
|
||||
startMeasure("remove message");
|
||||
const index = this.state.messages.findIndex(m => m.id === data.id);
|
||||
this.state.messages.splice(index, 1);
|
||||
stopMeasure();
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// 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();
|
||||
@@ -1,32 +0,0 @@
|
||||
<templates>
|
||||
<div t-name="App" class="main">
|
||||
<div class="left-thing">
|
||||
<div>Number of msg: <t t-esc="state.messages.length"/></div>
|
||||
<button t-on-click="addMessages(100)">Add 100 messages</button>
|
||||
<button t-on-click="addMessages(1000)">Add 1000 messages</button>
|
||||
<button t-on-click="addMessages(10000)">Add 10000 messages</button>
|
||||
<button t-on-click="addMessages(50000)">Add 50000 messages</button>
|
||||
<button t-on-click="updateSomeMessages">Update every 10th message</button>
|
||||
<button t-on-click="clear">Clear</button>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content">
|
||||
<t t-foreach="state.messages" t-as="message">
|
||||
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="Message" class="message">
|
||||
<span class="author"><t t-esc="props.author"/></span>
|
||||
<span class="msg"><t t-esc="props.msg"/></span>
|
||||
<button class="remove" t-on-click="removeMessage">Remove</button>
|
||||
<t t-widget="Counter"/>
|
||||
</div>
|
||||
|
||||
<div t-name="Counter">
|
||||
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -1,221 +0,0 @@
|
||||
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Counter extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
counter: 0
|
||||
};
|
||||
this.increment = this.increment.bind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
return React.createElement(
|
||||
"div",
|
||||
null,
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: this.increment
|
||||
},
|
||||
"Value: ",
|
||||
this.state.counter
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
increment() {
|
||||
this.setState({
|
||||
counter: this.state.counter + 1
|
||||
});
|
||||
}
|
||||
}
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Message extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.removeMessage = this.removeMessage.bind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
return React.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "message"
|
||||
},
|
||||
React.createElement(
|
||||
"span",
|
||||
{
|
||||
className: "author"
|
||||
},
|
||||
this.props.message.author
|
||||
),
|
||||
React.createElement(
|
||||
"span",
|
||||
{
|
||||
className: "msg"
|
||||
},
|
||||
this.props.message.msg
|
||||
),
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
className: "remove",
|
||||
onClick: this.removeMessage
|
||||
},
|
||||
"Remove"
|
||||
),
|
||||
React.createElement(Counter, null)
|
||||
);
|
||||
}
|
||||
|
||||
removeMessage() {
|
||||
this.props.removeCB(this.props.message.id);
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Main extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
messages: []
|
||||
};
|
||||
this.removeMessage = this.removeMessage.bind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
const msgList = this.state.messages.map(m =>
|
||||
React.createElement(Message, {
|
||||
key: m.id,
|
||||
message: m,
|
||||
removeCB: this.removeMessage
|
||||
})
|
||||
);
|
||||
return React.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "main"
|
||||
},
|
||||
React.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "left-thing"
|
||||
},
|
||||
React.createElement(
|
||||
"div",
|
||||
null,
|
||||
"Number of msg: ",
|
||||
this.state.messages.length
|
||||
),
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: _ => this.addMessages(100)
|
||||
},
|
||||
"Add 100 messages"
|
||||
),
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: _ => this.addMessages(1000)
|
||||
},
|
||||
"Add 1000 messages"
|
||||
),
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: _ => this.addMessages(10000)
|
||||
},
|
||||
"Add 10000 messages"
|
||||
),
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: _ => this.addMessages(50000)
|
||||
},
|
||||
"Add 50000 messages"
|
||||
),
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: _ => this.updateSomeMessages()
|
||||
},
|
||||
"Update every 10th messags"
|
||||
),
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: _ => this.clear()
|
||||
},
|
||||
"Clear"
|
||||
)
|
||||
),
|
||||
React.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "right-thing"
|
||||
},
|
||||
React.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "content"
|
||||
},
|
||||
msgList
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
addMessages(n) {
|
||||
startMeasure("add " + n);
|
||||
const newMessages = this.state.messages.concat(buildData(n));
|
||||
this.setState({
|
||||
messages: newMessages
|
||||
});
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
clear() {
|
||||
startMeasure("clear");
|
||||
this.setState({
|
||||
messages: []
|
||||
});
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
updateSomeMessages() {
|
||||
startMeasure("update every 10th");
|
||||
const messages = this.state.messages;
|
||||
for (let i = 0; i < messages.length; i += 10) {
|
||||
messages[i].author += "!!!";
|
||||
}
|
||||
this.forceUpdate();
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
removeMessage(id) {
|
||||
startMeasure("remove message");
|
||||
const index = this.state.messages.findIndex(m => m.id === id);
|
||||
const messages = this.state.messages.slice();
|
||||
messages.splice(index, 1);
|
||||
this.setState({
|
||||
messages
|
||||
});
|
||||
stopMeasure();
|
||||
}
|
||||
} //-----------------------------
|
||||
// INIT
|
||||
//-----------------------------
|
||||
|
||||
ReactDOM.render(
|
||||
React.createElement(Main, null),
|
||||
document.getElementById("main")
|
||||
);
|
||||
@@ -1,42 +0,0 @@
|
||||
.main {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 220px 1fr;
|
||||
}
|
||||
|
||||
.left-thing {
|
||||
background-color: gray;
|
||||
padding: 20px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.left-thing button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.right-thing {
|
||||
padding: 20px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Message widget */
|
||||
.message .author {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.message {
|
||||
width: 400px;
|
||||
background-color: lightblue;
|
||||
margin: 10px 5px;
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.remove {
|
||||
float: right;
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
Vue.component("likes-counter", {
|
||||
data: function() {
|
||||
return {
|
||||
counter: 0
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
increment() {
|
||||
this.counter++;
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<button @click="increment">Value: {{counter}}</button>
|
||||
</div>`
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
Vue.component("my-message", {
|
||||
props: ["msg"],
|
||||
methods: {
|
||||
removeMessage() {
|
||||
this.$emit("removeMessage", this.msg.id);
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="message">
|
||||
<span class="author">{{msg.author}}</span>
|
||||
<span class="msg">{{msg.msg}}</span>
|
||||
<button class="remove" @click="removeMessage">Remove</button>
|
||||
<likes-counter></likes-counter>
|
||||
</div>`
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
const App = {
|
||||
name: "App",
|
||||
data() {
|
||||
return { messages: [] };
|
||||
},
|
||||
methods: {
|
||||
addMessages(n) {
|
||||
startMeasure("add " + n);
|
||||
const newMessages = buildData(n);
|
||||
this.messages.push.apply(this.messages, newMessages);
|
||||
stopMeasure();
|
||||
},
|
||||
|
||||
clear() {
|
||||
startMeasure("clear");
|
||||
this.messages = [];
|
||||
stopMeasure();
|
||||
},
|
||||
|
||||
updateSomeMessages() {
|
||||
startMeasure("update every 10th");
|
||||
const messages = this.messages;
|
||||
for (let i = 0; i < this.messages.length; i += 10) {
|
||||
messages[i].author += "!!!";
|
||||
}
|
||||
stopMeasure();
|
||||
},
|
||||
|
||||
removeMessage(id) {
|
||||
startMeasure("remove message");
|
||||
const index = this.messages.findIndex(m => m.id === id);
|
||||
this.messages.splice(index, 1);
|
||||
stopMeasure();
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="main">
|
||||
<div class="left-thing">
|
||||
<div>Number of msg: {{messages.length}}</div>
|
||||
<button @click="addMessages(100)">Add 100 messages</button>
|
||||
<button @click="addMessages(1000)">Add 1000 messages</button>
|
||||
<button @click="addMessages(10000)">Add 10000 messages</button>
|
||||
<button @click="addMessages(50000)">Add 50000 messages</button>
|
||||
<button @click="updateSomeMessages">Update every 10th message</button>
|
||||
<button @click="clear">Clear</button>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content">
|
||||
<my-message v-for="msg in messages" v-bind:key="msg.id" v-bind:msg="msg" @removeMessage="removeMessage"></my-message>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Application initialization
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
new Vue({
|
||||
render: h => h(App)
|
||||
}).$mount(`#main`);
|
||||
@@ -1,43 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>OWL Extras</title>
|
||||
<link rel="stylesheet" href="main.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="title">
|
||||
<h1>🦉 OWL Extra Stuff 🦉</h1>
|
||||
<div>Mysterious OWL: A web framework for structured, dynamic and maintainable applications</div>
|
||||
</div>
|
||||
|
||||
<div class="section misc">
|
||||
<h2>Misc</h2>
|
||||
<div class="links">
|
||||
<a href="playground">Playground (local)</a>
|
||||
<a href="https://github.com/odoo/owl">Github</a>
|
||||
<a href="https://odoo.github.io/owl/">Owl main page</a>
|
||||
<a href="https://github.com/odoo/owl/blob/master/doc/readme.md">Owl Documentation</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section benchmarks">
|
||||
<h2>Benchmarks</h2>
|
||||
|
||||
<ul>
|
||||
<li><a href="benchmarks/odoo-widgets-12.0">Odoo Widgets (12.0)</a></li>
|
||||
<li><a href="benchmarks/odoo-widgets-12.3">Odoo Widgets (12.3)</a></li>
|
||||
<li><a href="benchmarks/owl-0.7.0">OWL 0.7.0</a></li>
|
||||
<li><a href="benchmarks/owl-0.8.0">OWL 0.8.0</a></li>
|
||||
<li><a href="benchmarks/owl-0.9.0">OWL 0.9.0</a></li>
|
||||
<li><a href="benchmarks/owl-0.10.0">OWL 0.10.0</a></li>
|
||||
<li><a href="benchmarks/owl-0.11.0">OWL 0.11.0</a></li>
|
||||
<li><a href="benchmarks/owl-master">OWL Master</a></li>
|
||||
<li><a href="benchmarks/vue">Vue</a></li>
|
||||
<li><a href="benchmarks/react">React</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,23 +0,0 @@
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.title, .section {
|
||||
width: 768px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.title div {
|
||||
color: #333333;
|
||||
font-style: italic;
|
||||
}
|
||||
.section {
|
||||
background-color: #d5d5d5;
|
||||
padding: 16px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.links a {
|
||||
margin: 10px 5px;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,21 +0,0 @@
|
||||
# 🦉 Extra Stuff 🦉
|
||||
|
||||
To help work/improve/learn with OWL, we have here:
|
||||
|
||||
- a benchmarks application
|
||||
- a playground application
|
||||
|
||||
Both of them can be accessed by using a static http server. A simple python
|
||||
server is available in `server.py`. There is also a npm script to start it:
|
||||
`npm run extras` (and its version with a watcher: `npm run extras:watch`).
|
||||
|
||||
## Benchmarks
|
||||
|
||||
The benchmarks application is a very small application, implemented in different
|
||||
frameworks, and in different versions of Owl. This is a simple internal tool,
|
||||
useful to compare various performance metrics on some tasks.
|
||||
|
||||
## Playground
|
||||
|
||||
The playground is an important application designed to help learning and
|
||||
experimenting with Owl. It is available [online](https://odoo.github.io/owl/playground/).
|
||||
@@ -1,38 +0,0 @@
|
||||
import sys
|
||||
import thread
|
||||
import webbrowser
|
||||
import time
|
||||
|
||||
import BaseHTTPServer
|
||||
import SimpleHTTPServer
|
||||
|
||||
HOST = '127.0.0.1'
|
||||
PORT = 8000
|
||||
URL = 'http://{0}:{1}/extras'.format(HOST, PORT)
|
||||
|
||||
|
||||
# We define our own handler here to remap owl.js GET requests to the Owl build
|
||||
# in dist/. This is useful for the benchmarks and playground applications.
|
||||
# With this, we can simply copy the playground folder as is in the gh-page when
|
||||
# we want to update the playground.
|
||||
class OWLHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
if self.path == '/extras/owl.js':
|
||||
self.path = '/dist/owl.js'
|
||||
return SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
|
||||
|
||||
|
||||
def start_server():
|
||||
httpd = BaseHTTPServer.HTTPServer((HOST, PORT), OWLHandler)
|
||||
httpd.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
thread.start_new_thread(start_server, ())
|
||||
webbrowser.open_new(URL)
|
||||
|
||||
while True:
|
||||
try:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(0)
|
||||
+15
-10
@@ -1,34 +1,38 @@
|
||||
{
|
||||
"name": "owl",
|
||||
"version": "0.12.0",
|
||||
"name": "owl-framework",
|
||||
"version": "0.23.0",
|
||||
"description": "Odoo Web Library (OWL)",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"build:js": "tsc --target esnext --module es6 --outDir dist/owl src/*",
|
||||
"build:js": "tsc --target esnext --module es6 --outDir dist/owl",
|
||||
"build:bundle": "rollup -c",
|
||||
"build": "npm run build:js && npm run build:bundle",
|
||||
"buildcommonjs": "npm run build:js && npm run build:bundle -- -f cjs",
|
||||
"minify": "uglifyjs dist/owl.js -o dist/owl.min.js --compress --mangle",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"extras:serve": "python extras/server.py",
|
||||
"extras": "npm run build && npm run extras:serve",
|
||||
"extras:watch": "npm-run-all --parallel extras:serve \"build:* -- --watch\""
|
||||
"tools:serve": "python3 tools/server.py || python tools/server.py",
|
||||
"tools": "npm run build && npm run tools:serve",
|
||||
"pretools:watch": "npm run build",
|
||||
"tools:watch": "npm-run-all --parallel tools:serve \"build:* -- --watch\""
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/odoo/owl.git"
|
||||
},
|
||||
"author": "Odoo",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"license": "LGPL",
|
||||
"bugs": {
|
||||
"url": "https://github.com/odoo/owl/issues"
|
||||
},
|
||||
"homepage": "https://github.com/odoo/owl#readme",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^23.3.12",
|
||||
"cpx": "^1.5.0",
|
||||
"git-rev-sync": "^1.12.0",
|
||||
"jest": "^23.6.0",
|
||||
"jest-environment-jsdom": "^24.7.1",
|
||||
"live-server": "^1.2.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"rollup": "^1.6.0",
|
||||
@@ -37,10 +41,8 @@
|
||||
"source-map-support": "^0.5.10",
|
||||
"ts-jest": "^23.10.5",
|
||||
"typescript": "^3.2.2",
|
||||
"uglify-es": "^3.3.9",
|
||||
"jest-environment-jsdom": "^24.7.1"
|
||||
"uglify-es": "^3.3.9"
|
||||
},
|
||||
"dependencies": {},
|
||||
"jest": {
|
||||
"roots": [
|
||||
"<rootDir>/src",
|
||||
@@ -59,5 +61,8 @@
|
||||
"json",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"prettier": {
|
||||
"printWidth": 100
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,610 +0,0 @@
|
||||
import { EventBus } from "./event_bus";
|
||||
import { Observer } from "./observer";
|
||||
import { QWeb, CompiledTemplate } from "./qweb_core";
|
||||
import { h, patch, VNode } from "./vdom";
|
||||
|
||||
/**
|
||||
* Owl Component System
|
||||
*
|
||||
* This file introduces a declarative and composable component system. It
|
||||
* contains:
|
||||
*
|
||||
* - the Env interface (generic type for the environment)
|
||||
* - the Meta interface (the owl specific metadata attached to a component)
|
||||
* - the Component class
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Types/helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export interface Env {
|
||||
qweb: QWeb;
|
||||
}
|
||||
|
||||
export interface Meta<T extends Env, Props> {
|
||||
readonly id: number;
|
||||
vnode: VNode | null;
|
||||
isMounted: boolean;
|
||||
isDestroyed: boolean;
|
||||
parent: Component<T, any, any> | null;
|
||||
children: { [key: number]: Component<T, any, any> };
|
||||
// children mapping: from templateID to widgetID
|
||||
// should it be a map number => Widget?
|
||||
cmap: { [key: number]: number };
|
||||
|
||||
renderId: number;
|
||||
renderProps: Props | null;
|
||||
renderPromise: Promise<VNode> | null;
|
||||
boundHandlers: { [key: number]: any };
|
||||
observer?: Observer;
|
||||
render?: CompiledTemplate;
|
||||
mountedHandlers: { [key: number]: Function };
|
||||
}
|
||||
|
||||
// If a component does not define explicitely a template
|
||||
// key, it needs to find a template with its name (or a parent's). This is
|
||||
// qweb dependant, so we need a place to store this information indexed by
|
||||
// qweb instances.
|
||||
const TEMPLATE_MAP: { [key: number]: { [name: string]: string } } = {};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Widget
|
||||
//------------------------------------------------------------------------------
|
||||
let nextId = 1;
|
||||
|
||||
export class Component<
|
||||
T extends Env,
|
||||
Props extends {},
|
||||
State extends {}
|
||||
> extends EventBus {
|
||||
readonly __owl__: Meta<Env, Props>;
|
||||
template?: string;
|
||||
|
||||
get el(): HTMLElement | null {
|
||||
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
|
||||
}
|
||||
|
||||
env: T;
|
||||
state?: State;
|
||||
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
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates an instance of Component.
|
||||
*
|
||||
* The root widget of a component tree needs an environment:
|
||||
*
|
||||
* ```javascript
|
||||
* const root = new RootWidget(env, props);
|
||||
* ```
|
||||
*
|
||||
* Every other widget simply needs a reference to its parent:
|
||||
*
|
||||
* ```javascript
|
||||
* const child = new SomeWidget(parent, props);
|
||||
* ```
|
||||
*
|
||||
* Note that most of the time, only the root widget needs to be created by
|
||||
* hand. Other widgets should be created automatically by the framework (with
|
||||
* the t-widget directive in a template)
|
||||
*/
|
||||
constructor(parent: Component<T, any, any> | T, props?: Props) {
|
||||
super();
|
||||
|
||||
const defaultProps = (<any>this.constructor).defaultProps;
|
||||
if (defaultProps) {
|
||||
props = this._applyDefaultProps(props, defaultProps);
|
||||
}
|
||||
if (QWeb.dev) {
|
||||
this._validateProps(props || {});
|
||||
}
|
||||
// is this a good idea?
|
||||
// Pro: if props is empty, we can create easily a widget
|
||||
// Con: this is not really safe
|
||||
// Pro: but creating widget (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;
|
||||
}
|
||||
this.__owl__ = {
|
||||
id: id,
|
||||
vnode: null,
|
||||
isMounted: false,
|
||||
isDestroyed: false,
|
||||
parent: p,
|
||||
children: {},
|
||||
cmap: {},
|
||||
renderId: 1,
|
||||
renderPromise: null,
|
||||
renderProps: props || null,
|
||||
boundHandlers: {},
|
||||
mountedHandlers: {}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* willStart is an asynchronous hook that can be implemented to perform some
|
||||
* action before the initial rendering of a component.
|
||||
*
|
||||
* It will be called exactly once before the initial rendering. It is useful
|
||||
* in some cases, for example, to load external assets (such as a JS library)
|
||||
* before the widget is rendered.
|
||||
*
|
||||
* Note that a slow willStart method will slow down the rendering of the user
|
||||
* interface. Therefore, some effort should be made to make this method as
|
||||
* fast as possible.
|
||||
*
|
||||
* Note: this method should not be called manually.
|
||||
*/
|
||||
async willStart() {}
|
||||
|
||||
/**
|
||||
* mounted is a hook that is called each time a component is attached to the
|
||||
* DOM. This is a good place to add some listeners, or to interact with the
|
||||
* DOM, if the component needs to perform some measure for example.
|
||||
*
|
||||
* Note: this method should not be called manually.
|
||||
*
|
||||
* @see willUnmount
|
||||
*/
|
||||
mounted() {}
|
||||
|
||||
/**
|
||||
* The willUpdateProps is an asynchronous hook, called just before new props
|
||||
* are set. This is useful if the component needs some asynchronous task
|
||||
* performed, depending on the props (for example, assuming that the props are
|
||||
* some record Id, fetching the record data).
|
||||
*
|
||||
* This hook is not called during the first render (but willStart is called
|
||||
* and performs a similar job).
|
||||
*/
|
||||
async willUpdateProps(nextProps: Props) {}
|
||||
|
||||
/**
|
||||
* The willPatch hook is called just before the DOM patching process starts.
|
||||
* 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
|
||||
* scrollbar
|
||||
*
|
||||
* The return value of willPatch will be given to the patched function.
|
||||
*/
|
||||
willPatch(): any {}
|
||||
|
||||
/**
|
||||
* This hook is called whenever a component did actually update its props,
|
||||
* state or env.
|
||||
*
|
||||
* This method is not called on the initial render. It is useful to interact
|
||||
* with the DOM (for example, through an external library) whenever the
|
||||
* component was updated.
|
||||
*
|
||||
* Updating the widget state in this hook is possible, but not encouraged.
|
||||
* One need to be careful, because updates here will cause rerender, which in
|
||||
* turn will cause other calls to updated. So, we need to be particularly
|
||||
* careful at avoiding endless cycles.
|
||||
*
|
||||
* The snapshot parameter is the result of the call to willPatch.
|
||||
*/
|
||||
patched(snapshot: any) {}
|
||||
|
||||
/**
|
||||
* willUnmount is a hook that is called each time just before a component is
|
||||
* unmounted from the DOM. This is a good place to remove some listeners, for
|
||||
* example.
|
||||
*
|
||||
* Note: this method should not be called manually.
|
||||
*
|
||||
* @see mounted
|
||||
*/
|
||||
willUnmount() {}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
async mount(target: HTMLElement): Promise<void> {
|
||||
const vnode = await this._prepare();
|
||||
if (this.__owl__.isDestroyed) {
|
||||
// widget was destroyed before we get here...
|
||||
return;
|
||||
}
|
||||
this._patch(vnode);
|
||||
target.appendChild(this.el!);
|
||||
|
||||
if (document.body.contains(target)) {
|
||||
this._callMounted();
|
||||
}
|
||||
}
|
||||
|
||||
_callMounted() {
|
||||
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;
|
||||
const handlers = __owl__.mountedHandlers;
|
||||
for (let key in handlers) {
|
||||
handlers[key]();
|
||||
}
|
||||
this.mounted();
|
||||
}
|
||||
|
||||
_callWillUnmount() {
|
||||
this.willUnmount();
|
||||
const __owl__ = this.__owl__;
|
||||
__owl__.isMounted = false;
|
||||
const children = __owl__.children;
|
||||
for (let id in children) {
|
||||
const comp = children[id];
|
||||
if (comp.__owl__.isMounted) {
|
||||
comp._callWillUnmount();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unmount() {
|
||||
if (this.__owl__.isMounted) {
|
||||
this._callWillUnmount();
|
||||
this.el!.remove();
|
||||
}
|
||||
}
|
||||
|
||||
async render(force: boolean = false, patchQueue?: any[]): Promise<void> {
|
||||
const __owl__ = this.__owl__;
|
||||
if (!__owl__.isMounted) {
|
||||
return;
|
||||
}
|
||||
const shouldPatch: boolean = !patchQueue;
|
||||
if (shouldPatch) {
|
||||
patchQueue = [];
|
||||
}
|
||||
const renderVDom = this._render(force, patchQueue);
|
||||
const renderId = __owl__.renderId;
|
||||
await renderVDom;
|
||||
|
||||
if (shouldPatch && __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.
|
||||
const patchLen = patchQueue!.length;
|
||||
for (let i = 0; i < patchLen; i++) {
|
||||
const patch = patchQueue![i];
|
||||
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];
|
||||
patch[0].patched(patch[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
const __owl__ = this.__owl__;
|
||||
if (!__owl__.isDestroyed) {
|
||||
const el = this.el;
|
||||
this._destroy(__owl__.parent);
|
||||
if (el) {
|
||||
el.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_destroy(parent) {
|
||||
const __owl__ = this.__owl__;
|
||||
const isMounted = __owl__.isMounted;
|
||||
if (isMounted) {
|
||||
this.willUnmount();
|
||||
__owl__.isMounted = false;
|
||||
}
|
||||
const children = __owl__.children;
|
||||
for (let key in children) {
|
||||
children[key]._destroy(this);
|
||||
}
|
||||
if (parent) {
|
||||
let id = __owl__.id;
|
||||
delete parent.__owl__.children[id];
|
||||
__owl__.parent = null;
|
||||
}
|
||||
this.clear();
|
||||
__owl__.isDestroyed = true;
|
||||
delete __owl__.vnode;
|
||||
}
|
||||
|
||||
shouldUpdate(nextProps: Props): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is the correct way to update the environment of a widget. Doing
|
||||
* this will cause a full rerender of the widget 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);
|
||||
}
|
||||
}
|
||||
|
||||
set(target: any, key: string | number, value: any) {
|
||||
this.__owl__.observer!.set(target, key, value);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
async _updateProps(
|
||||
nextProps: Props,
|
||||
forceUpdate: boolean = false,
|
||||
patchQueue?: any[]
|
||||
): Promise<void> {
|
||||
const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
|
||||
if (shouldUpdate) {
|
||||
const defaultProps = (<any>this.constructor).defaultProps;
|
||||
if (defaultProps) {
|
||||
nextProps = this._applyDefaultProps(nextProps, defaultProps);
|
||||
}
|
||||
if (QWeb.dev) {
|
||||
this._validateProps(nextProps);
|
||||
}
|
||||
await this.willUpdateProps(nextProps);
|
||||
this.props = nextProps;
|
||||
await this.render(forceUpdate, patchQueue);
|
||||
}
|
||||
}
|
||||
|
||||
_patch(vnode) {
|
||||
const __owl__ = this.__owl__;
|
||||
__owl__.renderPromise = null;
|
||||
const target = __owl__.vnode || document.createElement(vnode.sel!);
|
||||
__owl__.vnode = patch(target, vnode);
|
||||
}
|
||||
_prepare(): Promise<VNode> {
|
||||
const __owl__ = this.__owl__;
|
||||
__owl__.renderProps = this.props;
|
||||
__owl__.renderPromise = this._prepareAndRender();
|
||||
return __owl__.renderPromise;
|
||||
}
|
||||
|
||||
async _prepareAndRender(): Promise<VNode> {
|
||||
await this.willStart();
|
||||
const __owl__ = this.__owl__;
|
||||
if (__owl__.isDestroyed) {
|
||||
return Promise.resolve(h("div"));
|
||||
}
|
||||
const qweb = this.env.qweb;
|
||||
if (!this.template) {
|
||||
let tmap = TEMPLATE_MAP[qweb.id];
|
||||
if (!tmap) {
|
||||
tmap = {};
|
||||
TEMPLATE_MAP[qweb.id] = tmap;
|
||||
}
|
||||
let p = (<any>this).constructor;
|
||||
let name: string = p.name;
|
||||
let template = tmap[name];
|
||||
if (template) {
|
||||
this.template = template;
|
||||
} else {
|
||||
while (
|
||||
(template = p.name) &&
|
||||
!(template in qweb.templates) &&
|
||||
p !== Component
|
||||
) {
|
||||
p = p.__proto__;
|
||||
}
|
||||
if (p === Component) {
|
||||
this.template = "default";
|
||||
} else {
|
||||
tmap[name] = template;
|
||||
this.template = template;
|
||||
}
|
||||
}
|
||||
}
|
||||
__owl__.render = qweb.render.bind(qweb, this.template);
|
||||
this._observeState();
|
||||
return this._render();
|
||||
}
|
||||
async _render(
|
||||
force: boolean = false,
|
||||
patchQueue: any[] = []
|
||||
): Promise<VNode> {
|
||||
const __owl__ = this.__owl__;
|
||||
__owl__.renderId++;
|
||||
const promises: Promise<void>[] = [];
|
||||
const patch: any[] = [this];
|
||||
if (__owl__.isMounted) {
|
||||
patchQueue.push(patch);
|
||||
}
|
||||
if (__owl__.observer) {
|
||||
__owl__.observer.allowMutations = false;
|
||||
}
|
||||
let vnode = __owl__.render!(this, {
|
||||
promises,
|
||||
handlers: __owl__.boundHandlers,
|
||||
mountedHandlers: __owl__.mountedHandlers,
|
||||
forceUpdate: force,
|
||||
patchQueue
|
||||
});
|
||||
patch.push(vnode);
|
||||
if (__owl__.observer) {
|
||||
__owl__.observer.allowMutations = true;
|
||||
}
|
||||
|
||||
// this part is critical for the patching process to be done correctly. The
|
||||
// tricky part is that a child widget can be rerendered on its own, which
|
||||
// will update its own vnode representation without the knowledge of the
|
||||
// parent widget. With this, we make sure that the parent widget will be
|
||||
// able to patch itself properly after
|
||||
vnode.key = __owl__.id;
|
||||
__owl__.renderProps = this.props;
|
||||
__owl__.renderPromise = Promise.all(promises).then(() => vnode);
|
||||
return __owl__.renderPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only called by qweb t-widget directive
|
||||
*/
|
||||
_mount(vnode: VNode, elm: HTMLElement): VNode {
|
||||
const __owl__ = this.__owl__;
|
||||
__owl__.vnode = patch(elm, vnode);
|
||||
if (__owl__.parent!.__owl__.isMounted && !__owl__.isMounted) {
|
||||
this._callMounted();
|
||||
}
|
||||
return __owl__.vnode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only called by qweb t-widget directive (when t-keepalive is set)
|
||||
*/
|
||||
_remount() {
|
||||
const __owl__ = this.__owl__;
|
||||
if (!__owl__.isMounted) {
|
||||
__owl__.isMounted = true;
|
||||
this.mounted();
|
||||
}
|
||||
}
|
||||
|
||||
_observeState() {
|
||||
if (this.state) {
|
||||
const __owl__ = this.__owl__;
|
||||
__owl__.observer = new Observer();
|
||||
__owl__.observer.observe(this.state);
|
||||
__owl__.observer.notifyCB = this.render.bind(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply default props (only top level).
|
||||
*
|
||||
* Note that this method does not modify in place the props, it returns a new
|
||||
* prop object
|
||||
*/
|
||||
_applyDefaultProps(props: Object | undefined, defaultProps: Object): Props {
|
||||
props = props ? Object.create(props) : {};
|
||||
for (let propName in defaultProps) {
|
||||
if (props![propName] === undefined) {
|
||||
props![propName] = defaultProps[propName];
|
||||
}
|
||||
}
|
||||
return <Props>props;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the component props (or next props) against the (static) props
|
||||
* description. This is potentially an expensive operation: it may needs to
|
||||
* visit recursively the props and all the children to check if they are valid.
|
||||
* This is why it is only done in 'dev' mode.
|
||||
*/
|
||||
_validateProps(props: Object) {
|
||||
const propsDef = (<any>this.constructor).props;
|
||||
if (propsDef instanceof Array) {
|
||||
// list of strings (prop names)
|
||||
for (let i = 0, l = propsDef.length; i < l; i++) {
|
||||
if (!(propsDef[i] in props)) {
|
||||
throw new Error(
|
||||
`Missing props '${propsDef[i]}' (widget '${this.constructor.name}')`
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (propsDef) {
|
||||
// propsDef is an object now
|
||||
for (let propName in propsDef) {
|
||||
if (!(propName in props)) {
|
||||
if (propsDef[propName] && !propsDef[propName].optional) {
|
||||
throw new Error(
|
||||
`Missing props '${propName}' (widget '${this.constructor.name}')`
|
||||
);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let isValid = isValidProp(props[propName], propsDef[propName]);
|
||||
if (!isValid) {
|
||||
throw new Error(
|
||||
`Props '${propName}' of invalid type in widget '${
|
||||
this.constructor.name
|
||||
}'`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Prop validation helper
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Check if an invidual prop value matches its (static) prop definition
|
||||
*/
|
||||
function isValidProp(prop, propDef): boolean {
|
||||
if (typeof propDef === "function") {
|
||||
// Check if a value is constructed by some Constructor. Note that there is a
|
||||
// slight abuse of language: we want to consider primitive values as well.
|
||||
//
|
||||
// So, even though 1 is not an instance of Number, we want to consider that
|
||||
// it is valid.
|
||||
if (typeof prop === "object") {
|
||||
return prop instanceof propDef;
|
||||
}
|
||||
return typeof prop === propDef.name.toLowerCase();
|
||||
} else if (propDef instanceof Array) {
|
||||
// If this code is executed, this means that we want to check if a prop
|
||||
// matches at least one of its descriptor.
|
||||
let result = false;
|
||||
for (let i = 0, iLen = propDef.length; i < iLen; i++) {
|
||||
result = result || isValidProp(prop, propDef[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// propsDef is an object
|
||||
let result = isValidProp(prop, propDef.type);
|
||||
if (propDef.type === Array) {
|
||||
for (let i = 0, iLen = prop.length; i < iLen; i++) {
|
||||
result = result && isValidProp(prop[i], propDef.element);
|
||||
}
|
||||
}
|
||||
if (propDef.type === Object) {
|
||||
const shape = propDef.shape;
|
||||
for (let key in shape) {
|
||||
result = result && isValidProp(prop[key], shape[key]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,735 @@
|
||||
import { Observer } from "../core/observer";
|
||||
import { CompiledTemplate, QWeb } from "../qweb/index";
|
||||
import { h, patch, VNode } from "../vdom/index";
|
||||
import "./directive";
|
||||
import "./props_validation";
|
||||
|
||||
/**
|
||||
* Owl Component System
|
||||
*
|
||||
* This file introduces a declarative and composable component system. It
|
||||
* contains:
|
||||
*
|
||||
* - the Env interface (generic type for the environment)
|
||||
* - the Fiber interface (owl metadata attached to a rendering)
|
||||
* - the Internal interface (the owl specific metadata attached to a component)
|
||||
* - the Component class
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Types/helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* An Env (environment) is an object that will be (mostly) shared between all
|
||||
* components of an Owl application. It is the location which should contain
|
||||
* the qweb instance necessary to render all components.
|
||||
*
|
||||
* Note that it is totally fine to extend the environment with application
|
||||
* specific keys/objects/whatever. For example, a key `isMobile` (to declare
|
||||
* if we are in "mobile" mode), or a shared bus could be useful.
|
||||
*/
|
||||
export interface Env {
|
||||
qweb: QWeb;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fibers are small abstractions designed to contain all the internal state
|
||||
* associated to a "rendering work unit", relative to a specific component.
|
||||
*
|
||||
* A rendering will cause the creation of a fiber for each impacted components.
|
||||
*/
|
||||
export interface Fiber<Props> {
|
||||
force: boolean;
|
||||
rootFiber: Fiber<any> | null;
|
||||
isCancelled: boolean;
|
||||
scope: any;
|
||||
vars: any;
|
||||
patchQueue: Fiber<any>[];
|
||||
component: Component<any, any>;
|
||||
vnode: VNode | null;
|
||||
props: Props;
|
||||
promise: Promise<VNode> | null;
|
||||
// handlers?: any;
|
||||
// mountedHandlers?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is mostly an internal detail of implementation. The Meta interface is
|
||||
* useful to typecheck and describe the internal keys used by Owl to manage the
|
||||
* component tree.
|
||||
*/
|
||||
interface Internal<T extends Env, Props> {
|
||||
// each component has a unique id, useful mostly to handle parent/child
|
||||
// relationships
|
||||
readonly id: number;
|
||||
vnode: VNode | null;
|
||||
isMounted: boolean;
|
||||
isDestroyed: boolean;
|
||||
|
||||
// parent and children keys are obviously useful to setup the parent-children
|
||||
// relationship.
|
||||
parent: Component<T, any> | null;
|
||||
children: { [key: number]: Component<T, any> };
|
||||
// children mapping: from templateID to componentID. templateID identifies a
|
||||
// place in a template. The t-component directive needs it to be able to get
|
||||
// the component instance back whenever the template is rerendered.
|
||||
cmap: { [key: number]: number };
|
||||
|
||||
currentFiber: Fiber<Props> | null;
|
||||
|
||||
boundHandlers: { [key: number]: any };
|
||||
observer: Observer | null;
|
||||
render: CompiledTemplate;
|
||||
mountedCB: Function | null;
|
||||
willUnmountCB: Function | null;
|
||||
willPatchCB: Function | null;
|
||||
patchedCB: Function | null;
|
||||
willStartCB: Function | null;
|
||||
willUpdatePropsCB: Function | null;
|
||||
classObj: { [key: string]: boolean } | null;
|
||||
refs: { [key: string]: Component<T, any> | HTMLElement | undefined } | null;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Component
|
||||
//------------------------------------------------------------------------------
|
||||
let nextId = 1;
|
||||
|
||||
export class Component<T extends Env, Props extends {}> {
|
||||
readonly __owl__: Internal<Env, Props>;
|
||||
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;
|
||||
|
||||
/**
|
||||
* The `el` is the root element of the component. Note that it could be null:
|
||||
* this is the case if the component is not mounted yet, or is destroyed.
|
||||
*/
|
||||
get el(): HTMLElement | null {
|
||||
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
|
||||
}
|
||||
|
||||
env: T;
|
||||
props: Props;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Lifecycle
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 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
|
||||
* hand. Other components should be created automatically by the framework (with
|
||||
* the t-component directive in a template)
|
||||
*/
|
||||
constructor(parent: Component<T, any> | T, props?: Props) {
|
||||
const defaultProps = (<any>this.constructor).defaultProps;
|
||||
Component.current = this;
|
||||
if (defaultProps) {
|
||||
props = this.__applyDefaultProps(props, defaultProps);
|
||||
}
|
||||
// is this a good idea?
|
||||
// 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> | null = null;
|
||||
if (parent instanceof Component) {
|
||||
p = parent;
|
||||
this.env = parent.env;
|
||||
parent.__owl__.children[id] = this;
|
||||
} else {
|
||||
this.env = parent;
|
||||
if (QWeb.dev) {
|
||||
// we only validate props for root widgets here. "Regular" widget
|
||||
// props are validated by the t-component directive
|
||||
QWeb.utils.validateProps(this.constructor, this.props);
|
||||
}
|
||||
this.env.qweb.on("update", this, () => {
|
||||
if (this.__owl__.isMounted) {
|
||||
this.render(true);
|
||||
}
|
||||
if (this.__owl__.isDestroyed) {
|
||||
// this is unlikely to happen, but if a root widget is destroyed,
|
||||
// we want to remove our subscription. The usual way to do that
|
||||
// would be to perform some check in the destroy method, but since
|
||||
// it is very performance sensitive, and since this is a rare event,
|
||||
// we simply do it lazily
|
||||
this.env.qweb.off("update", this);
|
||||
}
|
||||
});
|
||||
}
|
||||
const qweb = this.env.qweb;
|
||||
|
||||
this.__owl__ = {
|
||||
id: id,
|
||||
vnode: null,
|
||||
isMounted: false,
|
||||
isDestroyed: false,
|
||||
parent: p,
|
||||
children: {},
|
||||
cmap: {},
|
||||
currentFiber: null,
|
||||
boundHandlers: {},
|
||||
mountedCB: null,
|
||||
willUnmountCB: null,
|
||||
willPatchCB: null,
|
||||
patchedCB: null,
|
||||
willStartCB: null,
|
||||
willUpdatePropsCB: null,
|
||||
observer: null,
|
||||
render: qweb.render.bind(qweb, this.__getTemplate(qweb)),
|
||||
classObj: null,
|
||||
refs: null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* willStart is an asynchronous hook that can be implemented to perform some
|
||||
* action before the initial rendering of a component.
|
||||
*
|
||||
* It will be called exactly once before the initial rendering. It is useful
|
||||
* in some cases, for example, to load external assets (such as a JS library)
|
||||
* before the component is rendered.
|
||||
*
|
||||
* Note that a slow willStart method will slow down the rendering of the user
|
||||
* interface. Therefore, some effort should be made to make this method as
|
||||
* fast as possible.
|
||||
*
|
||||
* Note: this method should not be called manually.
|
||||
*/
|
||||
async willStart() {}
|
||||
|
||||
/**
|
||||
* mounted is a hook that is called each time a component is attached to the
|
||||
* DOM. This is a good place to add some listeners, or to interact with the
|
||||
* DOM, if the component needs to perform some measure for example.
|
||||
*
|
||||
* Note: this method should not be called manually.
|
||||
*
|
||||
* @see willUnmount
|
||||
*/
|
||||
mounted() {}
|
||||
|
||||
/**
|
||||
* The willUpdateProps is an asynchronous hook, called just before new props
|
||||
* are set. This is useful if the component needs some asynchronous task
|
||||
* performed, depending on the props (for example, assuming that the props are
|
||||
* some record Id, fetching the record data).
|
||||
*
|
||||
* This hook is not called during the first render (but willStart is called
|
||||
* and performs a similar job).
|
||||
*/
|
||||
async willUpdateProps(nextProps: Props) {}
|
||||
|
||||
/**
|
||||
* The willPatch hook is called just before the DOM patching process starts.
|
||||
* 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
|
||||
* scrollbar
|
||||
*/
|
||||
willPatch(): any {}
|
||||
|
||||
/**
|
||||
* This hook is called whenever a component did actually update its props,
|
||||
* state or env.
|
||||
*
|
||||
* This method is not called on the initial render. It is useful to interact
|
||||
* with the DOM (for example, through an external library) whenever the
|
||||
* component was updated.
|
||||
*
|
||||
* Updating the component state in this hook is possible, but not encouraged.
|
||||
* One need to be careful, because updates here will cause rerender, which in
|
||||
* turn will cause other calls to updated. So, we need to be particularly
|
||||
* careful at avoiding endless cycles.
|
||||
*/
|
||||
patched() {}
|
||||
|
||||
/**
|
||||
* willUnmount is a hook that is called each time just before a component is
|
||||
* unmounted from the DOM. This is a good place to remove some listeners, for
|
||||
* example.
|
||||
*
|
||||
* Note: this method should not be called manually.
|
||||
*
|
||||
* @see mounted
|
||||
*/
|
||||
willUnmount() {}
|
||||
|
||||
/**
|
||||
* catchError is a method called whenever some error happens in the rendering or
|
||||
* lifecycle hooks of a child.
|
||||
*/
|
||||
catchError(error: Error): void {}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Mount the component to a target element.
|
||||
*
|
||||
* This should only be done if the component was created manually. Components
|
||||
* created declaratively in templates are managed by the Owl system.
|
||||
*
|
||||
* Note that a component can be mounted an unmounted several times
|
||||
*/
|
||||
async mount(target: HTMLElement, renderBeforeRemount: boolean = false): Promise<void> {
|
||||
const __owl__ = this.__owl__;
|
||||
if (__owl__.isMounted) {
|
||||
return;
|
||||
}
|
||||
const fiber = this.__createFiber(false, undefined, undefined, undefined);
|
||||
if (!__owl__.vnode) {
|
||||
fiber.promise = this.__prepareAndRender(fiber);
|
||||
const vnode = await fiber.promise;
|
||||
if (__owl__.isDestroyed) {
|
||||
// component was destroyed before we get here...
|
||||
return;
|
||||
}
|
||||
this.__patch(vnode);
|
||||
} else if (renderBeforeRemount) {
|
||||
fiber.patchQueue.push(fiber);
|
||||
fiber.promise = this.__render(fiber);
|
||||
await fiber.promise;
|
||||
this.__applyPatchQueue(fiber);
|
||||
}
|
||||
target.appendChild(this.el!);
|
||||
|
||||
if (document.body.contains(target)) {
|
||||
this.__callMounted();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The unmount method is the opposite of the mount method. It is useful
|
||||
* to call willUnmount calls and remove the component from the DOM.
|
||||
*/
|
||||
unmount() {
|
||||
if (this.__owl__.isMounted) {
|
||||
this.__callWillUnmount();
|
||||
this.el!.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The render method is the main entry point to render a component (once it
|
||||
* is ready. This method is not initially called when the component is
|
||||
* rendered the first time).
|
||||
*
|
||||
* This method will cause all its sub components to potentially rerender
|
||||
* themselves. Note that `render` is not called if a component is updated via
|
||||
* its props.
|
||||
*/
|
||||
async render(force: boolean = false): Promise<void> {
|
||||
const __owl__ = this.__owl__;
|
||||
if (!__owl__.isMounted) {
|
||||
return;
|
||||
}
|
||||
const fiber = this.__createFiber(force, undefined, undefined, undefined);
|
||||
fiber.patchQueue.push(fiber);
|
||||
fiber.promise = this.__render(fiber);
|
||||
await fiber.promise;
|
||||
|
||||
if (__owl__.isMounted && fiber === __owl__.currentFiber) {
|
||||
// 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(fiber);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the component. This operation is quite complex:
|
||||
* - it recursively destroy all children
|
||||
* - call the willUnmount hooks if necessary
|
||||
* - remove the dom node from the dom
|
||||
*
|
||||
* This should only be called manually if you created the component. Most
|
||||
* components will be automatically destroyed.
|
||||
*/
|
||||
destroy() {
|
||||
const __owl__ = this.__owl__;
|
||||
if (!__owl__.isDestroyed) {
|
||||
const el = this.el;
|
||||
this.__destroy(__owl__.parent);
|
||||
if (el) {
|
||||
el.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by the component system whenever its props are
|
||||
* updated. If it returns true, then the component will be rendered.
|
||||
* Otherwise, it will skip the rendering (also, its props will not be updated)
|
||||
*/
|
||||
shouldUpdate(nextProps: Props): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* up to the parent DOM nodes. Thus, it must be called between mounted() and
|
||||
* willUnmount().
|
||||
*/
|
||||
trigger(eventType: string, payload?: any) {
|
||||
if (this.el) {
|
||||
const ev = new CustomEvent(eventType, {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
detail: payload
|
||||
});
|
||||
this.el.dispatchEvent(ev);
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* This method is a helper to create a fiber element.
|
||||
*/
|
||||
__createFiber(force, scope, vars, parent?: Fiber<any>): Fiber<Props> {
|
||||
const fiber: Fiber<Props> = {
|
||||
force,
|
||||
scope,
|
||||
vars,
|
||||
rootFiber: null,
|
||||
isCancelled: false,
|
||||
component: this,
|
||||
vnode: null,
|
||||
patchQueue: parent ? parent.patchQueue : [],
|
||||
props: this.props,
|
||||
promise: null
|
||||
};
|
||||
fiber.rootFiber = parent ? parent.rootFiber : fiber;
|
||||
this.__owl__.currentFiber = fiber;
|
||||
return fiber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Private helper to perform a full destroy, from the point of view of an Owl
|
||||
* component. It does not remove the el (this is done only once on the top
|
||||
* level destroyed component, for performance reasons).
|
||||
*
|
||||
* The job of this method is mostly to call willUnmount hooks, and to perform
|
||||
* all necessary internal cleanup.
|
||||
*
|
||||
* Note that it does not call the __callWillUnmount method to avoid visiting
|
||||
* all children many times.
|
||||
*/
|
||||
__destroy(parent: Component<any, any> | null) {
|
||||
const __owl__ = this.__owl__;
|
||||
const isMounted = __owl__.isMounted;
|
||||
if (isMounted) {
|
||||
if (__owl__.willUnmountCB) {
|
||||
__owl__.willUnmountCB();
|
||||
}
|
||||
this.willUnmount();
|
||||
__owl__.isMounted = false;
|
||||
}
|
||||
const children = __owl__.children;
|
||||
for (let key in children) {
|
||||
children[key].__destroy(this);
|
||||
}
|
||||
if (parent) {
|
||||
let id = __owl__.id;
|
||||
delete parent.__owl__.children[id];
|
||||
__owl__.parent = null;
|
||||
}
|
||||
__owl__.isDestroyed = true;
|
||||
delete __owl__.vnode;
|
||||
}
|
||||
|
||||
__callMounted() {
|
||||
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;
|
||||
try {
|
||||
this.mounted();
|
||||
if (__owl__.mountedCB) {
|
||||
__owl__.mountedCB();
|
||||
}
|
||||
} catch (e) {
|
||||
errorHandler(e, this);
|
||||
}
|
||||
}
|
||||
|
||||
__callWillUnmount() {
|
||||
const __owl__ = this.__owl__;
|
||||
if (__owl__.willUnmountCB) {
|
||||
__owl__.willUnmountCB();
|
||||
}
|
||||
this.willUnmount();
|
||||
__owl__.isMounted = false;
|
||||
const children = __owl__.children;
|
||||
for (let id in children) {
|
||||
const comp = children[id];
|
||||
if (comp.__owl__.isMounted) {
|
||||
comp.__callWillUnmount();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The __updateProps method is called by the t-component directive whenever
|
||||
* it updates a component (so, when the parent template is rerendered).
|
||||
*/
|
||||
async __updateProps(
|
||||
nextProps: Props,
|
||||
parentFiber: Fiber<any>,
|
||||
scope?: any,
|
||||
vars?: any
|
||||
): Promise<void> {
|
||||
const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps);
|
||||
if (shouldUpdate) {
|
||||
const defaultProps = (<any>this.constructor).defaultProps;
|
||||
if (defaultProps) {
|
||||
nextProps = this.__applyDefaultProps(nextProps, defaultProps);
|
||||
}
|
||||
await Promise.all([
|
||||
this.willUpdateProps(nextProps),
|
||||
this.__owl__.willUpdatePropsCB && this.__owl__.willUpdatePropsCB(nextProps)
|
||||
]);
|
||||
this.props = nextProps;
|
||||
const fiber = this.__createFiber(parentFiber.force, scope, vars, parentFiber);
|
||||
fiber.patchQueue.push(fiber);
|
||||
|
||||
await this.__render(fiber);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main patching method. We call the virtual dom patch method here to convert
|
||||
* a virtual dom vnode into some actual dom.
|
||||
*/
|
||||
__patch(vnode) {
|
||||
const __owl__ = this.__owl__;
|
||||
const target = __owl__.vnode || document.createElement(vnode.sel!);
|
||||
__owl__.vnode = patch(target, vnode);
|
||||
}
|
||||
|
||||
/**
|
||||
* The __prepare method is only called by the t-component directive, when a
|
||||
* subcomponent is created. It gets its scope and vars, if any, from the
|
||||
* parent template.
|
||||
*/
|
||||
__prepare(parentFiber: Fiber<any>, scope: any, vars: any): Promise<VNode> {
|
||||
const fiber = this.__createFiber(parentFiber.force, scope, vars, parentFiber);
|
||||
fiber.promise = this.__prepareAndRender(fiber);
|
||||
return fiber.promise;
|
||||
}
|
||||
|
||||
__getTemplate(qweb: QWeb): string {
|
||||
let p = (<any>this).constructor;
|
||||
if (!p.hasOwnProperty("_template")) {
|
||||
if (p.template) {
|
||||
p._template = p.template;
|
||||
} else {
|
||||
// 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
|
||||
// one of its subclass).
|
||||
|
||||
let template: string;
|
||||
while ((template = p.name) && !(template in qweb.templates) && p !== Component) {
|
||||
p = p.__proto__;
|
||||
}
|
||||
if (p === Component) {
|
||||
throw new Error(`Could not find template for component "${this.constructor.name}"`);
|
||||
} else {
|
||||
p._template = template;
|
||||
}
|
||||
}
|
||||
}
|
||||
return p._template;
|
||||
}
|
||||
async __prepareAndRender(fiber: Fiber<Props>): Promise<VNode> {
|
||||
try {
|
||||
await Promise.all([this.willStart(), this.__owl__.willStartCB && this.__owl__.willStartCB()]);
|
||||
} catch (e) {
|
||||
errorHandler(e, this);
|
||||
return Promise.resolve(h("div"));
|
||||
}
|
||||
const __owl__ = this.__owl__;
|
||||
if (__owl__.isDestroyed) {
|
||||
return Promise.resolve(h("div"));
|
||||
}
|
||||
return this.__render(fiber);
|
||||
}
|
||||
|
||||
__render(fiber: Fiber<Props>): Promise<VNode> {
|
||||
const __owl__ = this.__owl__;
|
||||
const promises: Promise<void>[] = [];
|
||||
if (__owl__.observer) {
|
||||
__owl__.observer.allowMutations = false;
|
||||
}
|
||||
let vnode;
|
||||
try {
|
||||
vnode = __owl__.render!(this, {
|
||||
promises,
|
||||
handlers: __owl__.boundHandlers,
|
||||
fiber: fiber
|
||||
});
|
||||
} catch (e) {
|
||||
vnode = __owl__.vnode || h("div");
|
||||
errorHandler(e, this);
|
||||
}
|
||||
fiber.vnode = vnode;
|
||||
if (__owl__.observer) {
|
||||
__owl__.observer.allowMutations = true;
|
||||
}
|
||||
|
||||
// this part is critical for the patching process to be done correctly. The
|
||||
// tricky part is that a child component can be rerendered on its own, which
|
||||
// will update its own vnode representation without the knowledge of the
|
||||
// parent component. With this, we make sure that the parent component will be
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only called by qweb t-component directive (when t-keepalive is set)
|
||||
*/
|
||||
__remount() {
|
||||
const __owl__ = this.__owl__;
|
||||
if (!__owl__.isMounted) {
|
||||
__owl__.isMounted = true;
|
||||
this.mounted();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply default props (only top level).
|
||||
*
|
||||
* Note that this method does not modify in place the props, it returns a new
|
||||
* prop object
|
||||
*/
|
||||
__applyDefaultProps(props: Object | undefined, defaultProps: Object): Props {
|
||||
props = props ? Object.assign({}, props) : {};
|
||||
for (let propName in defaultProps) {
|
||||
if (props![propName] === undefined) {
|
||||
props![propName] = defaultProps[propName];
|
||||
}
|
||||
}
|
||||
return <Props>props;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the given patch queue from a fiber.
|
||||
* 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 reverse order
|
||||
*/
|
||||
__applyPatchQueue(fiber: Fiber<Props>) {
|
||||
const patchQueue = fiber.patchQueue;
|
||||
let component: Component<any, any> = this;
|
||||
try {
|
||||
const patchLen = patchQueue.length;
|
||||
for (let i = 0; i < patchLen; i++) {
|
||||
component = patchQueue[i].component;
|
||||
if (component.__owl__.willPatchCB) {
|
||||
component.__owl__.willPatchCB();
|
||||
}
|
||||
component.willPatch();
|
||||
}
|
||||
for (let i = 0; i < patchLen; i++) {
|
||||
const fiber = patchQueue[i];
|
||||
component = fiber.component;
|
||||
component.__patch(fiber.vnode);
|
||||
}
|
||||
for (let i = patchLen - 1; i >= 0; i--) {
|
||||
component = patchQueue[i].component;
|
||||
component.patched();
|
||||
if (component.__owl__.patchedCB) {
|
||||
component.__owl__.patchedCB();
|
||||
}
|
||||
}
|
||||
} 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>) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,522 @@
|
||||
import { QWeb } from "../qweb/index";
|
||||
import { INTERP_REGEXP } from "../qweb/compilation_context";
|
||||
import { MODS_CODE } from "../qweb/extensions";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-component
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const T_COMPONENT_MODS_CODE = Object.assign({}, MODS_CODE, {
|
||||
self: "if (e.target !== vn.elm) {return}"
|
||||
});
|
||||
|
||||
QWeb.utils.defineProxy = function defineProxy(target, source) {
|
||||
for (let k in source) {
|
||||
Object.defineProperty(target, k, {
|
||||
get() {
|
||||
return source[k];
|
||||
},
|
||||
set(val) {
|
||||
source[k] = val;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The t-component directive is certainly a complicated and hard to maintain piece
|
||||
* of code. To help you, fellow developer, if you have to maintain it, I offer
|
||||
* you this advice: Good luck...
|
||||
*
|
||||
* Since it is not 'direct' code, but rather code that generates other code, it
|
||||
* is not easy to understand. To help you, here is a detailed and commented
|
||||
* explanation of the code generated by the t-component directive for the following
|
||||
* situation:
|
||||
* ```xml
|
||||
* <Child
|
||||
* t-key="'somestring'"
|
||||
* flag="state.flag"
|
||||
* t-transition="fade"/>
|
||||
* ```
|
||||
*
|
||||
* ```js
|
||||
* // we assign utils on top of the function because it will be useful for
|
||||
* // each components
|
||||
* let utils = this.utils;
|
||||
*
|
||||
* // this is the virtual node representing the parent div
|
||||
* let c1 = [], p1 = { key: 1 };
|
||||
* var vn1 = h("div", p1, c1);
|
||||
*
|
||||
* // t-component directive: we start by evaluating the expression given by t-key:
|
||||
* let key5 = "somestring";
|
||||
*
|
||||
* // def3 is the promise that will contain later either the new component
|
||||
* // creation, or the props update...
|
||||
* let def3;
|
||||
*
|
||||
* // this is kind of tricky: we need here to find if the component was already
|
||||
* // created by a previous rendering. This is done by checking the internal
|
||||
* // `cmap` (children map) of the parent component: it maps keys to component ids,
|
||||
* // and, then, if there is an id, we look into the children list to get the
|
||||
* // instance
|
||||
* let w4 =
|
||||
* key5 in context.__owl__.cmap
|
||||
* ? context.__owl__.children[context.__owl__.cmap[key5]]
|
||||
* : false;
|
||||
*
|
||||
* // We keep the index of the position of the component in the closure. We push
|
||||
* // null to reserve the slot, and will replace it later by the component vnode,
|
||||
* // when it will be ready (do not forget that preparing/rendering a component is
|
||||
* // asynchronous)
|
||||
* let _2_index = c1.length;
|
||||
* c1.push(null);
|
||||
*
|
||||
* // we evaluate here the props given to the component. It is done here to be
|
||||
* // able to easily reference it later, and also, it might be an expensive
|
||||
* // computation, so it is certainly better to do it only once
|
||||
* let props4 = { flag: context["state"].flag };
|
||||
*
|
||||
* // If we have a component, currently rendering, but not ready yet, we do not want
|
||||
* // to wait for it to be ready if we can avoid it
|
||||
* if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
||||
* // we check if the props are the same. In that case, we can simply reuse
|
||||
* // the previous rendering and skip all useless work
|
||||
* if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
||||
* def3 = w4.__owl__.renderPromise;
|
||||
* } else {
|
||||
* // if the props are not the same, we destroy the component and starts anew.
|
||||
* // this will be faster than waiting for its rendering, then updating it
|
||||
* w4.destroy();
|
||||
* w4 = false;
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* if (!w4) {
|
||||
* // in this situation, we need to create a new component. First step is
|
||||
* // to get a reference to the class, then create an instance with
|
||||
* // current context as parent, and the props.
|
||||
* let W4 = context.component && context.components[componentKey4] || QWeb.component[componentKey4];
|
||||
|
||||
* if (!W4) {
|
||||
* throw new Error("Cannot find the definition of component 'child'");
|
||||
* }
|
||||
* w4 = new W4(owner, props4);
|
||||
*
|
||||
* // Whenever we rerender the parent component, we need to be sure that we
|
||||
* // are able to find the component instance. To do that, we register it to
|
||||
* // the parent cmap (children map). Note that the 'template' key is
|
||||
* // used here, since this is what identify the component from the template
|
||||
* // perspective.
|
||||
* context.__owl__.cmap[key5] = w4.__owl__.id;
|
||||
*
|
||||
* // __prepare is called, to basically call willStart, then render the
|
||||
* // component
|
||||
* def3 = w4.__prepare();
|
||||
*
|
||||
* def3 = def3.then(vnode => {
|
||||
* // we create here a virtual node for the parent (NOT the component). This
|
||||
* // means that the vdom of the parent will be stopped here, and from
|
||||
* // the parent's perspective, it simply is a vnode with no children.
|
||||
* // However, it shares the same dom element with the component root
|
||||
* // vnode.
|
||||
* let pvnode = h(vnode.sel, { key: key5 });
|
||||
*
|
||||
* // we add hooks to the parent vnode so we can interact with the new
|
||||
* // component at the proper time
|
||||
* pvnode.data.hook = {
|
||||
* insert(vn) {
|
||||
* // the __mount method will patch the component vdom into the elm vn.elm,
|
||||
* // then call the mounted hooks. However, suprisingly, the snabbdom
|
||||
* // patch method actually replace the elm by a new elm, so we need
|
||||
* // to synchronise the pvnode elm with the resulting elm
|
||||
* let nvn = w4.__mount(vnode, vn.elm);
|
||||
* pvnode.elm = nvn.elm;
|
||||
* // what follows is only present if there are animations on the component
|
||||
* utils.transitionInsert(vn, "fade");
|
||||
* },
|
||||
* remove() {
|
||||
* // override with empty function to prevent from removing the node
|
||||
* // directly. It will be removed when destroy is called anyway, which
|
||||
* // delays the removal if there are animations.
|
||||
* },
|
||||
* destroy() {
|
||||
* // if there are animations, we delay the call to destroy on the
|
||||
* // component, if not, we call it directly.
|
||||
* let finalize = () => {
|
||||
* w4.destroy();
|
||||
* };
|
||||
* utils.transitionRemove(vn, "fade", finalize);
|
||||
* }
|
||||
* };
|
||||
* // the pvnode is inserted at the correct position in the div's children
|
||||
* c1[_2_index] = pvnode;
|
||||
*
|
||||
* // we keep here a reference to the parent vnode (representing the
|
||||
* // component, so we can reuse it later whenever we update the component
|
||||
* w4.__owl__.pvnode = pvnode;
|
||||
* });
|
||||
* } else {
|
||||
* // this is the 'update' path of the directive.
|
||||
* // the call to __updateProps is the actual component update
|
||||
* // Note that we only update the props if we cannot reuse the previous
|
||||
* // rendering work (in the case it was rendered with the same props)
|
||||
* def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
||||
* def3 = def3.then(() => {
|
||||
* // if component was destroyed in the meantime, we do nothing (so, this
|
||||
* // means that the parent's element children list will have a null in
|
||||
* // the component's position, which will cause the pvnode to be removed
|
||||
* // when it is patched.
|
||||
* if (w4.__owl__.isDestroyed) {
|
||||
* return;
|
||||
* }
|
||||
* // like above, we register the pvnode to the children list, so it
|
||||
* // will not be patched out of the dom.
|
||||
* let pvnode = w4.__owl__.pvnode;
|
||||
* c1[_2_index] = pvnode;
|
||||
* });
|
||||
* }
|
||||
*
|
||||
* // we register the deferred here so the parent can coordinate its patch operation
|
||||
* // with all the children.
|
||||
* extra.promises.push(def3);
|
||||
* return vn1;
|
||||
* ```
|
||||
*/
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "component",
|
||||
extraNames: ["props", "keepalive", "asyncroot"],
|
||||
priority: 100,
|
||||
atNodeEncounter({ ctx, value, node, qweb }): boolean {
|
||||
ctx.addLine("//COMPONENT");
|
||||
ctx.rootContext.shouldDefineOwner = true;
|
||||
ctx.rootContext.shouldDefineQWeb = true;
|
||||
ctx.rootContext.shouldDefineParent = true;
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
let keepAlive = node.getAttribute("t-keepalive") ? true : false;
|
||||
let hasDynamicProps = node.getAttribute("t-props") ? true : false;
|
||||
let async = node.getAttribute("t-asyncroot") ? true : false;
|
||||
|
||||
// t-on- events and t-transition
|
||||
const events: [string, string[], string, string][] = [];
|
||||
let transition: string = "";
|
||||
const attributes = (<Element>node).attributes;
|
||||
const props: { [key: string]: string } = {};
|
||||
for (let i = 0; i < attributes.length; i++) {
|
||||
const name = attributes[i].name;
|
||||
const value = attributes[i].textContent!;
|
||||
if (name.startsWith("t-on-")) {
|
||||
const [eventName, ...mods] = name.slice(5).split(".");
|
||||
let extraArgs;
|
||||
let handlerName = value.replace(/\(.*\)/, function(args) {
|
||||
extraArgs = args.slice(1, -1);
|
||||
return "";
|
||||
});
|
||||
events.push([eventName, mods, handlerName, extraArgs]);
|
||||
} else if (name === "t-transition") {
|
||||
transition = value;
|
||||
} else if (!name.startsWith("t-")) {
|
||||
if (name !== "class" && name !== "style") {
|
||||
// this is a prop!
|
||||
props[name] = ctx.formatExpression(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let key = node.getAttribute("t-key");
|
||||
if (key) {
|
||||
key = ctx.formatExpression(key);
|
||||
}
|
||||
|
||||
// computing the props string representing the props object
|
||||
let propStr = Object.keys(props)
|
||||
.map(k => k + ":" + props[k])
|
||||
.join(",");
|
||||
let dummyID = ctx.generateID();
|
||||
let defID = 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}`;
|
||||
}
|
||||
|
||||
let ref = node.getAttribute("t-ref");
|
||||
let refExpr = "";
|
||||
let refKey: string = "";
|
||||
if (ref) {
|
||||
ctx.rootContext.shouldDefineRefs = true;
|
||||
refKey = `ref${ctx.generateID()}`;
|
||||
ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`);
|
||||
refExpr = `context.__owl__.refs[${refKey}] = w${componentID};`;
|
||||
}
|
||||
let transitionsInsertCode = "";
|
||||
if (transition) {
|
||||
transitionsInsertCode = `utils.transitionInsert(vn, '${transition}');`;
|
||||
}
|
||||
let finalizeComponentCode = `w${componentID}.${keepAlive ? "unmount" : "destroy"}();`;
|
||||
if (ref && !keepAlive) {
|
||||
finalizeComponentCode += `delete context.__owl__.refs[${refKey}];`;
|
||||
}
|
||||
if (transition) {
|
||||
finalizeComponentCode = `let finalize = () => {
|
||||
${finalizeComponentCode}
|
||||
};
|
||||
utils.transitionRemove(vn, '${transition}', finalize);`;
|
||||
}
|
||||
|
||||
let createHook = "";
|
||||
let classAttr = node.getAttribute("class");
|
||||
let tattClass = node.getAttribute("t-att-class");
|
||||
let styleAttr = node.getAttribute("style");
|
||||
let tattStyle = node.getAttribute("t-att-style");
|
||||
if (tattStyle) {
|
||||
const attVar = `_${ctx.generateID()}`;
|
||||
ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattStyle)};`);
|
||||
tattStyle = attVar;
|
||||
}
|
||||
let classObj = "";
|
||||
if (classAttr || tattClass || styleAttr || tattStyle || events.length) {
|
||||
if (classAttr) {
|
||||
let classDef = classAttr
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.map(a => `'${a}':true`)
|
||||
.join(",");
|
||||
classObj = `_${ctx.generateID()}`;
|
||||
ctx.addLine(`let ${classObj} = {${classDef}};`);
|
||||
}
|
||||
if (tattClass) {
|
||||
let tattExpr = ctx.formatExpression(tattClass);
|
||||
if (tattExpr[0] !== "{" || tattExpr[tattExpr.length - 1] !== "}") {
|
||||
tattExpr = `utils.toObj(${tattExpr})`;
|
||||
}
|
||||
if (classAttr) {
|
||||
ctx.addLine(`Object.assign(${classObj}, ${tattExpr})`);
|
||||
} else {
|
||||
classObj = `_${ctx.generateID()}`;
|
||||
ctx.addLine(`let ${classObj} = ${tattExpr};`);
|
||||
}
|
||||
}
|
||||
let eventsCode = events
|
||||
.map(function([eventName, mods, handlerName, extraArgs]) {
|
||||
let params = "owner";
|
||||
if (extraArgs) {
|
||||
if (ctx.inLoop) {
|
||||
let argId = ctx.generateID();
|
||||
// we need to evaluate the arguments now, because the handler will
|
||||
// be set asynchronously later when the widget is ready, and the
|
||||
// context might be different.
|
||||
ctx.addLine(`let arg${argId} = ${ctx.formatExpression(extraArgs)};`);
|
||||
params = `owner, arg${argId}`;
|
||||
} else {
|
||||
params = `owner, ${ctx.formatExpression(extraArgs)}`;
|
||||
}
|
||||
}
|
||||
let handler;
|
||||
if (mods.length > 0) {
|
||||
handler = `function (e) {`;
|
||||
handler += mods
|
||||
.map(function(mod) {
|
||||
return T_COMPONENT_MODS_CODE[mod];
|
||||
})
|
||||
.join("");
|
||||
handler += `owner['${handlerName}'].call(${params}, e);}`;
|
||||
} else {
|
||||
handler = `owner['${handlerName}'].bind(${params})`;
|
||||
}
|
||||
return `vn.elm.addEventListener('${eventName}', ${handler});`;
|
||||
})
|
||||
.join("");
|
||||
const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false);
|
||||
const styleCode = styleExpr ? `vn.elm.style = ${styleExpr};` : "";
|
||||
createHook = `vnode.data.hook = {create(_, vn){${styleCode}${eventsCode}}};`;
|
||||
}
|
||||
|
||||
ctx.addLine(
|
||||
`let w${componentID} = ${templateID} in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[${templateID}]] : false;`
|
||||
);
|
||||
if (ctx.parentNode) {
|
||||
ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`);
|
||||
}
|
||||
let shouldProxy = false;
|
||||
if (async || keepAlive) {
|
||||
ctx.addLine(
|
||||
`const fiber${componentID} = Object.assign(Object.create(extra.fiber), {patchQueue: []});`
|
||||
);
|
||||
}
|
||||
if (async) {
|
||||
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();
|
||||
ctx.rootContext.rootNode = id;
|
||||
shouldProxy = true;
|
||||
ctx.rootContext.shouldDefineResult = true;
|
||||
ctx.addLine(`let vn${id} = {};`);
|
||||
ctx.addLine(`result = vn${id};`);
|
||||
}
|
||||
}
|
||||
if (hasDynamicProps) {
|
||||
const dynamicProp = ctx.formatExpression(node.getAttribute("t-props")!);
|
||||
ctx.addLine(`let props${componentID} = Object.assign({${propStr}}, ${dynamicProp});`);
|
||||
} else {
|
||||
ctx.addLine(`let props${componentID} = {${propStr}};`);
|
||||
}
|
||||
ctx.addIf(
|
||||
`w${componentID} && w${componentID}.__owl__.currentFiber && !w${componentID}.__owl__.vnode`
|
||||
);
|
||||
ctx.addIf(
|
||||
`utils.shallowEqual(props${componentID}, w${componentID}.__owl__.currentFiber.props)`
|
||||
);
|
||||
ctx.addLine(`def${defID} = w${componentID}.__owl__.currentFiber.promise;`);
|
||||
ctx.addElse();
|
||||
ctx.addLine(`w${componentID}.destroy();`);
|
||||
ctx.addLine(`w${componentID} = false;`);
|
||||
ctx.closeIf();
|
||||
ctx.closeIf();
|
||||
|
||||
ctx.addIf(`!w${componentID}`);
|
||||
// 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} + '"')}`
|
||||
);
|
||||
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
|
||||
const varDefs: string[] = [];
|
||||
const hasSlots = node.childNodes.length;
|
||||
if (hasSlots) {
|
||||
ctx.rootContext.shouldTrackScope = true;
|
||||
for (let v of Object.values(ctx.variables)) {
|
||||
if (v["id"]) {
|
||||
varDefs.push(v["id"]);
|
||||
}
|
||||
}
|
||||
|
||||
const clone = <Element>node.cloneNode(true);
|
||||
const slotNodes = clone.querySelectorAll("[t-set]");
|
||||
const slotId = QWeb.nextSlotId++;
|
||||
ctx.addLine(`w${componentID}.__owl__.slotId = ${slotId};`);
|
||||
if (slotNodes.length) {
|
||||
for (let i = 0, length = slotNodes.length; i < length; i++) {
|
||||
const slotNode = slotNodes[i];
|
||||
slotNode.parentElement!.removeChild(slotNode);
|
||||
const key = slotNode.getAttribute("t-set")!;
|
||||
slotNode.removeAttribute("t-set");
|
||||
const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx);
|
||||
QWeb.slots[`${slotId}_${key}`] = slotFn;
|
||||
}
|
||||
}
|
||||
if (clone.childNodes.length) {
|
||||
const t = clone.ownerDocument!.createElement("t");
|
||||
for (let child of Object.values(clone.childNodes)) {
|
||||
t.appendChild(child);
|
||||
}
|
||||
const slotFn = qweb._compile(`slot_default_template`, t, ctx);
|
||||
QWeb.slots[`${slotId}_default`] = slotFn;
|
||||
}
|
||||
}
|
||||
|
||||
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.addLine(`def${defID} = w${componentID}.__prepare(extra.fiber, ${scopeVars});`);
|
||||
// hack: specify empty remove hook to prevent the node from being removed from the DOM
|
||||
let registerCode = `c${ctx.parentNode}[_${dummyID}_index]=pvnode;`;
|
||||
if (shouldProxy) {
|
||||
registerCode = `utils.defineProxy(vn${ctx.rootNode}, pvnode);`;
|
||||
}
|
||||
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;});`
|
||||
);
|
||||
|
||||
ctx.addElse();
|
||||
// need to update component
|
||||
let patchQueueCode = async || keepAlive ? `fiber${componentID}` : "extra.fiber";
|
||||
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 ? extra.fiber : fiber${componentID}`;
|
||||
}
|
||||
if (QWeb.dev) {
|
||||
ctx.addLine(`utils.validateProps(w${componentID}.constructor, props${componentID})`);
|
||||
}
|
||||
ctx.addLine(
|
||||
`def${defID} = def${defID} || w${componentID}.__updateProps(props${componentID}, ${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();
|
||||
|
||||
if (classObj) {
|
||||
ctx.addLine(`w${componentID}.__owl__.classObj=${classObj};`);
|
||||
}
|
||||
|
||||
if (async) {
|
||||
ctx.addLine(
|
||||
`def${defID}.then(w${componentID}.__applyPatchQueue.bind(w${componentID}, fiber${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;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
import { QWeb } from "../qweb/index";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Prop validation helper
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Validate the component props (or next props) against the (static) props
|
||||
* description. This is potentially an expensive operation: it may needs to
|
||||
* visit recursively the props and all the children to check if they are valid.
|
||||
* This is why it is only done in 'dev' mode.
|
||||
*/
|
||||
|
||||
QWeb.utils.validateProps = function(Widget, props: Object) {
|
||||
const propsDef = (<any>Widget).props;
|
||||
if (propsDef instanceof Array) {
|
||||
// list of strings (prop names)
|
||||
for (let i = 0, l = propsDef.length; i < l; i++) {
|
||||
const propName = propsDef[i];
|
||||
if (propName[propName.length - 1] === "?") {
|
||||
// optional prop
|
||||
break;
|
||||
}
|
||||
if (!props[propName]) {
|
||||
throw new Error(`Missing props '${propsDef[i]}' (component '${Widget.name}')`);
|
||||
}
|
||||
}
|
||||
for (let key in props) {
|
||||
if (!propsDef.includes(key) && !propsDef.includes(key + "?")) {
|
||||
throw new Error(`Unknown prop '${key}' given to component '${Widget.name}'`);
|
||||
}
|
||||
}
|
||||
} else if (propsDef) {
|
||||
// propsDef is an object now
|
||||
for (let propName in propsDef) {
|
||||
if (props[propName] === undefined) {
|
||||
if (propsDef[propName] && !propsDef[propName].optional) {
|
||||
throw new Error(`Missing props '${propName}' (component '${Widget.name}')`);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let isValid = isValidProp(props[propName], propsDef[propName]);
|
||||
if (!isValid) {
|
||||
throw new Error(`Props '${propName}' of invalid type in component '${Widget.name}'`);
|
||||
}
|
||||
}
|
||||
for (let propName in props) {
|
||||
if (!(propName in propsDef)) {
|
||||
throw new Error(`Unknown prop '${propName}' given to component '${Widget.name}'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if an invidual prop value matches its (static) prop definition
|
||||
*/
|
||||
function isValidProp(prop, propDef): boolean {
|
||||
if (propDef === true) {
|
||||
return true;
|
||||
}
|
||||
if (typeof propDef === "function") {
|
||||
// Check if a value is constructed by some Constructor. Note that there is a
|
||||
// slight abuse of language: we want to consider primitive values as well.
|
||||
//
|
||||
// So, even though 1 is not an instance of Number, we want to consider that
|
||||
// it is valid.
|
||||
if (typeof prop === "object") {
|
||||
return prop instanceof propDef;
|
||||
}
|
||||
return typeof prop === propDef.name.toLowerCase();
|
||||
} else if (propDef instanceof Array) {
|
||||
// If this code is executed, this means that we want to check if a prop
|
||||
// matches at least one of its descriptor.
|
||||
let result = false;
|
||||
for (let i = 0, iLen = propDef.length; i < iLen; i++) {
|
||||
result = result || isValidProp(prop, propDef[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// propsDef is an object
|
||||
let result = isValidProp(prop, propDef.type);
|
||||
if (propDef.type === Array) {
|
||||
for (let i = 0, iLen = prop.length; i < iLen; i++) {
|
||||
result = result && isValidProp(prop[i], propDef.element);
|
||||
}
|
||||
}
|
||||
if (propDef.type === Object) {
|
||||
const shape = propDef.shape;
|
||||
for (let key in shape) {
|
||||
result = result && isValidProp(prop[key], shape[key]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Component } from "./component/component";
|
||||
import { Observer } from "./core/observer";
|
||||
import { EventBus } from "./core/event_bus";
|
||||
import { onWillUnmount } from "./hooks";
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export class Context extends EventBus {
|
||||
state: any;
|
||||
observer: Observer;
|
||||
id: 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 = this.__notifyComponents.bind(this);
|
||||
this.state = this.observer.observe(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* A potential cheap way to improve this situation is to keep track of the
|
||||
* depth of a component in the component tree. A root component has a depth of
|
||||
* 1, then its children of 2 and so on... Then, we can update all components
|
||||
* with the same depth in parallel.
|
||||
*/
|
||||
async __notifyComponents() {
|
||||
const id = ++this.id;
|
||||
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, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, method): any {
|
||||
const __owl__ = component.__owl__;
|
||||
const id = __owl__.id;
|
||||
const mapping = ctx.mapping;
|
||||
if (id in mapping) {
|
||||
return ctx.state;
|
||||
}
|
||||
mapping[id] = 0;
|
||||
const renderFn = __owl__.render;
|
||||
__owl__.render = function(comp, params) {
|
||||
mapping[id] = ctx.id;
|
||||
return renderFn(comp, params);
|
||||
};
|
||||
ctx.on("update", component, async contextId => {
|
||||
if (mapping[id] < contextId) {
|
||||
mapping[id] = contextId;
|
||||
await method();
|
||||
}
|
||||
});
|
||||
onWillUnmount(() => {
|
||||
ctx.off("update", component);
|
||||
delete mapping[id];
|
||||
});
|
||||
return ctx.state;
|
||||
}
|
||||
@@ -29,7 +29,7 @@ export class EventBus {
|
||||
* Add a listener for the 'eventType' events.
|
||||
*
|
||||
* Note that the 'owner' of this event can be anything, but will more likely
|
||||
* be a widget or a class. The idea is that the callback will be called with
|
||||
* be a component or a class. The idea is that the callback will be called with
|
||||
* the proper owner bound.
|
||||
*
|
||||
* Also, the owner should be kind of unique. This will be used to remove the
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Owl Observer
|
||||
*
|
||||
* This code contains the logic that allows Owl to observe and react to state
|
||||
* changes.
|
||||
*
|
||||
* This is a Observer class that can observe any JS values. The way it works
|
||||
* can be summarized thusly:
|
||||
* - primitive values are not observed at all
|
||||
* - Objects and arrays are observed by replacing them with a Proxy
|
||||
* - each object/array metadata are tracked in a weakmap, and keep a revision
|
||||
* number
|
||||
*
|
||||
* Note that this code is loosely inspired by Vue.
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Observer
|
||||
//------------------------------------------------------------------------------
|
||||
export class Observer {
|
||||
rev: number = 1;
|
||||
allowMutations: boolean = true;
|
||||
dirty: boolean = false;
|
||||
weakMap: WeakMap<any, any> = new WeakMap();
|
||||
|
||||
notifyCB() {}
|
||||
async notifyChange() {
|
||||
this.dirty = true;
|
||||
await Promise.resolve();
|
||||
if (this.dirty) {
|
||||
this.dirty = false;
|
||||
this.notifyCB();
|
||||
}
|
||||
}
|
||||
|
||||
observe<T>(value: T, parent?: any): T {
|
||||
if (value === null || typeof value !== "object" || value instanceof Date) {
|
||||
// fun fact: typeof null === 'object'
|
||||
return value;
|
||||
}
|
||||
let metadata = this.weakMap.get(value) || this._observe(value, parent);
|
||||
return metadata.proxy;
|
||||
}
|
||||
|
||||
revNumber(value): number {
|
||||
const metadata = this.weakMap.get(value);
|
||||
return metadata ? metadata.rev : 0;
|
||||
}
|
||||
deepRevNumber(value): number {
|
||||
const metadata = this.weakMap.get(value);
|
||||
return metadata ? metadata.deepRev : 0;
|
||||
}
|
||||
|
||||
_observe(value, parent) {
|
||||
var self = this;
|
||||
|
||||
const proxy = new Proxy(value, {
|
||||
get(target, k) {
|
||||
const targetValue = target[k];
|
||||
return self.observe(targetValue, value);
|
||||
},
|
||||
set(target, key: string, newVal): boolean {
|
||||
const value = target[key];
|
||||
if (newVal !== value) {
|
||||
if (!self.allowMutations) {
|
||||
throw new Error(
|
||||
`Observed state cannot be changed here! (key: "${key}", val: "${newVal}")`
|
||||
);
|
||||
}
|
||||
self._updateRevNumber(target);
|
||||
target[key] = newVal;
|
||||
self.notifyChange();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
deleteProperty(target, key) {
|
||||
if (key in target) {
|
||||
delete target[key];
|
||||
self._updateRevNumber(target);
|
||||
self.notifyChange();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
const metadata = {
|
||||
value,
|
||||
proxy,
|
||||
rev: this.rev,
|
||||
deepRev: this.rev,
|
||||
parent
|
||||
};
|
||||
|
||||
this.weakMap.set(value, metadata);
|
||||
this.weakMap.set(metadata.proxy, metadata);
|
||||
return metadata;
|
||||
}
|
||||
|
||||
_updateRevNumber(target: any) {
|
||||
this.rev++;
|
||||
let metadata = this.weakMap.get(target);
|
||||
metadata.rev!++;
|
||||
let parent = target;
|
||||
do {
|
||||
metadata = this.weakMap.get(parent);
|
||||
metadata.deepRev++;
|
||||
} while ((parent = metadata.parent) && parent !== target);
|
||||
}
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
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];
|
||||
return val instanceof HTMLElement ? val : 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);
|
||||
}
|
||||
+26
-13
@@ -4,20 +4,34 @@
|
||||
*
|
||||
* Note that dynamic values, such as a date or a commit hash are added by rollup
|
||||
*/
|
||||
export { Component } from "./component";
|
||||
export { EventBus } from "./event_bus";
|
||||
export { Observer } from "./observer";
|
||||
import { EventBus } from "./core/event_bus";
|
||||
import { Observer } from "./core/observer";
|
||||
import { QWeb } from "./qweb/index";
|
||||
import * as _store from "./store";
|
||||
import * as _utils from "./utils";
|
||||
import * as _tags from "./tags";
|
||||
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";
|
||||
|
||||
// we need to import manually the extra directives so they can register
|
||||
// themselves in QWeb, otherwise these files will not even be loaded.
|
||||
import "./qweb_directives";
|
||||
import "./qweb_extensions";
|
||||
import { QWeb } from "./qweb_core";
|
||||
export { Component } from "./component/component";
|
||||
export { QWeb };
|
||||
|
||||
export { connect, Store } from "./store";
|
||||
import * as _utils from "./utils";
|
||||
|
||||
export const Context = _context.Context;
|
||||
export const useState = _hooks.useState;
|
||||
export const core = { EventBus, Observer };
|
||||
export const router = { Router, RouteComponent, Link };
|
||||
export const Store = _store.Store;
|
||||
export const utils = _utils;
|
||||
export const tags = _tags;
|
||||
export const hooks = Object.assign({}, _hooks, {
|
||||
useContext: _context.useContext,
|
||||
useDispatch: _store.useDispatch,
|
||||
useGetters: _store.useGetters,
|
||||
useStore: _store.useStore
|
||||
});
|
||||
export const __info__ = {};
|
||||
|
||||
Object.defineProperty(__info__, "mode", {
|
||||
@@ -32,8 +46,7 @@ Object.defineProperty(__info__, "mode", {
|
||||
`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.`)
|
||||
console.log(`Owl is now running in 'prod' mode.`);
|
||||
}
|
||||
}
|
||||
});
|
||||
export const utils = _utils;
|
||||
|
||||
-167
@@ -1,167 +0,0 @@
|
||||
/**
|
||||
* Owl Observer
|
||||
*
|
||||
* This code contains the logic that allows Owl to observe and react to state
|
||||
* changes.
|
||||
*
|
||||
* This is a Observer class that can observe any JS values. The way it works
|
||||
* can be summarized thusly:
|
||||
* - primitive values are not observed at all
|
||||
* - Objects are observed by replacing all their keys with getters/setters
|
||||
* (recursively)
|
||||
* - Arrays are observed by replacing their prototype with a customized version,
|
||||
* which wrap some methods to allow the tracking of each state change.
|
||||
*
|
||||
* Note that this code is inspired by Vue.
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Modified Array prototype
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// we define here a new modified Array prototype, which basically override all
|
||||
// Array methods that change some state to be able to track their changes
|
||||
const methodsToPatch = [
|
||||
"push",
|
||||
"pop",
|
||||
"shift",
|
||||
"unshift",
|
||||
"splice",
|
||||
"sort",
|
||||
"reverse"
|
||||
];
|
||||
const methodLen = methodsToPatch.length;
|
||||
|
||||
const ArrayProto = Array.prototype;
|
||||
const ModifiedArrayProto = Object.create(ArrayProto);
|
||||
|
||||
for (let i = 0; i < methodLen; i++) {
|
||||
const method = methodsToPatch[i];
|
||||
const initialMethod = ArrayProto[method];
|
||||
ModifiedArrayProto[method] = function(...args) {
|
||||
if (!this.__observer__.allowMutations) {
|
||||
throw new Error(`Array cannot be changed here")`);
|
||||
}
|
||||
this.__observer__.rev++;
|
||||
this.__observer__.notifyChange();
|
||||
this.__owl__.rev++;
|
||||
let parent = this;
|
||||
do {
|
||||
parent.__owl__.deepRev++;
|
||||
} while ((parent = parent.__owl__.parent));
|
||||
let inserted;
|
||||
switch (method) {
|
||||
case "push":
|
||||
case "unshift":
|
||||
inserted = args;
|
||||
break;
|
||||
case "splice":
|
||||
inserted = args.slice(2);
|
||||
break;
|
||||
}
|
||||
if (inserted) {
|
||||
for (let i = 0, iLen = inserted.length; i < iLen; i++) {
|
||||
this.__observer__.observe(inserted[i], this);
|
||||
}
|
||||
}
|
||||
return initialMethod.call(this, ...args);
|
||||
};
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Observer
|
||||
//------------------------------------------------------------------------------
|
||||
export class Observer {
|
||||
rev: number = 1;
|
||||
allowMutations: boolean = true;
|
||||
dirty: boolean = false;
|
||||
|
||||
notifyCB() {}
|
||||
notifyChange() {
|
||||
this.dirty = true;
|
||||
Promise.resolve().then(() => {
|
||||
if (this.dirty) {
|
||||
this.dirty = false;
|
||||
this.notifyCB();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
observe(value: any, parent?: any) {
|
||||
if (value === null) {
|
||||
// fun fact: typeof null === 'object'
|
||||
return;
|
||||
}
|
||||
if (typeof value !== "object") {
|
||||
return;
|
||||
}
|
||||
if ("__owl__" in value) {
|
||||
// already observed
|
||||
value.__owl__.parent = parent;
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
this._observeArr(value, parent);
|
||||
} else {
|
||||
this._observeObj(value, parent);
|
||||
}
|
||||
}
|
||||
|
||||
set(target: any, key: number | string, value: any) {
|
||||
this.rev++;
|
||||
this._addProp(target, key, value);
|
||||
target.__owl__.rev++;
|
||||
this.notifyChange();
|
||||
}
|
||||
|
||||
_observeObj<T extends { __owl__?: any }>(obj: T, parent?: any) {
|
||||
obj.__owl__ = { rev: this.rev, deepRev: this.rev, parent };
|
||||
Object.defineProperty(obj, "__owl__", { enumerable: false });
|
||||
for (let key in obj) {
|
||||
this._addProp(obj, key, obj[key]);
|
||||
}
|
||||
}
|
||||
|
||||
_observeArr(arr: Array<any>, parent?: any) {
|
||||
(<any>arr).__owl__ = { rev: this.rev, deepRev: this.rev, parent };
|
||||
Object.defineProperty(arr, "__owl__", { enumerable: false });
|
||||
(<any>arr).__proto__ = Object.create(ModifiedArrayProto);
|
||||
(<any>arr).__proto__.__observer__ = this;
|
||||
for (let i = 0, iLen = arr.length; i < iLen; i++) {
|
||||
this.observe(arr[i], arr);
|
||||
}
|
||||
}
|
||||
|
||||
_addProp<T extends { __owl__?: any }>(
|
||||
obj: T,
|
||||
key: string | number,
|
||||
value: any
|
||||
) {
|
||||
var self = this;
|
||||
Object.defineProperty(obj, key, {
|
||||
enumerable: true,
|
||||
get() {
|
||||
return value;
|
||||
},
|
||||
set(newVal) {
|
||||
if (newVal !== value) {
|
||||
self.rev++;
|
||||
if (!self.allowMutations) {
|
||||
throw new Error(
|
||||
`Observed state cannot be changed here! (key: "${key}", val: "${newVal}")`
|
||||
);
|
||||
}
|
||||
value = newVal;
|
||||
self.observe(newVal, obj);
|
||||
obj.__owl__.rev!++;
|
||||
let parent = obj;
|
||||
do {
|
||||
parent.__owl__.deepRev++;
|
||||
} while ((parent = parent.__owl__.parent) && parent !== obj);
|
||||
self.notifyChange();
|
||||
}
|
||||
}
|
||||
});
|
||||
this.observe(value, obj);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
import { Context, QWeb, UTILS } from "./qweb_core";
|
||||
import { CompilationContext } from "./compilation_context";
|
||||
import { QWebExprVar } from "./expression_parser";
|
||||
import { QWeb } from "./qweb";
|
||||
import { htmlToVDOM } from "../vdom/html_to_vdom";
|
||||
|
||||
/**
|
||||
* Owl QWeb Directives
|
||||
@@ -14,68 +17,67 @@ import { Context, QWeb, UTILS } from "./qweb_core";
|
||||
* - t-log
|
||||
*/
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-esc and t-raw
|
||||
//------------------------------------------------------------------------------
|
||||
(<any>UTILS).getFragment = function(str: string): DocumentFragment {
|
||||
QWeb.utils.getFragment = function(str: string): DocumentFragment {
|
||||
const temp = document.createElement("template");
|
||||
temp.innerHTML = str;
|
||||
return temp.content;
|
||||
};
|
||||
|
||||
function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Context) {
|
||||
QWeb.utils.htmlToVDOM = htmlToVDOM;
|
||||
|
||||
function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: CompilationContext) {
|
||||
if (value === "0" && ctx.caller) {
|
||||
qweb._compileNode(ctx.caller, ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
let exprID = value;
|
||||
if (!(value in ctx.definedVariables)) {
|
||||
exprID = `_${ctx.generateID()}`;
|
||||
ctx.addLine(`var ${exprID} = ${ctx.formatExpression(value)};`);
|
||||
}
|
||||
ctx.addIf(`${exprID} || ${exprID} === 0`);
|
||||
if (!ctx.parentNode) {
|
||||
throw new Error("Should not have a text node without a parent");
|
||||
}
|
||||
if (ctx.escaping) {
|
||||
ctx.addLine(`c${ctx.parentNode}.push({text: ${exprID}});`);
|
||||
} else {
|
||||
let fragID = ctx.generateID();
|
||||
ctx.addLine(`var frag${fragID} = this.utils.getFragment(${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) {
|
||||
ctx.addElse();
|
||||
qweb._compileChildren(node, ctx);
|
||||
}
|
||||
ctx.closeIf();
|
||||
return;
|
||||
}
|
||||
if (value instanceof NodeList) {
|
||||
for (let node of Array.from(value)) {
|
||||
if (value.xml instanceof NodeList) {
|
||||
for (let node of Array.from(value.xml)) {
|
||||
qweb._compileNode(<ChildNode>node, ctx);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let exprID: string;
|
||||
if (typeof value === "string") {
|
||||
exprID = `_${ctx.generateID()}`;
|
||||
ctx.addLine(`var ${exprID} = ${ctx.formatExpression(value)};`);
|
||||
} else {
|
||||
exprID = value.id;
|
||||
}
|
||||
ctx.addIf(`${exprID} || ${exprID} === 0`);
|
||||
if (ctx.escaping) {
|
||||
if (ctx.parentTextNode) {
|
||||
ctx.addLine(`vn${ctx.parentTextNode}.text += ${exprID};`);
|
||||
} else if (ctx.parentNode) {
|
||||
ctx.addLine(`c${ctx.parentNode}.push({text: ${exprID}});`);
|
||||
} else {
|
||||
let nodeID = ctx.generateID();
|
||||
ctx.rootContext.rootNode = nodeID;
|
||||
ctx.rootContext.parentTextNode = nodeID;
|
||||
ctx.addLine(`var vn${nodeID} = {text: ${exprID}};`);
|
||||
if (ctx.rootContext.shouldDefineResult) {
|
||||
ctx.addLine(`result = vn${nodeID}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
ctx.addLine(`c${ctx.parentNode}.push(...utils.htmlToVDOM(${exprID}));`);
|
||||
}
|
||||
if (node.childNodes.length) {
|
||||
ctx.addElse();
|
||||
qweb._compileChildren(node, ctx);
|
||||
}
|
||||
|
||||
ctx.closeIf();
|
||||
}
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "esc",
|
||||
priority: 70,
|
||||
atNodeEncounter({ node, qweb, ctx }): boolean {
|
||||
if (node.nodeName !== "t") {
|
||||
let nodeID = qweb._compileGenericNode(node, ctx);
|
||||
ctx = ctx.withParent(nodeID);
|
||||
}
|
||||
let value = ctx.getValue(node.getAttribute("t-esc")!);
|
||||
compileValueNode(value, node, qweb, ctx.subContext("escaping", true));
|
||||
return true;
|
||||
@@ -86,10 +88,6 @@ QWeb.addDirective({
|
||||
name: "raw",
|
||||
priority: 80,
|
||||
atNodeEncounter({ node, qweb, ctx }): boolean {
|
||||
if (node.nodeName !== "t") {
|
||||
let nodeID = qweb._compileGenericNode(node, ctx);
|
||||
ctx = ctx.withParent(nodeID);
|
||||
}
|
||||
let value = ctx.getValue(node.getAttribute("t-raw")!);
|
||||
compileValueNode(value, node, qweb, ctx);
|
||||
return true;
|
||||
@@ -109,15 +107,19 @@ QWeb.addDirective({
|
||||
if (value) {
|
||||
const formattedValue = ctx.formatExpression(value);
|
||||
if (ctx.variables.hasOwnProperty(variable)) {
|
||||
ctx.addLine(`${ctx.variables[variable]} = ${formattedValue}`);
|
||||
ctx.addLine(`${(<QWebExprVar>ctx.variables[variable]).id} = ${formattedValue}`);
|
||||
} else {
|
||||
const varName = `_${ctx.generateID()}`;
|
||||
ctx.addLine(`var ${varName} = ${formattedValue};`);
|
||||
ctx.definedVariables[varName] = formattedValue;
|
||||
ctx.variables[variable] = varName;
|
||||
ctx.variables[variable] = {
|
||||
id: varName,
|
||||
expr: formattedValue
|
||||
};
|
||||
}
|
||||
} else {
|
||||
ctx.variables[variable] = node.childNodes;
|
||||
ctx.variables[variable] = {
|
||||
xml: node.childNodes
|
||||
};
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -185,35 +187,69 @@ QWeb.addDirective({
|
||||
nodeCopy.removeAttribute("t-call");
|
||||
|
||||
// extract variables from nodecopy
|
||||
const tempCtx = new Context();
|
||||
const tempCtx = new CompilationContext();
|
||||
tempCtx.nextID = ctx.rootContext.nextID;
|
||||
tempCtx.allowMultipleRoots = true;
|
||||
qweb._compileNode(nodeCopy, tempCtx);
|
||||
const vars = Object.assign({}, ctx.variables, tempCtx.variables);
|
||||
var definedVariables = Object.assign(
|
||||
{},
|
||||
ctx.definedVariables,
|
||||
tempCtx.definedVariables
|
||||
);
|
||||
ctx.rootContext.nextID = tempCtx.nextID;
|
||||
|
||||
const templateMap = Object.create(ctx.templates);
|
||||
// open new scope, if necessary
|
||||
const hasNewVariables = Object.keys(definedVariables).length > 0;
|
||||
const hasNewVariables = Object.keys(tempCtx.variables).length > 0;
|
||||
|
||||
// compile sub template
|
||||
let subCtx = ctx.subContext("caller", nodeCopy).subContext("variables", Object.create(vars));
|
||||
subCtx = subCtx.subContext("templates", templateMap);
|
||||
|
||||
if (templateMap[subTemplate]) {
|
||||
// OUCH, IT IS A RECURSIVE TEMPLATE SITUATION...
|
||||
// This is a tricky situation... We obviously cannot inline the compiled
|
||||
// template. So, what we need to do is to compile it, and make sure we
|
||||
// properly transfer everything from the current scope to the sub template.
|
||||
ctx.rootContext.shouldTrackScope = true;
|
||||
ctx.rootContext.shouldDefineOwner = true;
|
||||
let subTemplateName;
|
||||
if (ctx.hasParentWidget) {
|
||||
subTemplateName = ctx.templateName;
|
||||
} else {
|
||||
subTemplateName = `__${ctx.generateID()}`;
|
||||
subCtx.variables = {};
|
||||
let id = 0;
|
||||
for (let v in vars) {
|
||||
subCtx.variables[v] = vars[v];
|
||||
(vars[v] as any).id = `_v${id++}`;
|
||||
}
|
||||
const subTemplateFn = qweb._compile(subTemplateName, nodeTemplate.elem, subCtx);
|
||||
qweb.recursiveFns[subTemplateName] = subTemplateFn;
|
||||
}
|
||||
let varCode = `{}`;
|
||||
if (Object.keys(vars).length) {
|
||||
let id = 0;
|
||||
const content = Object.values(vars)
|
||||
.map((v: any) => `_v${id++}: ${v.expr}`)
|
||||
.join(",");
|
||||
varCode = `{${content}}`;
|
||||
}
|
||||
ctx.addLine(
|
||||
`this.recursiveFns['${subTemplateName}'].call(this, context, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, fiber: {vars: ${varCode}, scope}}));`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
templateMap[subTemplate] = true;
|
||||
|
||||
if (hasNewVariables) {
|
||||
ctx.addLine("{");
|
||||
ctx.indent();
|
||||
// add new variables, if any
|
||||
for (let key in tempCtx.variables) {
|
||||
const v = tempCtx.variables[key];
|
||||
if ((<QWebExprVar>v).expr) {
|
||||
ctx.addLine(`let ${(<QWebExprVar>v).id} = ${(<QWebExprVar>v).expr};`);
|
||||
}
|
||||
// todo: handle XML variables...
|
||||
}
|
||||
}
|
||||
|
||||
// add new variables, if any
|
||||
for (let key in definedVariables) {
|
||||
ctx.addLine(`let ${key} = ${definedVariables[key]}`);
|
||||
}
|
||||
|
||||
// compile sub template
|
||||
const subCtx = ctx
|
||||
.subContext("caller", nodeCopy)
|
||||
.subContext("variables", Object.create(vars))
|
||||
.subContext("definedVariables", Object.create(definedVariables));
|
||||
|
||||
qweb._compileNode(nodeTemplate.elem, subCtx);
|
||||
|
||||
// close new scope
|
||||
@@ -222,6 +258,10 @@ QWeb.addDirective({
|
||||
ctx.addLine("}");
|
||||
}
|
||||
|
||||
if (node.hasAttribute("t-if") || node.hasAttribute("t-else") || node.hasAttribute("t-elif")) {
|
||||
ctx.closeIf();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
@@ -240,34 +280,26 @@ QWeb.addDirective({
|
||||
const name = node.getAttribute("t-as")!;
|
||||
let arrayID = ctx.generateID();
|
||||
ctx.addLine(`var _${arrayID} = ${ctx.formatExpression(elems)};`);
|
||||
ctx.addLine(
|
||||
`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`
|
||||
);
|
||||
ctx.addLine(
|
||||
`if (typeof _${arrayID} === 'number') { _${arrayID} = Array.from(Array(_${arrayID}).keys())}`
|
||||
);
|
||||
ctx.addLine(`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`);
|
||||
let keysID = ctx.generateID();
|
||||
ctx.addLine(
|
||||
`var _${keysID} = _${arrayID} instanceof Array ? _${arrayID} : Object.keys(_${arrayID});`
|
||||
);
|
||||
ctx.addLine(`var _length${keysID} = _${keysID}.length;`);
|
||||
let valuesID = ctx.generateID();
|
||||
ctx.addLine(
|
||||
`var _${valuesID} = _${arrayID} instanceof Array ? _${arrayID} : Object.values(_${arrayID});`
|
||||
);
|
||||
ctx.addLine(`var _${keysID} = _${valuesID} = _${arrayID};`);
|
||||
ctx.addIf(`!(_${arrayID} instanceof Array)`);
|
||||
ctx.addLine(`_${keysID} = Object.keys(_${arrayID});`);
|
||||
ctx.addLine(`_${valuesID} = Object.values(_${arrayID});`);
|
||||
ctx.closeIf();
|
||||
ctx.addLine(`var _length${keysID} = _${keysID}.length;`);
|
||||
ctx.addLine(`for (let i = 0; i < _length${keysID}; i++) {`);
|
||||
ctx.indent();
|
||||
ctx.addLine(`context.${name}_first = i === 0;`);
|
||||
ctx.addLine(`context.${name}_last = i === _length${keysID} - 1;`);
|
||||
ctx.addLine(`context.${name}_parity = i % 2 === 0 ? 'even' : 'odd';`);
|
||||
ctx.addLine(`context.${name}_index = i;`);
|
||||
ctx.addLine(`context.${name} = _${keysID}[i];`);
|
||||
ctx.addLine(`context.${name}_value = _${valuesID}[i];`);
|
||||
ctx.addToScope(name + "_first", "i === 0");
|
||||
ctx.addToScope(name + "_last", `i === _length${keysID} - 1`);
|
||||
ctx.addToScope(name + "_index", "i");
|
||||
ctx.addToScope(name, `_${keysID}[i]`);
|
||||
ctx.addToScope(name + "_value", `_${valuesID}[i]`);
|
||||
const nodeCopy = <Element>node.cloneNode(true);
|
||||
let shouldWarn =
|
||||
nodeCopy.tagName !== "t" && !nodeCopy.hasAttribute("t-key");
|
||||
let shouldWarn = nodeCopy.tagName !== "t" && !nodeCopy.hasAttribute("t-key");
|
||||
if (!shouldWarn && node.tagName === "t") {
|
||||
if (node.hasAttribute("t-widget") && !node.hasAttribute("t-key")) {
|
||||
if (node.hasAttribute("t-component") && !node.hasAttribute("t-key")) {
|
||||
shouldWarn = true;
|
||||
}
|
||||
if (
|
||||
@@ -281,9 +313,7 @@ QWeb.addDirective({
|
||||
}
|
||||
if (shouldWarn) {
|
||||
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");
|
||||
@@ -299,7 +329,7 @@ QWeb.addDirective({
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "debug",
|
||||
priority: 99,
|
||||
priority: 1,
|
||||
atNodeEncounter({ ctx }) {
|
||||
ctx.addLine("debugger;");
|
||||
}
|
||||
@@ -310,7 +340,7 @@ QWeb.addDirective({
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "log",
|
||||
priority: 99,
|
||||
priority: 1,
|
||||
atNodeEncounter({ ctx, value }) {
|
||||
const expr = ctx.formatExpression(value);
|
||||
ctx.addLine(`console.log(${expr})`);
|
||||
@@ -0,0 +1,179 @@
|
||||
import { compileExpr, QWebVar } from "./expression_parser";
|
||||
|
||||
export const INTERP_REGEXP = /\{\{.*?\}\}/g;
|
||||
//------------------------------------------------------------------------------
|
||||
// Compilation Context
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export class CompilationContext {
|
||||
nextID: number = 1;
|
||||
code: string[] = [];
|
||||
variables: { [key: string]: QWebVar } = {};
|
||||
escaping: boolean = false;
|
||||
parentNode: number | null = null;
|
||||
parentTextNode: number | null = null;
|
||||
rootNode: number | null = null;
|
||||
indentLevel: number = 0;
|
||||
rootContext: CompilationContext;
|
||||
caller: Element | undefined;
|
||||
shouldDefineOwner: boolean = false;
|
||||
shouldDefineParent: boolean = false;
|
||||
shouldDefineQWeb: boolean = false;
|
||||
shouldDefineUtils: boolean = false;
|
||||
shouldDefineRefs: boolean = false;
|
||||
shouldDefineResult: boolean = true;
|
||||
shouldProtectContext: boolean = false;
|
||||
shouldTrackScope: boolean = false;
|
||||
inLoop: boolean = false;
|
||||
inPreTag: boolean = false;
|
||||
templateName: string;
|
||||
allowMultipleRoots: boolean = false;
|
||||
hasParentWidget: boolean = false;
|
||||
scopeVars: any[] = [];
|
||||
currentKey: string = "";
|
||||
lastNodeKey: string = ""; // temp variable to communicate to previous caller
|
||||
templates: { [key: string]: boolean } = {};
|
||||
|
||||
constructor(name?: string) {
|
||||
this.rootContext = this;
|
||||
this.templateName = name || "noname";
|
||||
this.templates[this.templateName] = true;
|
||||
this.addLine("var h = this.h;");
|
||||
}
|
||||
|
||||
generateID(): number {
|
||||
const id = this.rootContext.nextID++;
|
||||
return id;
|
||||
}
|
||||
|
||||
generateCode(): string[] {
|
||||
const shouldTrackScope = this.shouldTrackScope && this.scopeVars.length;
|
||||
if (shouldTrackScope) {
|
||||
// add some vars to scope if needed
|
||||
for (let scopeVar of this.scopeVars.reverse()) {
|
||||
let { index, key, indent } = scopeVar;
|
||||
const prefix = new Array(indent + 2).join(" ");
|
||||
this.code.splice(index + 1, 0, prefix + `scope.${key} = context.${key};`);
|
||||
}
|
||||
this.code.unshift(" const scope = Object.create(null);");
|
||||
}
|
||||
if (this.shouldProtectContext) {
|
||||
this.code.unshift(" context = Object.create(context);");
|
||||
}
|
||||
if (this.shouldDefineResult) {
|
||||
this.code.unshift(" let result;");
|
||||
}
|
||||
if (this.shouldDefineRefs) {
|
||||
this.code.unshift(" context.__owl__.refs = context.__owl__.refs || {};");
|
||||
}
|
||||
if (this.shouldDefineOwner) {
|
||||
// this is necessary to prevent some directives (t-forach for ex) to
|
||||
// pollute the rendering context by adding some keys in it.
|
||||
this.code.unshift(" let owner = context;");
|
||||
}
|
||||
if (this.shouldDefineParent) {
|
||||
if (this.hasParentWidget) {
|
||||
this.code.unshift(" let parent = extra.parent;");
|
||||
} else {
|
||||
this.code.unshift(" let parent = context;");
|
||||
}
|
||||
}
|
||||
if (this.shouldDefineQWeb) {
|
||||
this.code.unshift(" let QWeb = this.constructor;");
|
||||
}
|
||||
if (this.shouldDefineUtils) {
|
||||
this.code.unshift(" let utils = this.constructor.utils;");
|
||||
}
|
||||
return this.code;
|
||||
}
|
||||
|
||||
withParent(node: number): CompilationContext {
|
||||
if (
|
||||
!this.allowMultipleRoots &&
|
||||
this === this.rootContext &&
|
||||
(this.parentNode || this.parentTextNode)
|
||||
) {
|
||||
throw new Error("A template should not have more than one root node");
|
||||
}
|
||||
if (!this.rootContext.rootNode) {
|
||||
this.rootContext.rootNode = node;
|
||||
}
|
||||
if (!this.parentNode && this.rootContext.shouldDefineResult) {
|
||||
this.addLine(`result = vn${node};`);
|
||||
}
|
||||
return this.subContext("parentNode", node);
|
||||
}
|
||||
|
||||
subContext(key: keyof CompilationContext, value: any): CompilationContext {
|
||||
const newContext = Object.create(this);
|
||||
newContext[key] = value;
|
||||
return newContext;
|
||||
}
|
||||
|
||||
indent() {
|
||||
this.indentLevel++;
|
||||
}
|
||||
|
||||
dedent() {
|
||||
this.indentLevel--;
|
||||
}
|
||||
|
||||
addLine(line: string): number {
|
||||
const prefix = new Array(this.indentLevel + 2).join(" ");
|
||||
this.code.push(prefix + line);
|
||||
return this.code.length - 1;
|
||||
}
|
||||
|
||||
addToScope(key: string, expr: string) {
|
||||
const index = this.addLine(`context.${key} = ${expr};`);
|
||||
this.rootContext.scopeVars.push({ index, key, indent: this.indentLevel });
|
||||
}
|
||||
|
||||
addIf(condition: string) {
|
||||
this.addLine(`if (${condition}) {`);
|
||||
this.indent();
|
||||
}
|
||||
|
||||
addElse() {
|
||||
this.dedent();
|
||||
this.addLine("} else {");
|
||||
this.indent();
|
||||
}
|
||||
|
||||
closeIf() {
|
||||
this.dedent();
|
||||
this.addLine("}");
|
||||
}
|
||||
|
||||
getValue(val: any): any {
|
||||
return val in this.variables ? this.getValue(this.variables[val]) : val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare an expression for being consumed at render time. Its main job
|
||||
* is to
|
||||
* - replace unknown variables by a lookup in the context
|
||||
* - replace already defined variables by their internal name
|
||||
*/
|
||||
formatExpression(expr: string): string {
|
||||
return compileExpr(expr, this.variables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform string interpolation on the given string. Note that if the whole
|
||||
* string is an expression, it simply returns it (formatted and enclosed in
|
||||
* parentheses).
|
||||
* For instance:
|
||||
* 'Hello {{x}}!' -> `Hello ${x}`
|
||||
* '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b')
|
||||
*/
|
||||
interpolate(s: string): string {
|
||||
let matches = s.match(INTERP_REGEXP);
|
||||
if (matches && matches[0].length === s.length) {
|
||||
return `(${this.formatExpression(s.slice(2, -2))})`;
|
||||
}
|
||||
|
||||
let r = s.replace(/\{\{.*?\}\}/g, s => "${" + this.formatExpression(s.slice(2, -2)) + "}");
|
||||
return "`" + r + "`";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
/**
|
||||
* Owl QWeb Expression Parser
|
||||
*
|
||||
* Owl needs in various contexts to be able to understand the structure of a
|
||||
* string representing a javascript expression. The usual goal is to be able
|
||||
* to rewrite some variables. For example, if a template has
|
||||
*
|
||||
* ```xml
|
||||
* <t t-if="computeSomething({val: state.val})">...</t>
|
||||
* ```
|
||||
*
|
||||
* this needs to be translated in something like this:
|
||||
*
|
||||
* ```js
|
||||
* if (context["computeSomething"]({val: context["state"].val})) { ... }
|
||||
* ```
|
||||
*
|
||||
* This file contains the implementation of an extremely naive tokenizer/parser
|
||||
* and evaluator for javascript expressions. The supported grammar is basically
|
||||
* only expressive enough to understand the shape of objects, of arrays, and
|
||||
* various operators.
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// 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 WORD_REPLACEMENT = {
|
||||
and: "&&",
|
||||
or: "||",
|
||||
gt: ">",
|
||||
gte: ">=",
|
||||
lt: "<",
|
||||
lte: "<="
|
||||
};
|
||||
|
||||
export interface QWebExprVar {
|
||||
id: string;
|
||||
expr: string;
|
||||
}
|
||||
|
||||
export interface QWebXMLVar {
|
||||
xml: NodeList;
|
||||
}
|
||||
|
||||
export type QWebVar = QWebExprVar | QWebXMLVar;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Tokenizer
|
||||
//------------------------------------------------------------------------------
|
||||
type TKind =
|
||||
| "LEFT_BRACE"
|
||||
| "RIGHT_BRACE"
|
||||
| "LEFT_BRACKET"
|
||||
| "RIGHT_BRACKET"
|
||||
| "LEFT_PAREN"
|
||||
| "RIGHT_PAREN"
|
||||
| "COMMA"
|
||||
| "VALUE"
|
||||
| "SYMBOL"
|
||||
| "OPERATOR"
|
||||
| "COLON";
|
||||
|
||||
interface Token {
|
||||
type: TKind;
|
||||
value: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
const STATIC_TOKEN_MAP: { [key: string]: TKind } = {
|
||||
"{": "LEFT_BRACE",
|
||||
"}": "RIGHT_BRACE",
|
||||
"[": "LEFT_BRACKET",
|
||||
"]": "RIGHT_BRACKET",
|
||||
":": "COLON",
|
||||
",": "COMMA",
|
||||
"(": "LEFT_PAREN",
|
||||
")": "RIGHT_PAREN"
|
||||
};
|
||||
|
||||
const OPERATORS = ".,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%".split(",");
|
||||
|
||||
type Tokenizer = (expr: string) => Token | false;
|
||||
|
||||
let tokenizeString: Tokenizer = function(expr) {
|
||||
let s = expr[0];
|
||||
let start = s;
|
||||
if (s !== "'" && s !== '"') {
|
||||
return false;
|
||||
}
|
||||
let i = 1;
|
||||
let cur;
|
||||
while (expr[i] && expr[i] !== start) {
|
||||
cur = expr[i];
|
||||
s += cur;
|
||||
if (cur === "\\") {
|
||||
i++;
|
||||
cur = expr[i];
|
||||
if (!cur) {
|
||||
throw new Error("Invalid expression");
|
||||
}
|
||||
s += cur;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (expr[i] !== start) {
|
||||
throw new Error("Invalid expression");
|
||||
}
|
||||
s += start;
|
||||
return { type: "VALUE", value: s };
|
||||
};
|
||||
|
||||
let tokenizeNumber: Tokenizer = function(expr) {
|
||||
let s = expr[0];
|
||||
if (s && s.match(/[0-9]/)) {
|
||||
let i = 1;
|
||||
while (expr[i] && expr[i].match(/[0-9]|\./)) {
|
||||
s += expr[i];
|
||||
i++;
|
||||
}
|
||||
return { type: "VALUE", value: s };
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let tokenizeSymbol: Tokenizer = function(expr) {
|
||||
let s = expr[0];
|
||||
if (s && s.match(/[a-zA-Z_\$]/)) {
|
||||
let i = 1;
|
||||
while (expr[i] && expr[i].match(/\w/)) {
|
||||
s += expr[i];
|
||||
i++;
|
||||
}
|
||||
if (s in WORD_REPLACEMENT) {
|
||||
return { type: "OPERATOR", value: WORD_REPLACEMENT[s], size: s.length };
|
||||
}
|
||||
return { type: "SYMBOL", value: s };
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const tokenizeStatic: Tokenizer = function(expr) {
|
||||
const char = expr[0];
|
||||
if (char && char in STATIC_TOKEN_MAP) {
|
||||
return { type: STATIC_TOKEN_MAP[char], value: char };
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const tokenizeOperator: Tokenizer = function(expr) {
|
||||
for (let op of OPERATORS) {
|
||||
if (expr.startsWith(op)) {
|
||||
return { type: "OPERATOR", value: op };
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const TOKENIZERS = [
|
||||
tokenizeString,
|
||||
tokenizeNumber,
|
||||
tokenizeSymbol,
|
||||
tokenizeStatic,
|
||||
tokenizeOperator
|
||||
];
|
||||
|
||||
/**
|
||||
* Convert a javascript expression (as a string) into a list of tokens. For
|
||||
* example: `tokenize("1 + b")` will return:
|
||||
* ```js
|
||||
* [
|
||||
* {type: "VALUE", value: "1"},
|
||||
* {type: "OPERATOR", value: "+"},
|
||||
* {type: "SYMBOL", value: "b"}
|
||||
* ]
|
||||
* ```
|
||||
*/
|
||||
export function tokenize(expr: string): Token[] {
|
||||
const result: Token[] = [];
|
||||
let token: boolean | Token = true;
|
||||
|
||||
while (token) {
|
||||
expr = expr.trim();
|
||||
if (expr) {
|
||||
for (let tokenizer of TOKENIZERS) {
|
||||
token = tokenizer(expr);
|
||||
if (token) {
|
||||
result.push(token);
|
||||
expr = expr.slice(token.size || token.value.length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
token = false;
|
||||
}
|
||||
}
|
||||
if (expr.length) {
|
||||
throw new Error(`Tokenizer error: could not tokenize "${expr}"`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Expression "evaluator"
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* This is the main function exported by this file. This is the code that will
|
||||
* process an expression (given as a string) and returns another expression with
|
||||
* proper lookups in the context.
|
||||
*
|
||||
* Usually, this kind of code would be very simple to do if we had an AST (so,
|
||||
* if we had a javascript parser), since then, we would only need to find the
|
||||
* variables and replace them. However, a parser is more complicated, and there
|
||||
* are no standard builtin parser API.
|
||||
*
|
||||
* Since this method is applied to simple javasript expressions, and the work to
|
||||
* be done is actually quite simple, we actually can get away with not using a
|
||||
* parser, which helps with the code size.
|
||||
*
|
||||
* Here is the heuristic used by this method to determine if a token is a
|
||||
* variable:
|
||||
* - by default, all symbols are considered a variable
|
||||
* - unless the previous token is a dot (in that case, this is a property: `a.b`)
|
||||
* - or if the previous token is a left brace or a comma, and the next token is
|
||||
* a colon (in that case, this is an object key: `{a: b}`)
|
||||
*/
|
||||
export function compileExpr(expr: string, vars: { [key: string]: QWebVar }): string {
|
||||
const tokens = tokenize(expr);
|
||||
let result = "";
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
let token = tokens[i];
|
||||
if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) {
|
||||
// we need to find if it is a variable
|
||||
let isVar = true;
|
||||
let prevToken = tokens[i - 1];
|
||||
if (prevToken) {
|
||||
if (prevToken.type === "OPERATOR" && prevToken.value === ".") {
|
||||
isVar = false;
|
||||
} else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") {
|
||||
let nextToken = tokens[i + 1];
|
||||
if (nextToken && nextToken.type === "COLON") {
|
||||
isVar = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isVar) {
|
||||
if (token.value in vars && "id" in vars[token.value]) {
|
||||
token.value = (<QWebExprVar>vars[token.value]).id;
|
||||
} else {
|
||||
token.value = `context['${token.value}']`;
|
||||
}
|
||||
}
|
||||
}
|
||||
result += token.value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
import { VNode } from "../vdom/index";
|
||||
import { QWeb } from "./qweb";
|
||||
|
||||
/**
|
||||
* Owl QWeb Extensions
|
||||
*
|
||||
* This file contains the implementation of non standard QWeb directives, added
|
||||
* by Owl and that will only work on Owl projects:
|
||||
*
|
||||
* - t-on
|
||||
* - t-ref
|
||||
* - t-transition
|
||||
* - t-mounted
|
||||
* - t-slot
|
||||
* - t-model
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-on
|
||||
//------------------------------------------------------------------------------
|
||||
// these are pieces of code that will be injected into the event handler if
|
||||
// modifiers are specified
|
||||
export const MODS_CODE = {
|
||||
prevent: "e.preventDefault();",
|
||||
self: "if (e.target !== this.elm) {return}",
|
||||
stop: "e.stopPropagation();"
|
||||
};
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "on",
|
||||
priority: 90,
|
||||
atNodeCreation({ ctx, fullName, value, nodeID }) {
|
||||
ctx.rootContext.shouldDefineOwner = true;
|
||||
const [eventName, ...mods] = fullName.slice(5).split(".");
|
||||
if (!eventName) {
|
||||
throw new Error("Missing event name with t-on directive");
|
||||
}
|
||||
let extraArgs;
|
||||
let handlerName = value.replace(/\(.*\)/, function(args) {
|
||||
extraArgs = args.slice(1, -1);
|
||||
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";
|
||||
if (mods.length > 0) {
|
||||
let handler = `function (e) {`;
|
||||
handler += mods
|
||||
.map(function(mod) {
|
||||
return MODS_CODE[mod];
|
||||
})
|
||||
.join("");
|
||||
if (!extraArgs) {
|
||||
handler += `context['${handlerName}'].call(${params}, e);}`;
|
||||
ctx.addLine(
|
||||
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`
|
||||
);
|
||||
ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`);
|
||||
} else {
|
||||
const handlerKey = `handler${ctx.generateID()}`;
|
||||
ctx.addLine(`const ${handlerKey} = context['${handlerName}'].bind(${params});`);
|
||||
handler += `${handlerKey}(e);}`;
|
||||
ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
|
||||
}
|
||||
} else {
|
||||
const handler = `context['${handlerName}'].bind(${params})`;
|
||||
if (extraArgs) {
|
||||
ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
|
||||
} else {
|
||||
ctx.addLine(
|
||||
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`
|
||||
);
|
||||
ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-ref
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "ref",
|
||||
priority: 95,
|
||||
atNodeCreation({ ctx, value, addNodeHook }) {
|
||||
ctx.rootContext.shouldDefineRefs = true;
|
||||
const refKey = `ref${ctx.generateID()}`;
|
||||
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
|
||||
addNodeHook("create", `context.__owl__.refs[${refKey}] = n.elm;`);
|
||||
addNodeHook("destroy", `delete context.__owl__.refs[${refKey}];`);
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-transition
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.utils.nextFrame = function(cb: () => void) {
|
||||
requestAnimationFrame(() => requestAnimationFrame(cb));
|
||||
};
|
||||
|
||||
QWeb.utils.transitionInsert = function(vn: VNode, name: string) {
|
||||
const elm = <HTMLElement>vn.elm;
|
||||
// remove potential duplicated vnode that is currently being removed, to
|
||||
// prevent from having twice the same node in the DOM during an animation
|
||||
const dup = elm.parentElement && elm.parentElement!.querySelector(`*[data-owl-key='${vn.key}']`);
|
||||
if (dup) {
|
||||
dup.remove();
|
||||
}
|
||||
|
||||
elm.classList.add(name + "-enter");
|
||||
elm.classList.add(name + "-enter-active");
|
||||
const finalize = () => {
|
||||
elm.classList.remove(name + "-enter-active");
|
||||
elm.classList.remove(name + "-enter-to");
|
||||
};
|
||||
this.nextFrame(() => {
|
||||
elm.classList.remove(name + "-enter");
|
||||
elm.classList.add(name + "-enter-to");
|
||||
whenTransitionEnd(elm, finalize);
|
||||
});
|
||||
};
|
||||
|
||||
QWeb.utils.transitionRemove = function(vn: VNode, name: string, rm: () => void) {
|
||||
const elm = <HTMLElement>vn.elm;
|
||||
elm.setAttribute("data-owl-key", vn.key!);
|
||||
|
||||
elm.classList.add(name + "-leave");
|
||||
elm.classList.add(name + "-leave-active");
|
||||
const finalize = () => {
|
||||
elm.classList.remove(name + "-leave-active");
|
||||
elm.classList.remove(name + "-leave-to");
|
||||
rm();
|
||||
};
|
||||
this.nextFrame(() => {
|
||||
elm.classList.remove(name + "-leave");
|
||||
elm.classList.add(name + "-leave-to");
|
||||
whenTransitionEnd(elm, finalize);
|
||||
});
|
||||
};
|
||||
|
||||
function getTimeout(delays: Array<string>, durations: Array<string>): number {
|
||||
/* istanbul ignore next */
|
||||
while (delays.length < durations.length) {
|
||||
delays = delays.concat(delays);
|
||||
}
|
||||
|
||||
return Math.max.apply(
|
||||
null,
|
||||
durations.map((d, i) => {
|
||||
return toMs(d) + toMs(delays[i]);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Old versions of Chromium (below 61.0.3163.100) formats floating pointer numbers
|
||||
// in a locale-dependent way, using a comma instead of a dot.
|
||||
// If comma is not replaced with a dot, the input will be rounded down (i.e. acting
|
||||
// as a floor function) causing unexpected behaviors
|
||||
function toMs(s: string): number {
|
||||
return Number(s.slice(0, -1).replace(",", ".")) * 1000;
|
||||
}
|
||||
|
||||
function whenTransitionEnd(elm: HTMLElement, cb) {
|
||||
const styles = window.getComputedStyle(elm);
|
||||
const delays: Array<string> = (styles.transitionDelay || "").split(", ");
|
||||
const durations: Array<string> = (styles.transitionDuration || "").split(", ");
|
||||
const timeout: number = getTimeout(delays, durations);
|
||||
if (timeout > 0) {
|
||||
elm.addEventListener("transitionend", cb, { once: true });
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "transition",
|
||||
priority: 96,
|
||||
atNodeCreation({ ctx, value, addNodeHook }) {
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
let name = value;
|
||||
const hooks = {
|
||||
insert: `utils.transitionInsert(vn, '${name}');`,
|
||||
remove: `utils.transitionRemove(vn, '${name}', rm);`
|
||||
};
|
||||
for (let hookName in hooks) {
|
||||
addNodeHook(hookName, hooks[hookName]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-slot
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "slot",
|
||||
priority: 80,
|
||||
atNodeEncounter({ ctx, value }): boolean {
|
||||
const slotKey = ctx.generateID();
|
||||
ctx.rootContext.shouldDefineOwner = true;
|
||||
ctx.addLine(
|
||||
`const slot${slotKey} = this.constructor.slots[context.__owl__.slotId + '_' + '${value}'];`
|
||||
);
|
||||
ctx.addIf(`slot${slotKey}`);
|
||||
let parentNode = `c${ctx.parentNode}`;
|
||||
if (!ctx.parentNode) {
|
||||
ctx.rootContext.shouldDefineResult = true;
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
parentNode = `children${ctx.nextID++}`;
|
||||
ctx.addLine(`let ${parentNode}= []`);
|
||||
ctx.addLine(`result = {}`);
|
||||
}
|
||||
ctx.addLine(
|
||||
`slot${slotKey}.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: ${parentNode}, vars: extra.vars, parent: owner}));`
|
||||
);
|
||||
if (!ctx.parentNode) {
|
||||
ctx.addLine(`Promise.all(extra.promises).then(() => utils.defineProxy(result, ${parentNode}[0]))`);
|
||||
}
|
||||
ctx.closeIf();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-model
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.utils.toNumber = function(val: string): number | string {
|
||||
const n = parseFloat(val);
|
||||
return isNaN(n) ? val : n;
|
||||
};
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "model",
|
||||
priority: 42,
|
||||
atNodeCreation({ ctx, nodeID, value, node, fullName, addNodeHook }) {
|
||||
const type = node.getAttribute("type");
|
||||
let handler;
|
||||
let event = fullName.includes(".lazy") ? "change" : "input";
|
||||
const expr = ctx.formatExpression(value);
|
||||
if (node.tagName === "select") {
|
||||
ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
|
||||
addNodeHook("create", `n.elm.value=${expr};`);
|
||||
event = "change";
|
||||
handler = `(ev) => {${expr} = ev.target.value}`;
|
||||
} else if (type === "checkbox") {
|
||||
ctx.addLine(`p${nodeID}.props = {checked: ${expr}};`);
|
||||
handler = `(ev) => {${expr} = ev.target.checked}`;
|
||||
} else if (type === "radio") {
|
||||
const nodeValue = node.getAttribute("value")!;
|
||||
ctx.addLine(`p${nodeID}.props = {checked:${expr} === '${nodeValue}'};`);
|
||||
handler = `(ev) => {${expr} = ev.target.value}`;
|
||||
event = "click";
|
||||
} else {
|
||||
ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
|
||||
const trimCode = fullName.includes(".trim") ? ".trim()" : "";
|
||||
let valueCode = `ev.target.value${trimCode}`;
|
||||
if (fullName.includes(".number")) {
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
valueCode = `utils.toNumber(${valueCode})`;
|
||||
}
|
||||
handler = `(ev) => {${expr} = ${valueCode}}`;
|
||||
}
|
||||
ctx.addLine(
|
||||
`extra.handlers['${event}' + ${nodeID}] = extra.handlers['${event}' + ${nodeID}] || (${handler});`
|
||||
);
|
||||
ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers['${event}' + ${nodeID}];`);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
import "./base_directives";
|
||||
import "./extensions";
|
||||
|
||||
export { CompiledTemplate, QWeb } from "./qweb";
|
||||
+299
-329
@@ -1,4 +1,8 @@
|
||||
import { VNode, h } from "./vdom";
|
||||
import { EventBus } from "../core/event_bus";
|
||||
import { h, patch, VNode } from "../vdom/index";
|
||||
import { CompilationContext } from "./compilation_context";
|
||||
import { shallowEqual } from "../utils";
|
||||
import { addNS } from "../vdom/vdom";
|
||||
|
||||
/**
|
||||
* Owl QWeb Engine
|
||||
@@ -32,7 +36,7 @@ interface Template {
|
||||
interface CompilationInfo {
|
||||
node: Element;
|
||||
qweb: QWeb;
|
||||
ctx: Context;
|
||||
ctx: CompilationContext;
|
||||
fullName: string;
|
||||
value: string;
|
||||
}
|
||||
@@ -56,120 +60,162 @@ export interface Directive {
|
||||
//------------------------------------------------------------------------------
|
||||
// Const/global stuff/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 WORD_REPLACEMENT = {
|
||||
and: "&&",
|
||||
or: "||",
|
||||
gt: ">",
|
||||
gte: ">=",
|
||||
lt: "<",
|
||||
lte: "<="
|
||||
};
|
||||
|
||||
const DISABLED_TAGS = [
|
||||
"input",
|
||||
"textarea",
|
||||
"button",
|
||||
"select",
|
||||
"option",
|
||||
"optgroup"
|
||||
];
|
||||
const DISABLED_TAGS = ["input", "textarea", "button", "select", "option", "optgroup"];
|
||||
|
||||
const lineBreakRE = /[\r\n]/;
|
||||
const whitespaceRE = /\s+/g;
|
||||
|
||||
const DIRECTIVE_NAMES = {
|
||||
name: 1,
|
||||
att: 1,
|
||||
attf: 1,
|
||||
key: 1
|
||||
};
|
||||
|
||||
const DIRECTIVES: Directive[] = [];
|
||||
|
||||
const NODE_HOOKS_PARAMS = {
|
||||
create: "(_, n)",
|
||||
insert: "vn",
|
||||
remove: "(vn, rm)"
|
||||
remove: "(vn, rm)",
|
||||
destroy: "()"
|
||||
};
|
||||
|
||||
interface Utils {
|
||||
h: typeof h;
|
||||
objectToAttrString(obj: Object): string;
|
||||
toObj(expr: any): Object;
|
||||
shallowEqual(p1: Object, p2: Object): boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const UTILS: Utils = {
|
||||
h: h,
|
||||
objectToAttrString(obj: Object): string {
|
||||
let classes: string[] = [];
|
||||
for (let k in obj) {
|
||||
if (obj[k]) {
|
||||
classes.push(k);
|
||||
const UTILS: Utils = {
|
||||
toObj(expr) {
|
||||
if (typeof expr === "string") {
|
||||
expr = expr.trim();
|
||||
if (!expr) {
|
||||
return {};
|
||||
}
|
||||
let words = expr.split(/\s+/);
|
||||
let result = {};
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
result[words[i]] = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return classes.join(" ");
|
||||
return expr;
|
||||
},
|
||||
shallowEqual,
|
||||
addNameSpace(vnode) {
|
||||
addNS(vnode.data, vnode.children, vnode.sel);
|
||||
}
|
||||
};
|
||||
|
||||
function parseXML(xml: string): Document {
|
||||
const parser = new DOMParser();
|
||||
|
||||
const doc = parser.parseFromString(xml, "text/xml");
|
||||
if (doc.getElementsByTagName("parsererror").length) {
|
||||
throw new Error("Invalid XML in template");
|
||||
let msg = "Invalid XML in template.";
|
||||
const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
|
||||
if (parsererrorText) {
|
||||
msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
|
||||
const re = /\d+/g;
|
||||
const firstMatch = re.exec(parsererrorText);
|
||||
if (firstMatch) {
|
||||
const lineNumber = Number(firstMatch[0]);
|
||||
const line = xml.split("\n")[lineNumber - 1];
|
||||
const secondMatch = re.exec(parsererrorText);
|
||||
if (line && secondMatch) {
|
||||
const columnIndex = Number(secondMatch[0]) - 1;
|
||||
if (line[columnIndex]) {
|
||||
msg +=
|
||||
`\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
|
||||
`${line}\n${"-".repeat(columnIndex - 1)}^`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error(msg);
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
let nextID = 1;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// QWeb rendering engine
|
||||
//------------------------------------------------------------------------------
|
||||
export class QWeb {
|
||||
templates: { [name: string]: Template } = {};
|
||||
utils = UTILS;
|
||||
static widgets = Object.create(null);
|
||||
export class QWeb extends EventBus {
|
||||
templates: { [name: string]: Template };
|
||||
static utils = UTILS;
|
||||
static components = Object.create(null);
|
||||
|
||||
static DIRECTIVE_NAMES: { [key: string]: 1 } = {
|
||||
name: 1,
|
||||
att: 1,
|
||||
attf: 1,
|
||||
key: 1
|
||||
};
|
||||
static DIRECTIVES: Directive[] = [];
|
||||
|
||||
static TEMPLATES: { [name: string]: Template } = {};
|
||||
|
||||
static nextId: number = 1;
|
||||
|
||||
h = h;
|
||||
// dev mode enables better error messages or more costly validations
|
||||
static dev: boolean = false;
|
||||
|
||||
// the id field is useful to be able to hash qweb instances. The current
|
||||
// use case is that component's templates are qweb dependant, and need to be
|
||||
// able to map a qweb instance to a template name.
|
||||
id = nextID++;
|
||||
// slots contains sub templates defined with t-set inside t-component nodes, and
|
||||
// are meant to be used by the t-slot directive.
|
||||
static slots = {};
|
||||
static nextSlotId = 1;
|
||||
|
||||
// recursiveTemplates contains sub templates called with t-call, but which
|
||||
// ends up in recursive situations. This is very similar to the slot situation,
|
||||
// as in we need to propagate the scope.
|
||||
recursiveFns = {};
|
||||
|
||||
isUpdating: boolean = false;
|
||||
|
||||
constructor(data?: string) {
|
||||
super();
|
||||
this.templates = Object.create(QWeb.TEMPLATES);
|
||||
if (data) {
|
||||
this.addTemplates(data);
|
||||
}
|
||||
this.addTemplate("default", "<div></div>");
|
||||
}
|
||||
|
||||
static addDirective(directive: Directive) {
|
||||
DIRECTIVES.push(directive);
|
||||
DIRECTIVE_NAMES[directive.name] = 1;
|
||||
DIRECTIVES.sort((d1, d2) => d1.priority - d2.priority);
|
||||
if (directive.name in QWeb.DIRECTIVE_NAMES) {
|
||||
throw new Error(`Directive "${directive.name} already registered`);
|
||||
}
|
||||
QWeb.DIRECTIVES.push(directive);
|
||||
QWeb.DIRECTIVE_NAMES[directive.name] = 1;
|
||||
QWeb.DIRECTIVES.sort((d1, d2) => d1.priority - d2.priority);
|
||||
if (directive.extraNames) {
|
||||
directive.extraNames.forEach(n => (DIRECTIVE_NAMES[n] = 1));
|
||||
directive.extraNames.forEach(n => (QWeb.DIRECTIVE_NAMES[n] = 1));
|
||||
}
|
||||
}
|
||||
|
||||
static register(name: string, Component: any) {
|
||||
if (QWeb.widgets[name]) {
|
||||
static registerComponent(name: string, Component: any) {
|
||||
if (QWeb.components[name]) {
|
||||
throw new Error(`Component '${name}' has already been registered`);
|
||||
}
|
||||
QWeb.widgets[name] = Component;
|
||||
QWeb.components[name] = Component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register globally a template. All QWeb instances will obtain their
|
||||
* templates from their own template map, and then, from the global static
|
||||
* TEMPLATES property.
|
||||
*/
|
||||
static registerTemplate(name: string, template: string) {
|
||||
if (QWeb.TEMPLATES[name]) {
|
||||
throw new Error(`Template '${name}' has already been registered`);
|
||||
}
|
||||
const qweb = new QWeb();
|
||||
qweb.addTemplate(name, template);
|
||||
QWeb.TEMPLATES[name] = qweb.templates[name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a template to the internal template map. Note that it is not
|
||||
* immediately compiled.
|
||||
*/
|
||||
addTemplate(name: string, xmlString: string) {
|
||||
addTemplate(name: string, xmlString: string, allowDuplicate?: boolean) {
|
||||
if (allowDuplicate && name in this.templates) {
|
||||
return;
|
||||
}
|
||||
const doc = parseXML(xmlString);
|
||||
if (!doc.firstChild) {
|
||||
throw new Error("Invalid template (should not be empty)");
|
||||
@@ -178,12 +224,12 @@ export class QWeb {
|
||||
}
|
||||
|
||||
/**
|
||||
* Load templates from a xml (as a string). This will look up for the first
|
||||
* <templates> tag, and will consider each child of this as a template, with
|
||||
* the name given by the t-name attribute.
|
||||
* Load templates from a xml (as a string or xml document). This will look up
|
||||
* for the first <templates> tag, and will consider each child of this as a
|
||||
* template, with the name given by the t-name attribute.
|
||||
*/
|
||||
addTemplates(xmlstr: string) {
|
||||
const doc = parseXML(xmlstr);
|
||||
addTemplates(xmlstr: string | Document) {
|
||||
const doc = typeof xmlstr === "string" ? parseXML(xmlstr) : xmlstr;
|
||||
const templates = doc.getElementsByTagName("templates")[0];
|
||||
if (!templates) {
|
||||
return;
|
||||
@@ -201,7 +247,7 @@ export class QWeb {
|
||||
this._processTemplate(elem);
|
||||
const template = {
|
||||
elem,
|
||||
fn: (context, extra) => {
|
||||
fn: function(this: QWeb, context, extra) {
|
||||
const compiledFunction = this._compile(name, elem);
|
||||
template.fn = compiledFunction;
|
||||
return compiledFunction.call(this, context, extra);
|
||||
@@ -232,9 +278,7 @@ export class QWeb {
|
||||
return a + b;
|
||||
}) > 1
|
||||
) {
|
||||
throw new Error(
|
||||
"Only one conditional branching directive is allowed per node"
|
||||
);
|
||||
throw new Error("Only one conditional branching directive is allowed per node");
|
||||
}
|
||||
// All text nodes between branch nodes are removed
|
||||
let textNode;
|
||||
@@ -264,50 +308,98 @@ export class QWeb {
|
||||
return template.fn.call(this, context, extra);
|
||||
}
|
||||
|
||||
_compile(name: string, elem: Element): CompiledTemplate {
|
||||
/**
|
||||
* Render a template to a html string.
|
||||
*
|
||||
* Note that this is more limited than the `render` method: it is not suitable
|
||||
* to render a full component tree, since this is an asynchronous operation.
|
||||
* This method can only render templates without components.
|
||||
*/
|
||||
renderToString(name: string, context: EvalContext = {}, extra?: any): string {
|
||||
const vnode = this.render(name, context, extra);
|
||||
if (vnode.sel === undefined) {
|
||||
return vnode.text!;
|
||||
}
|
||||
const node = document.createElement(vnode.sel);
|
||||
const result = patch(node, vnode);
|
||||
return (<HTMLElement>result.elm).outerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Force all widgets connected to this QWeb instance to rerender themselves.
|
||||
*
|
||||
* This method is mostly useful for external code that want to modify the
|
||||
* application in some cases. For example, a router plugin.
|
||||
*/
|
||||
forceUpdate() {
|
||||
this.isUpdating = true;
|
||||
Promise.resolve().then(() => {
|
||||
if (this.isUpdating) {
|
||||
this.isUpdating = false;
|
||||
this.trigger("update");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_compile(name: string, elem: Element, parentContext?: CompilationContext): CompiledTemplate {
|
||||
const isDebug = elem.attributes.hasOwnProperty("t-debug");
|
||||
const ctx = new Context(name);
|
||||
const ctx = new CompilationContext(name);
|
||||
if (elem.tagName !== "t") {
|
||||
ctx.shouldDefineResult = false;
|
||||
}
|
||||
if (parentContext) {
|
||||
ctx.templates = Object.create(parentContext.templates);
|
||||
ctx.variables = Object.create(parentContext.variables);
|
||||
ctx.nextID = parentContext.nextID + 1;
|
||||
ctx.parentNode = parentContext.parentNode || ctx.nextID++;
|
||||
ctx.allowMultipleRoots = true;
|
||||
ctx.hasParentWidget = true;
|
||||
ctx.shouldDefineResult = false;
|
||||
ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`);
|
||||
|
||||
for (let v in parentContext.variables) {
|
||||
let variable = <any>parentContext.variables[v];
|
||||
if (variable.id) {
|
||||
ctx.addLine(`let ${variable.id} = extra.fiber.vars.${variable.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (parentContext) {
|
||||
ctx.addLine(" Object.assign(context, extra.fiber.scope);");
|
||||
}
|
||||
this._compileNode(elem, ctx);
|
||||
|
||||
if (ctx.shouldProtectContext) {
|
||||
ctx.code.unshift(" context = Object.create(context);");
|
||||
}
|
||||
if (ctx.shouldDefineOwner) {
|
||||
// this is necessary to prevent some directives (t-forach for ex) to
|
||||
// pollute the rendering context by adding some keys in it.
|
||||
ctx.code.unshift(" let owner = context;");
|
||||
}
|
||||
if (ctx.shouldDefineQWeb) {
|
||||
ctx.code.unshift(" let QWeb = this.constructor;");
|
||||
}
|
||||
if (ctx.shouldDefineUtils) {
|
||||
ctx.code.unshift(" let utils = this.utils;");
|
||||
if (!parentContext) {
|
||||
if (ctx.shouldDefineResult) {
|
||||
ctx.addLine(`return result;`);
|
||||
} else {
|
||||
if (!ctx.rootNode) {
|
||||
throw new Error(`A template should have one root node (${ctx.templateName})`);
|
||||
}
|
||||
ctx.addLine(`return vn${ctx.rootNode};`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!ctx.rootNode) {
|
||||
throw new Error("A template should have one root node");
|
||||
}
|
||||
ctx.addLine(`return vn${ctx.rootNode};`);
|
||||
let code = ctx.generateCode();
|
||||
|
||||
let template;
|
||||
try {
|
||||
template = new Function(
|
||||
"context",
|
||||
"extra",
|
||||
ctx.code.join("\n")
|
||||
) as CompiledTemplate;
|
||||
template = new Function("context", "extra", code.join("\n")) as CompiledTemplate;
|
||||
} catch (e) {
|
||||
const templateName = ctx.templateName.replace(/`/g, "'");
|
||||
console.groupCollapsed(`Invalid Code generated by ${templateName}`);
|
||||
console.warn(code.join("\n"));
|
||||
console.groupEnd();
|
||||
throw new Error(
|
||||
`Invalid generated code while compiling template '${ctx.templateName.replace(
|
||||
/`/g,
|
||||
"'"
|
||||
)}': ${e.message}`
|
||||
`Invalid generated code while compiling template '${templateName}': ${e.message}`
|
||||
);
|
||||
}
|
||||
if (isDebug) {
|
||||
console.log(
|
||||
`Template: ${this.templates[name].elem.outerHTML}\nCompiled code:\n` +
|
||||
template.toString()
|
||||
);
|
||||
const tpl = this.templates[name];
|
||||
if (tpl) {
|
||||
const msg = `Template: ${tpl.elem.outerHTML}\nCompiled code:\n${template.toString()}`;
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
return template;
|
||||
}
|
||||
@@ -316,7 +408,7 @@ export class QWeb {
|
||||
* Generate code from an xml node
|
||||
*
|
||||
*/
|
||||
_compileNode(node: ChildNode, ctx: Context) {
|
||||
_compileNode(node: ChildNode, ctx: CompilationContext) {
|
||||
if (!(node instanceof Element)) {
|
||||
// this is a text node, there are no directive to apply
|
||||
let text = node.textContent!;
|
||||
@@ -327,18 +419,31 @@ export class QWeb {
|
||||
text = text.replace(whitespaceRE, " ");
|
||||
}
|
||||
if (ctx.parentNode) {
|
||||
ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`);
|
||||
if (node.nodeType === 3) {
|
||||
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) {
|
||||
ctx.addLine(`vn${ctx.parentTextNode}.text += \`${text}\`;`);
|
||||
} else {
|
||||
// this is an unusual situation: this text node is the result of the
|
||||
// template rendering.
|
||||
let nodeID = ctx.generateID();
|
||||
ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`);
|
||||
ctx.addLine(`result = vn${nodeID};`);
|
||||
ctx.rootContext.rootNode = nodeID;
|
||||
ctx.rootContext.parentNode = nodeID;
|
||||
ctx.rootContext.parentTextNode = nodeID;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const firstLetter = node.tagName[0];
|
||||
if (firstLetter === firstLetter.toUpperCase()) {
|
||||
// this is a component, we modify in place the xml document to change
|
||||
// <SomeComponent ... /> to <t t-component="SomeComponent" ... />
|
||||
node.setAttribute("t-component", node.tagName);
|
||||
}
|
||||
const attributes = (<Element>node).attributes;
|
||||
|
||||
const validDirectives: {
|
||||
@@ -347,36 +452,45 @@ export class QWeb {
|
||||
fullName: string;
|
||||
}[] = [];
|
||||
|
||||
let withHandlers = false;
|
||||
|
||||
// maybe this is not optimal: we iterate on all attributes here, and again
|
||||
// just after for each directive.
|
||||
for (let i = 0; i < attributes.length; i++) {
|
||||
let attrName = attributes[i].name;
|
||||
if (attrName.startsWith("t-")) {
|
||||
let dName = attrName.slice(2).split("-")[0];
|
||||
if (!(dName in DIRECTIVE_NAMES)) {
|
||||
let dName = attrName.slice(2).split(/-|\./)[0];
|
||||
if (!(dName in QWeb.DIRECTIVE_NAMES)) {
|
||||
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 = DIRECTIVES.length;
|
||||
const DIR_N = QWeb.DIRECTIVES.length;
|
||||
const ATTR_N = attributes.length;
|
||||
let withHandlers = false;
|
||||
for (let i = 0; i < DIR_N; i++) {
|
||||
let directive = DIRECTIVES[i];
|
||||
let directive = QWeb.DIRECTIVES[i];
|
||||
let fullName;
|
||||
let value;
|
||||
for (let j = 0; j < ATTR_N; j++) {
|
||||
const name = attributes[j].name;
|
||||
if (
|
||||
name === "t-" + directive.name ||
|
||||
name.startsWith("t-" + directive.name + "-")
|
||||
name.startsWith("t-" + directive.name + "-") ||
|
||||
name.startsWith("t-" + directive.name + ".")
|
||||
) {
|
||||
fullName = name;
|
||||
value = attributes[j].textContent;
|
||||
validDirectives.push({ directive, value, fullName });
|
||||
if (directive.name === "on") {
|
||||
if (directive.name === "on" || directive.name === "model") {
|
||||
withHandlers = true;
|
||||
}
|
||||
}
|
||||
@@ -400,6 +514,7 @@ export class QWeb {
|
||||
if (node.nodeName !== "t") {
|
||||
let nodeID = this._compileGenericNode(node, ctx, withHandlers);
|
||||
ctx = ctx.withParent(nodeID);
|
||||
ctx = ctx.subContext("currentKey", ctx.lastNodeKey);
|
||||
let nodeHooks = {};
|
||||
let addNodeHook = function(hook, handler) {
|
||||
nodeHooks[hook] = nodeHooks[hook] || [];
|
||||
@@ -437,6 +552,17 @@ export class QWeb {
|
||||
}
|
||||
|
||||
this._compileChildren(node, ctx);
|
||||
// svg support
|
||||
// we hadd svg namespace if it is a svg or if it is a g, but only if it is
|
||||
// the root node. This is the easiest way to support svg sub components:
|
||||
// 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 validDirectives) {
|
||||
if (directive.finalize) {
|
||||
@@ -447,7 +573,7 @@ export class QWeb {
|
||||
|
||||
_compileGenericNode(
|
||||
node: ChildNode,
|
||||
ctx: Context,
|
||||
ctx: CompilationContext,
|
||||
withHandlers: boolean = true
|
||||
): number {
|
||||
// nodeType 1 is generic tag
|
||||
@@ -473,66 +599,76 @@ export class QWeb {
|
||||
if (key === "disabled" && DISABLED_TAGS.indexOf(node.nodeName) > -1) {
|
||||
isProp = true;
|
||||
}
|
||||
if (
|
||||
(key === "readonly" && node.nodeName === "input") ||
|
||||
node.nodeName === "textarea"
|
||||
) {
|
||||
if ((key === "readonly" && node.nodeName === "input") || node.nodeName === "textarea") {
|
||||
isProp = true;
|
||||
}
|
||||
if (isProp) {
|
||||
props.push(`${key}: _${val}`);
|
||||
}
|
||||
}
|
||||
let classObj = "";
|
||||
|
||||
for (let i = 0; i < attributes.length; i++) {
|
||||
let name = attributes[i].name;
|
||||
const value = attributes[i].textContent!;
|
||||
|
||||
// regular attributes
|
||||
if (
|
||||
!name.startsWith("t-") &&
|
||||
!(<Element>node).getAttribute("t-attf-" + name)
|
||||
) {
|
||||
if (!name.startsWith("t-") && !(<Element>node).getAttribute("t-attf-" + name)) {
|
||||
const attID = ctx.generateID();
|
||||
ctx.addLine(`var _${attID} = '${value}';`);
|
||||
if (!name.match(/^[a-zA-Z]+$/)) {
|
||||
// attribute contains 'non letters' => we want to quote it
|
||||
name = '"' + name + '"';
|
||||
if (name === "class") {
|
||||
let classDef = value
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.map(a => `'${a}':true`)
|
||||
.join(",");
|
||||
classObj = `_${ctx.generateID()}`;
|
||||
ctx.addLine(`let ${classObj} = {${classDef}};`);
|
||||
} else {
|
||||
ctx.addLine(`var _${attID} = '${value}';`);
|
||||
if (!name.match(/^[a-zA-Z]+$/)) {
|
||||
// attribute contains 'non letters' => we want to quote it
|
||||
name = '"' + name + '"';
|
||||
}
|
||||
attrs.push(`${name}: _${attID}`);
|
||||
handleBooleanProps(name, attID);
|
||||
}
|
||||
attrs.push(`${name}: _${attID}`);
|
||||
handleBooleanProps(name, attID);
|
||||
}
|
||||
|
||||
// dynamic attributes
|
||||
if (name.startsWith("t-att-")) {
|
||||
let attName = name.slice(6);
|
||||
let formattedValue = ctx.formatExpression(ctx.getValue(value!));
|
||||
if (
|
||||
formattedValue[0] === "{" &&
|
||||
formattedValue[formattedValue.length - 1] === "}"
|
||||
) {
|
||||
formattedValue = `this.utils.objectToAttrString(${formattedValue})`;
|
||||
const v = ctx.getValue(value);
|
||||
let formattedValue = v.id || ctx.formatExpression(v);
|
||||
|
||||
if (attName === "class") {
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
formattedValue = `utils.toObj(${formattedValue})`;
|
||||
if (classObj) {
|
||||
ctx.addLine(`Object.assign(${classObj}, ${formattedValue})`);
|
||||
} else {
|
||||
classObj = `_${ctx.generateID()}`;
|
||||
ctx.addLine(`let ${classObj} = ${formattedValue};`);
|
||||
}
|
||||
} else {
|
||||
const attID = ctx.generateID();
|
||||
if (!attName.match(/^[a-zA-Z]+$/)) {
|
||||
// attribute contains 'non letters' => we want to quote it
|
||||
attName = '"' + attName + '"';
|
||||
}
|
||||
// we need to combine dynamic with non dynamic attributes:
|
||||
// class="a" t-att-class="'yop'" should be rendered as class="a yop"
|
||||
const attValue = (<Element>node).getAttribute(attName);
|
||||
if (attValue) {
|
||||
const attValueID = ctx.generateID();
|
||||
ctx.addLine(`var _${attValueID} = ${formattedValue};`);
|
||||
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
|
||||
const attrIndex = attrs.findIndex(att => att.startsWith(attName + ":"));
|
||||
attrs.splice(attrIndex, 1);
|
||||
}
|
||||
ctx.addLine(`var _${attID} = ${formattedValue};`);
|
||||
attrs.push(`${attName}: _${attID}`);
|
||||
handleBooleanProps(attName, attID);
|
||||
}
|
||||
const attID = ctx.generateID();
|
||||
if (!attName.match(/^[a-zA-Z]+$/)) {
|
||||
// attribute contains 'non letters' => we want to quote it
|
||||
attName = '"' + attName + '"';
|
||||
}
|
||||
// we need to combine dynamic with non dynamic attributes:
|
||||
// class="a" t-att-class="'yop'" should be rendered as class="a yop"
|
||||
const attValue = (<Element>node).getAttribute(attName);
|
||||
if (attValue) {
|
||||
const attValueID = ctx.generateID();
|
||||
ctx.addLine(`var _${attValueID} = ${formattedValue};`);
|
||||
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
|
||||
const attrIndex = attrs.findIndex(att =>
|
||||
att.startsWith(attName + ":")
|
||||
);
|
||||
attrs.splice(attrIndex, 1);
|
||||
}
|
||||
ctx.addLine(`var _${attID} = ${formattedValue};`);
|
||||
attrs.push(`${attName}: _${attID}`);
|
||||
handleBooleanProps(attName, attID);
|
||||
}
|
||||
|
||||
if (name.startsWith("t-attf-")) {
|
||||
@@ -562,7 +698,9 @@ export class QWeb {
|
||||
let nodeID = ctx.generateID();
|
||||
let nodeKey: any = (<Element>node).getAttribute("t-key");
|
||||
if (nodeKey) {
|
||||
nodeKey = ctx.formatExpression(nodeKey);
|
||||
ctx.addLine(`const nodeKey${nodeID} = ${ctx.formatExpression(nodeKey)}`);
|
||||
nodeKey = `nodeKey${nodeID}`;
|
||||
ctx.lastNodeKey = nodeKey;
|
||||
} else {
|
||||
nodeKey = nodeID;
|
||||
}
|
||||
@@ -573,6 +711,9 @@ export class QWeb {
|
||||
if (props.length > 0) {
|
||||
parts.push(`props:{${props.join(",")}}`);
|
||||
}
|
||||
if (classObj) {
|
||||
parts.push(`class:${classObj}`);
|
||||
}
|
||||
if (withHandlers) {
|
||||
parts.push(`on:{}`);
|
||||
}
|
||||
@@ -589,9 +730,7 @@ export class QWeb {
|
||||
ctx.addLine(`}`);
|
||||
ctx.closeIf();
|
||||
}
|
||||
ctx.addLine(
|
||||
`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`
|
||||
);
|
||||
ctx.addLine(`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`);
|
||||
if (ctx.parentNode) {
|
||||
ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`);
|
||||
}
|
||||
@@ -599,7 +738,7 @@ export class QWeb {
|
||||
return nodeID;
|
||||
}
|
||||
|
||||
_compileChildren(node: ChildNode, ctx: Context) {
|
||||
_compileChildren(node: ChildNode, ctx: CompilationContext) {
|
||||
if (node.childNodes.length > 0) {
|
||||
for (let child of Array.from(node.childNodes)) {
|
||||
this._compileNode(child, ctx);
|
||||
@@ -607,172 +746,3 @@ export class QWeb {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Compilation Context
|
||||
//------------------------------------------------------------------------------
|
||||
export class Context {
|
||||
nextID: number = 1;
|
||||
code: string[] = [];
|
||||
variables: { [key: string]: any } = {};
|
||||
definedVariables: { [key: string]: string } = {};
|
||||
escaping: boolean = false;
|
||||
parentNode: number | null = null;
|
||||
rootNode: number | null = null;
|
||||
indentLevel: number = 0;
|
||||
rootContext: Context;
|
||||
caller: Element | undefined;
|
||||
shouldDefineOwner: boolean = false;
|
||||
shouldDefineQWeb: boolean = false;
|
||||
shouldDefineUtils: boolean = false;
|
||||
shouldProtectContext: boolean = false;
|
||||
inLoop: boolean = false;
|
||||
inPreTag: boolean = false;
|
||||
templateName: string;
|
||||
|
||||
constructor(name?: string) {
|
||||
this.rootContext = this;
|
||||
this.templateName = name || "noname";
|
||||
this.addLine("var h = this.utils.h;");
|
||||
}
|
||||
|
||||
generateID(): number {
|
||||
const id = this.rootContext.nextID++;
|
||||
return id;
|
||||
}
|
||||
|
||||
withParent(node: number): Context {
|
||||
if (this === this.rootContext && this.parentNode) {
|
||||
throw new Error("A template should not have more than one root node");
|
||||
}
|
||||
if (!this.rootContext.rootNode) {
|
||||
this.rootContext.rootNode = node;
|
||||
}
|
||||
return this.subContext("parentNode", node);
|
||||
}
|
||||
|
||||
subContext(key: keyof Context, value: any): Context {
|
||||
const newContext = Object.create(this);
|
||||
newContext[key] = value;
|
||||
return newContext;
|
||||
}
|
||||
|
||||
indent() {
|
||||
this.indentLevel++;
|
||||
}
|
||||
|
||||
dedent() {
|
||||
this.indentLevel--;
|
||||
}
|
||||
|
||||
addLine(line: string) {
|
||||
const prefix = new Array(this.indentLevel + 2).join(" ");
|
||||
this.code.push(prefix + line);
|
||||
}
|
||||
|
||||
addIf(condition: string) {
|
||||
this.addLine(`if (${condition}) {`);
|
||||
this.indent();
|
||||
}
|
||||
|
||||
addElse() {
|
||||
this.dedent();
|
||||
this.addLine("} else {");
|
||||
this.indent();
|
||||
}
|
||||
|
||||
closeIf() {
|
||||
this.dedent();
|
||||
this.addLine("}");
|
||||
}
|
||||
|
||||
getValue(val: any): any {
|
||||
return val in this.variables ? this.getValue(this.variables[val]) : val;
|
||||
}
|
||||
|
||||
formatExpression(e: string): string {
|
||||
e = e.trim();
|
||||
if (e[0] === "{" && e[e.length - 1] === "}") {
|
||||
const innerExpr = e
|
||||
.slice(1, -1)
|
||||
.split(",")
|
||||
.map(p => {
|
||||
let [key, val] = p.trim().split(":");
|
||||
if (key === "") {
|
||||
return "";
|
||||
}
|
||||
if (!val) {
|
||||
val = key;
|
||||
}
|
||||
return `${key}: ${this.formatExpression(val)}`;
|
||||
})
|
||||
.join(",");
|
||||
return "{" + innerExpr + "}";
|
||||
}
|
||||
|
||||
// Thanks CHM for this code...
|
||||
const chars = e.split("");
|
||||
let instring = "";
|
||||
let invar = "";
|
||||
let invarPos = 0;
|
||||
let r = "";
|
||||
chars.push(" ");
|
||||
for (let i = 0, ilen = chars.length; i < ilen; i++) {
|
||||
let c = chars[i];
|
||||
if (instring.length) {
|
||||
if (c === instring && chars[i - 1] !== "\\") {
|
||||
instring = "";
|
||||
}
|
||||
} else if (c === '"' || c === "'") {
|
||||
instring = c;
|
||||
} else if (c.match(/[a-zA-Z_\$]/) && !invar.length) {
|
||||
invar = c;
|
||||
invarPos = i;
|
||||
continue;
|
||||
} else if (c.match(/\W/) && invar.length) {
|
||||
// TODO: Should check for possible spaces before dot
|
||||
if (chars[invarPos - 1] !== "." && RESERVED_WORDS.indexOf(invar) < 0) {
|
||||
if (!(invar in this.definedVariables)) {
|
||||
invar =
|
||||
WORD_REPLACEMENT[invar] ||
|
||||
(invar in this.variables &&
|
||||
this.formatExpression(this.variables[invar])) ||
|
||||
"context['" + invar + "']";
|
||||
}
|
||||
}
|
||||
r += invar;
|
||||
invar = "";
|
||||
} else if (invar.length) {
|
||||
invar += c;
|
||||
continue;
|
||||
}
|
||||
r += c;
|
||||
}
|
||||
const result = r.slice(0, -1);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform string interpolation on the given string. Note that if the whole
|
||||
* string is an expression, it simply returns it (formatted).
|
||||
* For instance:
|
||||
* 'Hello {{x}}!' -> `Hello ${x}`
|
||||
* '{{x}}' -> x
|
||||
*/
|
||||
interpolate(s: string): string {
|
||||
let matches = s.match(/\{\{.*?\}\}/g);
|
||||
if (matches && matches[0].length === s.length) {
|
||||
return this.formatExpression(s.slice(2, -2));
|
||||
}
|
||||
matches = s.match(/\#\{.*?\}/g);
|
||||
if (matches && matches[0].length === s.length) {
|
||||
return this.formatExpression(s.slice(2, -1));
|
||||
}
|
||||
|
||||
let formatter = expr => "${" + this.formatExpression(expr) + "}";
|
||||
let r = s
|
||||
.replace(/\{\{.*?\}\}/g, s => formatter(s.slice(2, -2)))
|
||||
.replace(/\#\{.*?\}/g, s => formatter(s.slice(2, -1)));
|
||||
return "`" + r + "`";
|
||||
}
|
||||
}
|
||||
@@ -1,556 +0,0 @@
|
||||
import { QWeb, UTILS } from "./qweb_core";
|
||||
|
||||
/**
|
||||
* Owl QWeb Extensions
|
||||
*
|
||||
* This file contains the implementation of non standard QWeb directives, added
|
||||
* by Owl and that will only work on Owl projects:
|
||||
*
|
||||
* - t-on
|
||||
* - t-ref
|
||||
* - t-transition
|
||||
* - t-widget/t-props/t-keepalive
|
||||
* - t-mounted
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-on
|
||||
//------------------------------------------------------------------------------
|
||||
// these are pieces of code that will be injected into the event handler if
|
||||
// modifiers are specified
|
||||
const MODS_CODE = {
|
||||
prevent: "e.preventDefault();",
|
||||
self: "if (e.target !== this.elm) {return}",
|
||||
stop: "e.stopPropagation();"
|
||||
};
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "on",
|
||||
priority: 90,
|
||||
atNodeCreation({ ctx, fullName, value, nodeID }) {
|
||||
ctx.rootContext.shouldDefineOwner = true;
|
||||
const [eventName, ...mods] = fullName.slice(5).split(".");
|
||||
if (!eventName) {
|
||||
throw new Error("Missing event name with t-on directive");
|
||||
}
|
||||
let extraArgs;
|
||||
let handlerName = value.replace(/\(.*\)/, function(args) {
|
||||
extraArgs = args.slice(1, -1);
|
||||
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 handler;
|
||||
if (mods.length > 0) {
|
||||
handler = `function (e) {`;
|
||||
handler += mods
|
||||
.map(function(mod) {
|
||||
return MODS_CODE[mod];
|
||||
})
|
||||
.join("");
|
||||
handler += `context['${handlerName}'].call(${params}, e);}`;
|
||||
} else {
|
||||
handler = `context['${handlerName}'].bind(${params})`;
|
||||
}
|
||||
if (extraArgs) {
|
||||
ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
|
||||
} else {
|
||||
ctx.addLine(
|
||||
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`
|
||||
);
|
||||
ctx.addLine(
|
||||
`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-ref
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "ref",
|
||||
priority: 95,
|
||||
atNodeCreation({ ctx, value, addNodeHook }) {
|
||||
const refKey = `ref${ctx.generateID()}`;
|
||||
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
|
||||
addNodeHook("create", `context.refs[${refKey}] = n.elm;`);
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-transition
|
||||
//------------------------------------------------------------------------------
|
||||
UTILS.nextFrame = function(cb: () => void) {
|
||||
requestAnimationFrame(() => requestAnimationFrame(cb));
|
||||
};
|
||||
|
||||
UTILS.transitionInsert = function(elm: HTMLElement, name: string) {
|
||||
elm.classList.add(name + "-enter");
|
||||
elm.classList.add(name + "-enter-active");
|
||||
const finalize = () => {
|
||||
elm.classList.remove(name + "-enter-active");
|
||||
elm.classList.remove(name + "-enter-to");
|
||||
};
|
||||
this.nextFrame(() => {
|
||||
elm.classList.remove(name + "-enter");
|
||||
elm.classList.add(name + "-enter-to");
|
||||
whenTransitionEnd(elm, finalize);
|
||||
});
|
||||
};
|
||||
|
||||
UTILS.transitionRemove = function(
|
||||
elm: HTMLElement,
|
||||
name: string,
|
||||
rm: () => void
|
||||
) {
|
||||
elm.classList.add(name + "-leave");
|
||||
elm.classList.add(name + "-leave-active");
|
||||
const finalize = () => {
|
||||
elm.classList.remove(name + "-leave-active");
|
||||
elm.classList.remove(name + "-leave-to");
|
||||
rm();
|
||||
};
|
||||
this.nextFrame(() => {
|
||||
elm.classList.remove(name + "-leave");
|
||||
elm.classList.add(name + "-leave-to");
|
||||
whenTransitionEnd(elm, finalize);
|
||||
});
|
||||
};
|
||||
|
||||
function getTimeout(delays: Array<string>, durations: Array<string>): number {
|
||||
/* istanbul ignore next */
|
||||
while (delays.length < durations.length) {
|
||||
delays = delays.concat(delays);
|
||||
}
|
||||
|
||||
return Math.max.apply(
|
||||
null,
|
||||
durations.map((d, i) => {
|
||||
return toMs(d) + toMs(delays[i]);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Old versions of Chromium (below 61.0.3163.100) formats floating pointer numbers
|
||||
// in a locale-dependent way, using a comma instead of a dot.
|
||||
// If comma is not replaced with a dot, the input will be rounded down (i.e. acting
|
||||
// as a floor function) causing unexpected behaviors
|
||||
function toMs(s: string): number {
|
||||
return Number(s.slice(0, -1).replace(",", ".")) * 1000;
|
||||
}
|
||||
|
||||
function whenTransitionEnd(elm: HTMLElement, cb) {
|
||||
const styles = window.getComputedStyle(elm);
|
||||
const delays: Array<string> = (styles.transitionDelay || "").split(", ");
|
||||
const durations: Array<string> = (styles.transitionDuration || "").split(
|
||||
", "
|
||||
);
|
||||
const timeout: number = getTimeout(delays, durations);
|
||||
if (timeout > 0) {
|
||||
elm.addEventListener("transitionend", cb, { once: true });
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "transition",
|
||||
priority: 96,
|
||||
atNodeCreation({ value, addNodeHook }) {
|
||||
let name = value;
|
||||
const hooks = {
|
||||
insert: `this.utils.transitionInsert(vn.elm, '${name}');`,
|
||||
remove: `this.utils.transitionRemove(vn.elm, '${name}', rm);`
|
||||
};
|
||||
for (let hookName in hooks) {
|
||||
addNodeHook(hookName, hooks[hookName]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-widget
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* The t-widget directive is certainly a complicated and hard to maintain piece
|
||||
* of code. To help you, fellow developer, if you have to maintain it, I offer
|
||||
* you this advice: Good luck...
|
||||
*
|
||||
* Since it is not 'direct' code, but rather code that generates other code, it
|
||||
* is not easy to understand. To help you, here is a detailed and commented
|
||||
* explanation of the code generated by the t-widget directive for the following
|
||||
* situation:
|
||||
* ```xml
|
||||
* <t t-widget="child"
|
||||
* t-key="'somestring'"
|
||||
* t-props="{flag:state.flag}"
|
||||
* t-transition="fade"/>
|
||||
* ```
|
||||
*
|
||||
* ```js
|
||||
* // this is the virtual node representing the parent div
|
||||
* let c1 = [], p1 = { key: 1 };
|
||||
* var vn1 = h("div", p1, c1);
|
||||
*
|
||||
* // t-widget directive: we start by evaluating the expression given by t-key:
|
||||
* let key5 = "somestring";
|
||||
*
|
||||
* // We keep the index of the position of the widget in the closure. We push
|
||||
* // null to reserve the slot, and will replace it later by the widget vnode,
|
||||
* // when it will be ready (do not forget that preparing/rendering a widget is
|
||||
* // asynchronous)
|
||||
* let _2_index = c1.length;
|
||||
* c1.push(null);
|
||||
*
|
||||
* // def3 is the deferred that will contain later either the new widget
|
||||
* // creation, or the props update...
|
||||
* let def3;
|
||||
*
|
||||
* // this is kind of tricky: we need here to find if the widget was already
|
||||
* // created by a previous rendering. This is done by checking the internal
|
||||
* // `cmap` (children map) of the parent widget: it maps keys to widget ids,
|
||||
* // and, then, if there is an id, we look into the children list to get the
|
||||
* // instance
|
||||
* let w4 =
|
||||
* key5 in context.__owl__.cmap
|
||||
* ? context.__owl__.children[context.__owl__.cmap[key5]]
|
||||
* : false;
|
||||
*
|
||||
* // we evaluate here the props given to the component. It is done here to be
|
||||
* // able to easily reference it later, and also, it might be an expensive
|
||||
* // computation, so it is certainly better to do it only once
|
||||
* let props4 = { flag: context["state"].flag };
|
||||
*
|
||||
* // If we have a widget, currently rendering, but not ready yet, and which was
|
||||
* // rendered with different props, we do not want to wait for it to be ready,
|
||||
* // then update it. We simply destroy it, and start anew.
|
||||
* if (
|
||||
* w4 &&
|
||||
* w4.__owl__.renderPromise &&
|
||||
* !w4.__owl__.isStarted &&
|
||||
* props4 !== w4.__owl__.renderProps
|
||||
* ) {
|
||||
* w4.destroy();
|
||||
* w4 = false;
|
||||
* }
|
||||
*
|
||||
* if (!w4) {
|
||||
* // in this situation, we need to create a new widget. First step is
|
||||
* // to get a reference to the class, then create an instance with
|
||||
* // current context as parent, and the props.
|
||||
* let W4 = context.widgets["child"];
|
||||
* if (!W4) {
|
||||
* throw new Error("Cannot find the definition of widget 'child'");
|
||||
* }
|
||||
* w4 = new W4(owner, props4);
|
||||
*
|
||||
* let utils = this.utils;
|
||||
*
|
||||
* // Whenever we rerender the parent widget, we need to be sure that we
|
||||
* // are able to find the widget instance. To do that, we register it to
|
||||
* // the parent cmap (children map). Note that the 'template' key is
|
||||
* // used here, since this is what identify the widget from the template
|
||||
* // perspective.
|
||||
* context.__owl__.cmap[key5] = w4.__owl__.id;
|
||||
*
|
||||
* // _prepare is called, to basically call willStart, then render the
|
||||
* // widget
|
||||
* def3 = w4._prepare();
|
||||
*
|
||||
* def3 = def3.then(vnode => {
|
||||
* // we create here a virtual node for the parent (NOT the widget). This
|
||||
* // means that the vdom of the parent will be stopped here, and from
|
||||
* // the parent's perspective, it simply is a vnode with no children.
|
||||
* // However, it shares the same dom element with the component root
|
||||
* // vnode.
|
||||
* let pvnode = h(vnode.sel, { key: key5 });
|
||||
*
|
||||
* // we add hooks to the parent vnode so we can interact with the new
|
||||
* // widget at the proper time
|
||||
* pvnode.data.hook = {
|
||||
* insert(vn) {
|
||||
* // the _mount method will patch the widget vdom into the elm vn.elm,
|
||||
* // then call the mounted hooks. However, suprisingly, the snabbdom
|
||||
* // patch method actually replace the elm by a new elm, so we need
|
||||
* // to synchronise the pvnode elm with the resulting elm
|
||||
* let nvn = w4._mount(vnode, vn.elm);
|
||||
* pvnode.elm = nvn.elm;
|
||||
* // what follows is only present if there are animations on the widget
|
||||
* utils.transitionInsert(vn.elm, "fade");
|
||||
* },
|
||||
* remove() {
|
||||
* // override with empty function to prevent from removing the node
|
||||
* // directly. It will be removed when destroy is called anyway, which
|
||||
* // delays the removal if there are animations.
|
||||
* },
|
||||
* destroy() {
|
||||
* // if there are animations, we delay the call to destroy on the
|
||||
* // widget, if not, we call it directly.
|
||||
* let finalize = () => {
|
||||
* w4.destroy();
|
||||
* };
|
||||
* utils.transitionRemove(vn.elm, "fade", finalize);
|
||||
* }
|
||||
* };
|
||||
* // the pvnode is inserted at the correct position in the div's children
|
||||
* c1[_2_index] = pvnode;
|
||||
*
|
||||
* // we keep here a reference to the parent vnode (representing the
|
||||
* // widget, so we can reuse it later whenever we update the widget
|
||||
* w4.__owl__.pvnode = pvnode;
|
||||
* });
|
||||
* } else {
|
||||
* // this is the 'update' path of the directive.
|
||||
* // the call to _updateProps is the actual widget update
|
||||
* def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
||||
* def3 = def3.then(() => {
|
||||
* // if widget was destroyed in the meantime, we do nothing (so, this
|
||||
* // means that the parent's element children list will have a null in
|
||||
* // the widget's position, which will cause the pvnode to be removed
|
||||
* // when it is patched.
|
||||
* if (w4.__owl__.isDestroyed) {
|
||||
* return;
|
||||
* }
|
||||
* // like above, we register the pvnode to the children list, so it
|
||||
* // will not be patched out of the dom.
|
||||
* let pvnode = w4.__owl__.pvnode;
|
||||
* c1[_2_index] = pvnode;
|
||||
* });
|
||||
* }
|
||||
*
|
||||
* // we register the deferred here so the parent can coordinate its patch operation
|
||||
* // with all the children.
|
||||
* extra.promises.push(def3);
|
||||
* return vn1;
|
||||
* ```
|
||||
*/
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "widget",
|
||||
extraNames: ["props", "keepalive"],
|
||||
priority: 100,
|
||||
atNodeEncounter({ ctx, value, node }): boolean {
|
||||
ctx.addLine("//WIDGET");
|
||||
ctx.rootContext.shouldDefineOwner = true;
|
||||
ctx.rootContext.shouldDefineQWeb = true;
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
let props = node.getAttribute("t-props");
|
||||
let keepAlive = node.getAttribute("t-keepalive") ? true : false;
|
||||
|
||||
// t-on- events and t-transition
|
||||
const events: [string, string][] = [];
|
||||
let transition: string = "";
|
||||
const attributes = (<Element>node).attributes;
|
||||
for (let i = 0; i < attributes.length; i++) {
|
||||
const name = attributes[i].name;
|
||||
if (name.startsWith("t-on-")) {
|
||||
events.push([name.slice(5), attributes[i].textContent!]);
|
||||
} else if (name === "t-transition") {
|
||||
transition = attributes[i].textContent!;
|
||||
}
|
||||
}
|
||||
|
||||
let key = node.getAttribute("t-key");
|
||||
if (key) {
|
||||
key = ctx.formatExpression(key);
|
||||
}
|
||||
if (props) {
|
||||
props = ctx.formatExpression(props);
|
||||
}
|
||||
let dummyID = ctx.generateID();
|
||||
let defID = ctx.generateID();
|
||||
let widgetID = 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};`);
|
||||
}
|
||||
ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`);
|
||||
ctx.addLine(`c${ctx.parentNode}.push(null);`);
|
||||
ctx.addLine(`let def${defID};`);
|
||||
let templateID = key
|
||||
? `key${keyID}`
|
||||
: ctx.inLoop
|
||||
? `String(-${widgetID} - i)`
|
||||
: String(widgetID);
|
||||
|
||||
let ref = node.getAttribute("t-ref");
|
||||
let refExpr = "";
|
||||
let refKey: string = "";
|
||||
if (ref) {
|
||||
refKey = `ref${ctx.generateID()}`;
|
||||
ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`);
|
||||
refExpr = `context.refs[${refKey}] = w${widgetID};`;
|
||||
}
|
||||
let transitionsInsertCode = "";
|
||||
if (transition) {
|
||||
transitionsInsertCode = `utils.transitionInsert(vn.elm, '${transition}');`;
|
||||
}
|
||||
let finalizeWidgetCode = `w${widgetID}.${
|
||||
keepAlive ? "unmount" : "destroy"
|
||||
}();`;
|
||||
if (ref) {
|
||||
finalizeWidgetCode += `delete context.refs[${refKey}];`; // FIXME: shouldn't we keep ref if keepAlive is true?
|
||||
}
|
||||
if (transition) {
|
||||
finalizeWidgetCode = `let finalize = () => {
|
||||
${finalizeWidgetCode}
|
||||
};
|
||||
utils.transitionRemove(vn.elm, '${transition}', finalize);`;
|
||||
}
|
||||
|
||||
let createHook = "";
|
||||
let classAttr = node.getAttribute("class");
|
||||
let tattClass = node.getAttribute("t-att-class");
|
||||
let styleAttr = node.getAttribute("style");
|
||||
let tattStyle = node.getAttribute("t-att-style");
|
||||
if (tattStyle) {
|
||||
const attVar = `_${ctx.generateID()}`;
|
||||
ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattStyle)};`);
|
||||
tattStyle = attVar;
|
||||
}
|
||||
let updateClassCode = "";
|
||||
if (classAttr || tattClass || styleAttr || tattStyle) {
|
||||
let classCode = "";
|
||||
if (classAttr) {
|
||||
classCode =
|
||||
classAttr
|
||||
.split(" ")
|
||||
.map(c => `vn.elm.classList.add('${c}')`)
|
||||
.join(";") + ";";
|
||||
}
|
||||
if (tattClass) {
|
||||
const attVar = `_${ctx.generateID()}`;
|
||||
ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattClass)};`);
|
||||
classCode = `for (let k in ${attVar}) {
|
||||
if (${attVar}[k]) {
|
||||
vn.elm.classList.add(k);
|
||||
}
|
||||
}`;
|
||||
updateClassCode = `let cl=w${widgetID}.el.classList;for (let k in ${attVar}) {if (${attVar}[k]) {cl.add(k)} else {cl.remove(k)}}`;
|
||||
}
|
||||
const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false);
|
||||
const styleCode = styleExpr ? `vn.elm.style = ${styleExpr}` : "";
|
||||
createHook = `vnode.data.hook = {create(_, vn){${classCode}${styleCode}}};`;
|
||||
}
|
||||
|
||||
ctx.addLine(
|
||||
`let w${widgetID} = ${templateID} in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[${templateID}]] : false;`
|
||||
);
|
||||
ctx.addLine(`let props${widgetID} = ${props || "{}"};`);
|
||||
ctx.addIf(
|
||||
`w${widgetID} && w${widgetID}.__owl__.renderPromise && !w${widgetID}.__owl__.vnode && props${widgetID} !== w${widgetID}.__owl__.renderProps`
|
||||
);
|
||||
ctx.addLine(`w${widgetID}.destroy();`);
|
||||
ctx.addLine(`w${widgetID} = false`);
|
||||
ctx.closeIf();
|
||||
|
||||
ctx.addIf(`!w${widgetID}`);
|
||||
// new widget
|
||||
ctx.addLine(`let widgetKey${widgetID} = ${ctx.interpolate(value)};`);
|
||||
ctx.addLine(
|
||||
`let W${widgetID} = context.widgets && context.widgets[widgetKey${widgetID}] || QWeb.widgets[widgetKey${widgetID}];`
|
||||
);
|
||||
|
||||
// maybe only do this in dev mode...
|
||||
ctx.addLine(
|
||||
`if (!W${widgetID}) {throw new Error('Cannot find the definition of widget "' + widgetKey${widgetID} + '"')}`
|
||||
);
|
||||
ctx.addLine(`w${widgetID} = new W${widgetID}(owner, props${widgetID});`);
|
||||
ctx.addLine(
|
||||
`context.__owl__.cmap[${templateID}] = w${widgetID}.__owl__.id;`
|
||||
);
|
||||
for (let [event, method] of events) {
|
||||
ctx.addLine(`w${widgetID}.on('${event}', owner, owner['${method}'])`);
|
||||
}
|
||||
ctx.addLine(`def${defID} = w${widgetID}._prepare();`);
|
||||
// hack: specify empty remove hook to prevent the node from being removed from the DOM
|
||||
// FIXME: click to re-add widget during remove transition -> leak
|
||||
ctx.addLine(
|
||||
`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${widgetID}._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeWidgetCode}}}});c${
|
||||
ctx.parentNode
|
||||
}[_${dummyID}_index]=pvnode;w${widgetID}.__owl__.pvnode = pvnode;});`
|
||||
);
|
||||
|
||||
ctx.addElse();
|
||||
// need to update widget
|
||||
ctx.addLine(
|
||||
`def${defID} = w${widgetID}._updateProps(props${widgetID}, extra.forceUpdate, extra.patchQueue);`
|
||||
);
|
||||
let keepAliveCode = "";
|
||||
if (keepAlive) {
|
||||
keepAliveCode = `pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w${widgetID}.el,vn.elm);vn.elm=w${widgetID}.el;w${widgetID}._remount();};`;
|
||||
}
|
||||
ctx.addLine(
|
||||
`def${defID} = def${defID}.then(()=>{if (w${widgetID}.__owl__.isDestroyed) {return};${
|
||||
tattStyle ? `w${widgetID}.el.style=${tattStyle};` : ""
|
||||
}${updateClassCode}let pvnode=w${widgetID}.__owl__.pvnode;${keepAliveCode}c${
|
||||
ctx.parentNode
|
||||
}[_${dummyID}_index]=pvnode;});`
|
||||
);
|
||||
ctx.closeIf();
|
||||
|
||||
ctx.addLine(`extra.promises.push(def${defID});`);
|
||||
|
||||
if (
|
||||
node.hasAttribute("t-if") ||
|
||||
node.hasAttribute("t-else") ||
|
||||
node.hasAttribute("t-elif")
|
||||
) {
|
||||
ctx.closeIf();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// 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}](); }`
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Component } from "../component/component";
|
||||
import { xml } from "../tags";
|
||||
import { Destination, RouterEnv } from "./router";
|
||||
|
||||
type Props = Destination;
|
||||
|
||||
export class Link<Env extends RouterEnv> extends Component<Env, Props> {
|
||||
static template = xml`
|
||||
<a t-att-class="{'router-link-active': isActive }"
|
||||
t-att-href="href"
|
||||
t-on-click="navigate">
|
||||
<t t-slot="default"/>
|
||||
</a>
|
||||
`;
|
||||
|
||||
href: string = this.env.router.destToPath(this.props);
|
||||
|
||||
async willUpdateProps(nextProps) {
|
||||
this.href = this.env.router.destToPath(nextProps);
|
||||
}
|
||||
|
||||
get isActive() {
|
||||
if (this.env.router.mode === "hash") {
|
||||
return (<any>document.location).hash === this.href;
|
||||
}
|
||||
return (<any>document.location).pathname === this.href;
|
||||
}
|
||||
|
||||
navigate(ev) {
|
||||
// don't redirect with control keys
|
||||
if (ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey) {
|
||||
return;
|
||||
}
|
||||
// don't redirect on right click
|
||||
if (ev.button !== undefined && ev.button !== 0) {
|
||||
return;
|
||||
}
|
||||
// don't redirect if `target="_blank"`
|
||||
if (ev.currentTarget && ev.currentTarget.getAttribute) {
|
||||
const target = ev.currentTarget.getAttribute("target");
|
||||
if (/\b_blank\b/i.test(target)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
ev.preventDefault();
|
||||
this.env.router.navigate(this.props);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
import { Env } from "../component/component";
|
||||
import { QWeb } from "../qweb/index";
|
||||
import { shallowEqual } from "../utils";
|
||||
|
||||
type NavigationGuard = (info: {
|
||||
env: Env;
|
||||
to: Route | null;
|
||||
from: Route | null;
|
||||
}) => boolean | Destination;
|
||||
|
||||
export interface Route {
|
||||
name: string;
|
||||
path: string;
|
||||
component?: any;
|
||||
redirect?: Destination;
|
||||
params: string[];
|
||||
beforeRouteEnter?: NavigationGuard;
|
||||
}
|
||||
|
||||
export type RouteParams = { [key: string]: string | number };
|
||||
|
||||
export interface RouterEnv extends Env {
|
||||
router: Router;
|
||||
}
|
||||
|
||||
export interface Destination {
|
||||
path?: string;
|
||||
to?: string;
|
||||
params?: RouteParams;
|
||||
}
|
||||
|
||||
interface PositiveMatchResult {
|
||||
type: "match";
|
||||
route: Route;
|
||||
params: RouteParams;
|
||||
}
|
||||
|
||||
interface NegativeMatchResult {
|
||||
type: "nomatch";
|
||||
}
|
||||
|
||||
interface CancelledMatch {
|
||||
type: "cancelled";
|
||||
}
|
||||
|
||||
type MatchResult = PositiveMatchResult | NegativeMatchResult | CancelledMatch;
|
||||
|
||||
interface Options {
|
||||
mode: Router["mode"];
|
||||
}
|
||||
|
||||
const paramRegexp = /\{\{(.*?)\}\}/;
|
||||
|
||||
export class Router {
|
||||
currentRoute: Route | null = null;
|
||||
currentParams: RouteParams | null = null;
|
||||
mode: "history" | "hash";
|
||||
|
||||
routes: { [id: string]: Route };
|
||||
routeIds: string[];
|
||||
env: RouterEnv;
|
||||
|
||||
constructor(env: Env, routes: Partial<Route>[], options: Options = { mode: "history" }) {
|
||||
env.router = this;
|
||||
this.mode = options.mode;
|
||||
this.env = env as RouterEnv;
|
||||
|
||||
this.routes = {};
|
||||
this.routeIds = [];
|
||||
let nextId = 1;
|
||||
for (let partialRoute of routes) {
|
||||
if (!partialRoute.name) {
|
||||
partialRoute.name = "__route__" + nextId++;
|
||||
}
|
||||
if (partialRoute.component) {
|
||||
QWeb.registerComponent("__component__" + partialRoute.name, partialRoute.component);
|
||||
}
|
||||
if (partialRoute.redirect) {
|
||||
this.validateDestination(partialRoute.redirect);
|
||||
}
|
||||
partialRoute.params = partialRoute.path ? findParams(partialRoute.path) : [];
|
||||
this.routes[partialRoute.name] = partialRoute as Route;
|
||||
this.routeIds.push(partialRoute.name);
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public API
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
async start() {
|
||||
(this as any)._listener = ev => this._navigate(this.currentPath(), ev);
|
||||
window.addEventListener("popstate", (this as any)._listener);
|
||||
if (this.mode === "hash") {
|
||||
window.addEventListener("hashchange", (this as any)._listener);
|
||||
}
|
||||
const result = await this.matchAndApplyRules(this.currentPath());
|
||||
if (result.type === "match") {
|
||||
this.currentRoute = result.route;
|
||||
this.currentParams = result.params;
|
||||
const currentPath = this.routeToPath(result.route, result.params);
|
||||
if (currentPath !== this.currentPath()) {
|
||||
this.setUrlFromPath(currentPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async navigate(to: Destination): Promise<boolean> {
|
||||
const path = this.destToPath(to);
|
||||
return this._navigate(path);
|
||||
}
|
||||
async _navigate(path: string, ev?: any): Promise<boolean> {
|
||||
const initialName = this.currentRouteName;
|
||||
const initialParams = this.currentParams;
|
||||
const result = await this.matchAndApplyRules(path);
|
||||
if (result.type === "match") {
|
||||
const finalPath = this.routeToPath(result.route, result.params);
|
||||
const isPopStateEvent = ev && ev instanceof PopStateEvent;
|
||||
if (!isPopStateEvent) {
|
||||
this.setUrlFromPath(finalPath);
|
||||
}
|
||||
this.currentRoute = result.route;
|
||||
this.currentParams = result.params;
|
||||
} else if (result.type === "nomatch") {
|
||||
this.currentRoute = null;
|
||||
this.currentParams = null;
|
||||
}
|
||||
const didChange =
|
||||
this.currentRouteName !== initialName || !shallowEqual(this.currentParams, initialParams);
|
||||
if (didChange) {
|
||||
this.env.qweb.forceUpdate();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
destToPath(dest: Destination): string {
|
||||
this.validateDestination(dest);
|
||||
return dest.path || this.routeToPath(this.routes[dest.to!], dest.params!);
|
||||
}
|
||||
|
||||
get currentRouteName(): string | null {
|
||||
return this.currentRoute && this.currentRoute.name;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private helpers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
private setUrlFromPath(path: string) {
|
||||
const separator = this.mode === "hash" ? "/" : "";
|
||||
const url = location.origin + separator + path;
|
||||
if (url !== window.location.href) {
|
||||
window.history.pushState({}, path, url);
|
||||
}
|
||||
}
|
||||
|
||||
private validateDestination(dest: Destination) {
|
||||
if ((!dest.path && !dest.to) || (dest.path && dest.to)) {
|
||||
throw new Error(`Invalid destination: ${JSON.stringify(dest)}`);
|
||||
}
|
||||
}
|
||||
|
||||
private routeToPath(route: Route, params: RouteParams): string {
|
||||
const path = route.path;
|
||||
const parts = path.split("/");
|
||||
const l = parts.length;
|
||||
for (let i = 0; i < l; i++) {
|
||||
const part = parts[i];
|
||||
const match = part.match(paramRegexp);
|
||||
if (match) {
|
||||
const key = match[1].split(".")[0];
|
||||
parts[i] = <string>params[key];
|
||||
}
|
||||
}
|
||||
const prefix = this.mode === "hash" ? "#" : "";
|
||||
return prefix + parts.join("/");
|
||||
}
|
||||
|
||||
private currentPath(): string {
|
||||
let result = this.mode === "history" ? window.location.pathname : window.location.hash.slice(1);
|
||||
return result || "/";
|
||||
}
|
||||
|
||||
private match(path: string): MatchResult {
|
||||
for (let routeId of this.routeIds) {
|
||||
let route = this.routes[routeId];
|
||||
let params = this.getRouteParams(route, path);
|
||||
if (params) {
|
||||
return {
|
||||
type: "match",
|
||||
route: route,
|
||||
params: params
|
||||
};
|
||||
}
|
||||
}
|
||||
return { type: "nomatch" };
|
||||
}
|
||||
|
||||
private async matchAndApplyRules(path: string): Promise<MatchResult> {
|
||||
const result = this.match(path);
|
||||
if (result.type === "match") {
|
||||
return this.applyRules(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private async applyRules(matchResult: PositiveMatchResult): Promise<MatchResult> {
|
||||
const route = matchResult.route;
|
||||
if (route.redirect) {
|
||||
const path = this.destToPath(route.redirect);
|
||||
return this.matchAndApplyRules(path);
|
||||
}
|
||||
if (route.beforeRouteEnter) {
|
||||
const result = await route.beforeRouteEnter({
|
||||
env: this.env,
|
||||
from: this.currentRoute,
|
||||
to: route
|
||||
});
|
||||
if (result === false) {
|
||||
return { type: "cancelled" };
|
||||
} else if (result !== true) {
|
||||
// we want to navigate to another destination
|
||||
const path = this.destToPath(result);
|
||||
return this.matchAndApplyRules(path);
|
||||
}
|
||||
}
|
||||
|
||||
return matchResult;
|
||||
}
|
||||
|
||||
private getRouteParams(route: Route, path: string): RouteParams | false {
|
||||
if (route.path === "*") {
|
||||
return {};
|
||||
}
|
||||
if (path.startsWith("#")) {
|
||||
path = path.slice(1);
|
||||
}
|
||||
const descrParts = route.path.split("/");
|
||||
const targetParts = path.split("/");
|
||||
const l = descrParts.length;
|
||||
if (l !== targetParts.length) {
|
||||
return false;
|
||||
}
|
||||
const result = {};
|
||||
for (let i = 0; i < l; i++) {
|
||||
const descr = descrParts[i];
|
||||
let target: string | number = targetParts[i];
|
||||
const match = descr.match(paramRegexp);
|
||||
if (match) {
|
||||
const [key, suffix] = match[1].split(".");
|
||||
if (suffix === "number") {
|
||||
target = parseInt(target, 10);
|
||||
}
|
||||
result[key] = target;
|
||||
} else if (descr !== target) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
function findParams(str: string): string[] {
|
||||
const globalParamRegexp = /\{\{(.*?)\}\}/g;
|
||||
const result: string[] = [];
|
||||
let m;
|
||||
do {
|
||||
m = globalParamRegexp.exec(str);
|
||||
if (m) {
|
||||
result.push(m[1].split(".")[0]);
|
||||
}
|
||||
} while (m);
|
||||
return result;
|
||||
}
|
||||
+85
-226
@@ -1,17 +1,20 @@
|
||||
import { Component, Env } from "./component";
|
||||
import { EventBus } from "./event_bus";
|
||||
import { Observer } from "./observer";
|
||||
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
|
||||
* - a connect function
|
||||
* - 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/mutations, and for each state changes,
|
||||
* 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
|
||||
@@ -21,263 +24,119 @@ import { Observer } from "./observer";
|
||||
// Store Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
type Mutation = ({ state, commit, set, getters }, payload: any) => void;
|
||||
type Action = ({ commit, state, dispatch, env, getters }, payload: any) => void;
|
||||
type Getter = ({ state, getters }, payload) => any;
|
||||
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 };
|
||||
mutations?: { [name: string]: Mutation };
|
||||
}
|
||||
|
||||
interface StoreOption {
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
export class Store extends EventBus {
|
||||
state: any;
|
||||
export class Store extends Context {
|
||||
actions: any;
|
||||
mutations: any;
|
||||
_commitLevel: number = 0;
|
||||
history: any[] = [];
|
||||
debug: boolean;
|
||||
env: any;
|
||||
observer: Observer;
|
||||
set: any;
|
||||
getters: { [name: string]: (payload?) => any };
|
||||
updateFunctions: { [key: number]: (() => boolean)[] };
|
||||
|
||||
constructor(config: StoreConfig, options: StoreOption = {}) {
|
||||
super();
|
||||
this.debug = options.debug || false;
|
||||
this.state = config.state || {};
|
||||
constructor(config: StoreConfig) {
|
||||
super(config.state);
|
||||
this.actions = config.actions;
|
||||
this.mutations = config.mutations;
|
||||
this.env = config.env;
|
||||
this.observer = new Observer();
|
||||
this.observer.notifyCB = this.trigger.bind(this, "update");
|
||||
this.observer.allowMutations = false;
|
||||
this.observer.observe(this.state);
|
||||
this.getters = {};
|
||||
|
||||
if (this.debug) {
|
||||
this.history.push({ state: this.state });
|
||||
}
|
||||
this.set = this.observer.set.bind(this.observer);
|
||||
|
||||
for (let entry of Object.entries(config.getters || {})) {
|
||||
const name: string = entry[0];
|
||||
const func: (...any) => any = entry[1];
|
||||
this.getters[name] = payload => {
|
||||
return func({ state: this.state, getters: this.getters }, payload);
|
||||
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 {
|
||||
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](
|
||||
{
|
||||
commit: this.commit.bind(this),
|
||||
dispatch: this.dispatch.bind(this),
|
||||
env: this.env,
|
||||
state: this.state,
|
||||
getters: this.getters
|
||||
},
|
||||
payload
|
||||
...payload
|
||||
);
|
||||
if (result instanceof Promise) {
|
||||
return new Promise((resolve, reject) => {
|
||||
result.then(() => resolve());
|
||||
result.catch(reject);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
interface SelectorOptions {
|
||||
store?: Store;
|
||||
isEqual?: (a: any, b: any) => boolean;
|
||||
}
|
||||
|
||||
const isStrictEqual = (a, b) => a === b;
|
||||
|
||||
export function useStore(selector, options: SelectorOptions = {}): any {
|
||||
const component: Component<any, any> = Component.current!;
|
||||
const store = options.store || (component.env.store as Store);
|
||||
let result = selector(store.state, component.props);
|
||||
const hashFn = store.observer.deepRevNumber.bind(store.observer);
|
||||
let revNumber = hashFn(result) || result;
|
||||
const isEqual = options.isEqual || isStrictEqual;
|
||||
if (!store.updateFunctions[component.__owl__.id]) {
|
||||
store.updateFunctions[component.__owl__.id] = [];
|
||||
}
|
||||
const updateFunctions = store.updateFunctions[component.__owl__.id];
|
||||
updateFunctions.push(function(): boolean {
|
||||
const oldResult = result;
|
||||
result = selector(store!.state, component.props);
|
||||
const newRevNumber = hashFn(result);
|
||||
if (
|
||||
(newRevNumber > 0 && revNumber !== newRevNumber) ||
|
||||
(newRevNumber === 0 && !isEqual(oldResult, result))
|
||||
) {
|
||||
revNumber = newRevNumber;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
|
||||
commit(type: string, payload?: any): any {
|
||||
if (!this.mutations[type]) {
|
||||
throw new Error(`[Error] mutation ${type} is undefined`);
|
||||
useContextWithCB(store, component, function(): Promise<void> | void {
|
||||
let shouldRender = false;
|
||||
updateFunctions.forEach(function (updateFn) {
|
||||
shouldRender = updateFn() || shouldRender;
|
||||
});
|
||||
if (shouldRender) {
|
||||
return component.render();
|
||||
}
|
||||
this._commitLevel++;
|
||||
this.observer.allowMutations = true;
|
||||
|
||||
const res = this.mutations[type].call(
|
||||
null,
|
||||
{
|
||||
commit: this.commit.bind(this),
|
||||
state: this.state,
|
||||
set: this.set,
|
||||
getters: this.getters
|
||||
},
|
||||
payload
|
||||
);
|
||||
|
||||
if (this._commitLevel === 1) {
|
||||
this.observer.allowMutations = false;
|
||||
if (this.debug) {
|
||||
this.history.push({
|
||||
state: this.state,
|
||||
mutation: type,
|
||||
payload: payload
|
||||
});
|
||||
}
|
||||
});
|
||||
onWillUpdateProps(props => {
|
||||
// FIXME: only do that if not keepalive + do it in destroy in that case
|
||||
delete store.updateFunctions[component.__owl__.id];
|
||||
result = selector(store.state, props);
|
||||
});
|
||||
return new Proxy(result, {
|
||||
get(target, k) {
|
||||
return result[k];
|
||||
},
|
||||
set(target, k, v) {
|
||||
result[k] = v;
|
||||
return true;
|
||||
}
|
||||
this._commitLevel--;
|
||||
return res;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Connect function
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
function revNumber<T extends Object>(o: T): number {
|
||||
if (o !== null && typeof o === "object" && (<any>o).__owl__) {
|
||||
return (<any>o).__owl__.rev;
|
||||
}
|
||||
return 0;
|
||||
export function useDispatch(store?: Store): Store["dispatch"] {
|
||||
store = store || (Component.current!.env.store as Store);
|
||||
return store.dispatch.bind(store);
|
||||
}
|
||||
|
||||
function deepRevNumber<T extends Object>(o: T): number {
|
||||
if (o !== null && typeof o === "object" && (<any>o).__owl__) {
|
||||
return (<any>o).__owl__.deepRev;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
type Constructor<T> = new (...args: any[]) => T;
|
||||
interface EnvWithStore extends Env {
|
||||
store: Store;
|
||||
}
|
||||
|
||||
let nextID = 1;
|
||||
|
||||
export function connect(mapStateToProps, options: any = {}) {
|
||||
let hashFunction = options.hashFunction || null;
|
||||
|
||||
if (!hashFunction) {
|
||||
let deep = "deep" in options ? options.deep : true;
|
||||
let defaultRevFunction = deep ? deepRevNumber : revNumber;
|
||||
hashFunction = function({ storeProps }, options) {
|
||||
const { currentStoreProps } = options;
|
||||
if ("__owl__" in storeProps) {
|
||||
return defaultRevFunction(storeProps);
|
||||
}
|
||||
let hash = 0;
|
||||
for (let key in storeProps) {
|
||||
const val = storeProps[key];
|
||||
const hashVal = defaultRevFunction(val);
|
||||
if (hashVal === 0) {
|
||||
if (val !== currentStoreProps[key]) {
|
||||
options.didChange = true;
|
||||
}
|
||||
} else {
|
||||
hash += hashVal;
|
||||
}
|
||||
}
|
||||
return hash;
|
||||
};
|
||||
}
|
||||
|
||||
return function<E extends EnvWithStore, P, S>(
|
||||
Comp: Constructor<Component<E, P, S>>
|
||||
) {
|
||||
const Result = class extends Comp {
|
||||
constructor(parent, props?: any) {
|
||||
const env = parent instanceof Component ? parent.env : parent;
|
||||
const ownProps = Object.assign({}, props || {});
|
||||
const storeProps = mapStateToProps(
|
||||
env.store.state,
|
||||
ownProps,
|
||||
env.store.getters
|
||||
);
|
||||
const mergedProps = Object.assign({}, props || {}, storeProps);
|
||||
super(parent, mergedProps);
|
||||
(<any>this.__owl__).ownProps = ownProps;
|
||||
(<any>this.__owl__).currentStoreProps = storeProps;
|
||||
(<any>this.__owl__).storeHash = hashFunction(
|
||||
{
|
||||
state: env.store.state,
|
||||
storeProps: storeProps,
|
||||
revNumber,
|
||||
deepRevNumber
|
||||
},
|
||||
{
|
||||
currentStoreProps: storeProps
|
||||
}
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 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.env.store.on("update", this, this._checkUpdate);
|
||||
super._callMounted();
|
||||
}
|
||||
willUnmount() {
|
||||
this.env.store.off("update", this);
|
||||
super.willUnmount();
|
||||
}
|
||||
|
||||
_checkUpdate() {
|
||||
const ownProps = (<any>this.__owl__).ownProps;
|
||||
const storeProps = mapStateToProps(
|
||||
this.env.store.state,
|
||||
ownProps,
|
||||
this.env.store.getters
|
||||
);
|
||||
const options: any = {
|
||||
currentStoreProps: (<any>this.__owl__).currentStoreProps
|
||||
};
|
||||
const storeHash = hashFunction(
|
||||
{
|
||||
state: this.env.store.state,
|
||||
storeProps: storeProps,
|
||||
revNumber,
|
||||
deepRevNumber
|
||||
},
|
||||
options
|
||||
);
|
||||
let didChange = options.didChange;
|
||||
if (storeHash !== (<any>this.__owl__).storeHash) {
|
||||
didChange = true;
|
||||
(<any>this.__owl__).storeHash = storeHash;
|
||||
}
|
||||
if (didChange) {
|
||||
(<any>this.__owl__).currentStoreProps = storeProps;
|
||||
this._updateProps(ownProps, false);
|
||||
}
|
||||
}
|
||||
_updateProps(nextProps, forceUpdate, patchQueue?: any[]) {
|
||||
if ((<any>this.__owl__).ownProps !== nextProps) {
|
||||
(<any>this.__owl__).currentStoreProps = mapStateToProps(
|
||||
this.env.store.state,
|
||||
nextProps,
|
||||
this.env.store.getters
|
||||
);
|
||||
}
|
||||
(<any>this.__owl__).ownProps = nextProps;
|
||||
const mergedProps = Object.assign(
|
||||
{},
|
||||
nextProps,
|
||||
(<any>this.__owl__).currentStoreProps
|
||||
);
|
||||
return super._updateProps(mergedProps, forceUpdate, patchQueue);
|
||||
}
|
||||
};
|
||||
|
||||
// we assign here a unique name to the resulting anonymous class.
|
||||
// this is necessary for Owl to be able to properly deduce templates.
|
||||
// Otherwise, all connected components would have the same name, and then
|
||||
// each component after the first will necessarily have the same template.
|
||||
let name = `ConnectedComponent${nextID++}`;
|
||||
Object.defineProperty(Result, "name", { value: name });
|
||||
return Result;
|
||||
};
|
||||
export function useGetters(store?: Store): Store["getters"] {
|
||||
store = store || (Component.current!.env.store as Store);
|
||||
return store.getters;
|
||||
}
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { QWeb } from "./qweb/index";
|
||||
|
||||
/**
|
||||
* Owl Tags
|
||||
*
|
||||
* We have here a (very) small collection of tag functions:
|
||||
*
|
||||
* - xml
|
||||
*
|
||||
* 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
|
||||
* an inline template with just the template xml:
|
||||
* ```js
|
||||
* class A extends Component {
|
||||
* static template = xml`<div>some template</div>`;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function xml(strings, ...args) {
|
||||
const name = `__template__${QWeb.nextId++}`;
|
||||
const value = String.raw(strings, ...args);
|
||||
QWeb.registerTemplate(name, value);
|
||||
return name;
|
||||
}
|
||||
+19
-14
@@ -10,12 +10,14 @@
|
||||
* - debounce
|
||||
*/
|
||||
|
||||
export function whenReady(fn) {
|
||||
if (document.readyState === "complete") {
|
||||
fn();
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", fn, false);
|
||||
}
|
||||
export function whenReady(fn?: any) {
|
||||
return new Promise(function(resolve) {
|
||||
if (document.readyState !== "loading") {
|
||||
resolve();
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", resolve, false);
|
||||
}
|
||||
}).then(fn || function() {});
|
||||
}
|
||||
|
||||
const loadedScripts: { [key: string]: Promise<void> } = {};
|
||||
@@ -46,9 +48,7 @@ export async function loadTemplates(url: string): Promise<string> {
|
||||
if (!result.ok) {
|
||||
throw new Error("Error while fetching xml templates");
|
||||
}
|
||||
let templates = await result.text();
|
||||
templates = templates.replace(/<!--[\s\S]*?-->/g, "");
|
||||
return templates;
|
||||
return await result.text();
|
||||
}
|
||||
|
||||
export function escape(str: string | number | undefined): string {
|
||||
@@ -74,11 +74,7 @@ export function escape(str: string | number | undefined): string {
|
||||
*
|
||||
* Inspired by https://davidwalsh.name/javascript-debounce-function
|
||||
*/
|
||||
export function debounce(
|
||||
func: Function,
|
||||
wait: number,
|
||||
immediate?: boolean
|
||||
): Function {
|
||||
export function debounce(func: Function, wait: number, immediate?: boolean): Function {
|
||||
let timeout;
|
||||
return function(this: any) {
|
||||
const context = this;
|
||||
@@ -97,3 +93,12 @@ export function debounce(
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function shallowEqual(p1, p2): boolean {
|
||||
for (let k in p1) {
|
||||
if (p1[k] !== p2[k]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
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[] = [];
|
||||
if (node.hasChildNodes) {
|
||||
for (let c of node.childNodes) {
|
||||
children.push(htmlToVNode(c));
|
||||
}
|
||||
}
|
||||
return h((node as Element).tagName, { attrs }, children);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { attrsModule, classModule, eventListenersModule, propsModule } from "./modules";
|
||||
import { init } from "./vdom";
|
||||
//------------------------------------------------------------------------------
|
||||
// patch
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export { h, VNode } from "./vdom";
|
||||
|
||||
export const patch = init([eventListenersModule, attrsModule, propsModule, classModule]);
|
||||
@@ -0,0 +1,234 @@
|
||||
import { Module, VNode, VNodeData } from "./vdom";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// module/props.ts
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
function updateProps(oldVnode: VNode, vnode: VNode): void {
|
||||
var key: string,
|
||||
cur: any,
|
||||
old: any,
|
||||
elm = vnode.elm,
|
||||
oldProps = (oldVnode.data as VNodeData).props,
|
||||
props = (vnode.data as VNodeData).props;
|
||||
|
||||
if (!oldProps && !props) return;
|
||||
if (oldProps === props) return;
|
||||
oldProps = oldProps || {};
|
||||
props = props || {};
|
||||
|
||||
for (key in oldProps) {
|
||||
if (!props[key]) {
|
||||
delete (elm as any)[key];
|
||||
}
|
||||
}
|
||||
for (key in props) {
|
||||
cur = props[key];
|
||||
old = oldProps[key];
|
||||
if (old !== cur && (key !== "value" || (elm as any)[key] !== cur)) {
|
||||
(elm as any)[key] = cur;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const propsModule = {
|
||||
create: updateProps,
|
||||
update: updateProps
|
||||
} as Module;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// module/eventlisteners.ts
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
function invokeHandler(handler: any, vnode?: VNode, event?: Event): void {
|
||||
if (typeof handler === "function") {
|
||||
// call function handler
|
||||
handler.call(vnode, event, vnode);
|
||||
} else if (typeof handler === "object") {
|
||||
// call handler with arguments
|
||||
if (typeof handler[0] === "function") {
|
||||
// special case for single argument for performance
|
||||
if (handler.length === 2) {
|
||||
handler[0].call(vnode, handler[1], event, vnode);
|
||||
} else {
|
||||
var args = handler.slice(1);
|
||||
args.push(event);
|
||||
args.push(vnode);
|
||||
handler[0].apply(vnode, args);
|
||||
}
|
||||
} else {
|
||||
// call multiple handlers
|
||||
for (let i = 0, iLen = handler.length; i < iLen; i++) {
|
||||
invokeHandler(handler[i], vnode, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleEvent(event: Event, vnode: VNode) {
|
||||
var name = event.type,
|
||||
on = (vnode.data as VNodeData).on;
|
||||
|
||||
// call event handler(s) if exists
|
||||
if (on && on[name]) {
|
||||
invokeHandler(on[name], vnode, event);
|
||||
}
|
||||
}
|
||||
|
||||
function createListener() {
|
||||
return function handler(event: Event) {
|
||||
handleEvent(event, (handler as any).vnode);
|
||||
};
|
||||
}
|
||||
|
||||
function updateEventListeners(oldVnode: VNode, vnode?: VNode): void {
|
||||
var oldOn = (oldVnode.data as VNodeData).on,
|
||||
oldListener = (oldVnode as any).listener,
|
||||
oldElm: Element = oldVnode.elm as Element,
|
||||
on = vnode && (vnode.data as VNodeData).on,
|
||||
elm: Element = (vnode && vnode.elm) as Element,
|
||||
name: string;
|
||||
|
||||
// optimization for reused immutable handlers
|
||||
if (oldOn === on) {
|
||||
return;
|
||||
}
|
||||
|
||||
// remove existing listeners which no longer used
|
||||
if (oldOn && oldListener) {
|
||||
// if element changed or deleted we remove all existing listeners unconditionally
|
||||
if (!on) {
|
||||
for (name in oldOn) {
|
||||
// remove listener if element was changed or existing listeners removed
|
||||
oldElm.removeEventListener(name, oldListener, false);
|
||||
}
|
||||
} else {
|
||||
for (name in oldOn) {
|
||||
// remove listener if existing listener removed
|
||||
if (!on[name]) {
|
||||
oldElm.removeEventListener(name, oldListener, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add new listeners which has not already attached
|
||||
if (on) {
|
||||
// reuse existing listener or create new
|
||||
var listener = ((vnode as any).listener = (oldVnode as any).listener || createListener());
|
||||
// update vnode for listener
|
||||
listener.vnode = vnode;
|
||||
|
||||
// if element changed or added we add all needed listeners unconditionally
|
||||
if (!oldOn) {
|
||||
for (name in on) {
|
||||
// add listener if element was changed or new listeners added
|
||||
elm.addEventListener(name, listener, false);
|
||||
}
|
||||
} else {
|
||||
for (name in on) {
|
||||
// add listener if new listener added
|
||||
if (!oldOn[name]) {
|
||||
elm.addEventListener(name, listener, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const eventListenersModule = {
|
||||
create: updateEventListeners,
|
||||
update: updateEventListeners,
|
||||
destroy: updateEventListeners
|
||||
} as Module;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// attributes.ts
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const xlinkNS = "http://www.w3.org/1999/xlink";
|
||||
const xmlNS = "http://www.w3.org/XML/1998/namespace";
|
||||
const colonChar = 58;
|
||||
const xChar = 120;
|
||||
|
||||
function updateAttrs(oldVnode: VNode, vnode: VNode): void {
|
||||
var key: string,
|
||||
elm: Element = vnode.elm as Element,
|
||||
oldAttrs = (oldVnode.data as VNodeData).attrs,
|
||||
attrs = (vnode.data as VNodeData).attrs;
|
||||
|
||||
if (!oldAttrs && !attrs) return;
|
||||
if (oldAttrs === attrs) return;
|
||||
oldAttrs = oldAttrs || {};
|
||||
attrs = attrs || {};
|
||||
|
||||
// update modified attributes, add new attributes
|
||||
for (key in attrs) {
|
||||
const cur = attrs[key];
|
||||
const old = oldAttrs[key];
|
||||
if (old !== cur) {
|
||||
if (cur === true) {
|
||||
elm.setAttribute(key, "");
|
||||
} else if (cur === false) {
|
||||
elm.removeAttribute(key);
|
||||
} else {
|
||||
if (key.charCodeAt(0) !== xChar) {
|
||||
elm.setAttribute(key, cur);
|
||||
} else if (key.charCodeAt(3) === colonChar) {
|
||||
// Assume xml namespace
|
||||
elm.setAttributeNS(xmlNS, key, cur);
|
||||
} else if (key.charCodeAt(5) === colonChar) {
|
||||
// Assume xlink namespace
|
||||
elm.setAttributeNS(xlinkNS, key, cur);
|
||||
} else {
|
||||
elm.setAttribute(key, cur);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// remove removed attributes
|
||||
// use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value)
|
||||
// the other option is to remove all attributes with value == undefined
|
||||
for (key in oldAttrs) {
|
||||
if (!(key in attrs)) {
|
||||
elm.removeAttribute(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const attrsModule = {
|
||||
create: updateAttrs,
|
||||
update: updateAttrs
|
||||
} as Module;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// class.ts
|
||||
//------------------------------------------------------------------------------
|
||||
function updateClass(oldVnode: VNode, vnode: VNode): void {
|
||||
var cur: any,
|
||||
name: string,
|
||||
elm: Element,
|
||||
oldClass = (oldVnode.data as VNodeData).class,
|
||||
klass = (vnode.data as VNodeData).class;
|
||||
|
||||
if (!oldClass && !klass) return;
|
||||
if (oldClass === klass) return;
|
||||
oldClass = oldClass || {};
|
||||
klass = klass || {};
|
||||
|
||||
elm = vnode.elm as Element;
|
||||
|
||||
for (name in oldClass) {
|
||||
if (!klass[name]) {
|
||||
elm.classList.remove(name);
|
||||
}
|
||||
}
|
||||
for (name in klass) {
|
||||
cur = klass[name];
|
||||
if (cur !== oldClass[name]) {
|
||||
(elm.classList as any)[cur ? "add" : "remove"](name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const classModule = { create: updateClass, update: updateClass } as Module;
|
||||
+45
-373
@@ -27,6 +27,21 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
type Props = Record<string, any>;
|
||||
type Attrs = Record<string, string | number | boolean>;
|
||||
type On = { [N in keyof HTMLElementEventMap]?: (ev: HTMLElementEventMap[N]) => void } & {
|
||||
[event: string]: EventListener;
|
||||
};
|
||||
|
||||
export interface Module {
|
||||
pre: PreHook;
|
||||
create: CreateHook;
|
||||
update: UpdateHook;
|
||||
destroy: DestroyHook;
|
||||
remove: RemoveHook;
|
||||
post: PostHook;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// vnode.ts
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -41,7 +56,7 @@ export interface VNode {
|
||||
key: Key | undefined;
|
||||
}
|
||||
|
||||
interface VNodeData {
|
||||
export interface VNodeData {
|
||||
props?: Props;
|
||||
attrs?: Attrs;
|
||||
on?: On;
|
||||
@@ -59,14 +74,7 @@ function vnode(
|
||||
elm: Element | Text | undefined
|
||||
): VNode {
|
||||
let key = data === undefined ? undefined : data.key;
|
||||
return {
|
||||
sel: sel,
|
||||
data: data,
|
||||
children: children,
|
||||
text: text,
|
||||
elm: elm,
|
||||
key: key
|
||||
};
|
||||
return { sel, data, children, text, elm, key };
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -116,14 +124,7 @@ function createKeyToOldIdx(
|
||||
return map;
|
||||
}
|
||||
|
||||
const hooks: (keyof Module)[] = [
|
||||
"create",
|
||||
"update",
|
||||
"remove",
|
||||
"destroy",
|
||||
"pre",
|
||||
"post"
|
||||
];
|
||||
const hooks: (keyof Module)[] = ["create", "update", "remove", "destroy", "pre", "post"];
|
||||
|
||||
export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
let i: number,
|
||||
@@ -145,13 +146,7 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
function emptyNodeAt(elm: Element) {
|
||||
const id = elm.id ? "#" + elm.id : "";
|
||||
const c = elm.className ? "." + elm.className.split(" ").join(".") : "";
|
||||
return vnode(
|
||||
api.tagName(elm).toLowerCase() + id + c,
|
||||
{},
|
||||
[],
|
||||
undefined,
|
||||
elm
|
||||
);
|
||||
return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm);
|
||||
}
|
||||
|
||||
function createRmCb(childElm: Node, listeners: number) {
|
||||
@@ -186,19 +181,14 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
const dotIdx = sel.indexOf(".", hashIdx);
|
||||
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 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))
|
||||
? api.createElementNS(i, tag)
|
||||
: api.createElement(tag));
|
||||
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);
|
||||
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);
|
||||
if (array(children)) {
|
||||
for (i = 0, iLen = children.length; i < iLen; ++i) {
|
||||
const ch = children[i];
|
||||
@@ -244,8 +234,7 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
data = vnode.data;
|
||||
if (data !== undefined) {
|
||||
if (isDef((i = data.hook)) && isDef((i = i.destroy))) i(vnode);
|
||||
for (i = 0, iLen = cbs.destroy.length; i < iLen; ++i)
|
||||
cbs.destroy[i](vnode);
|
||||
for (i = 0, iLen = cbs.destroy.length; i < iLen; ++i) cbs.destroy[i](vnode);
|
||||
if (vnode.children !== undefined) {
|
||||
for (j = 0, jLen = vnode.children.length; j < jLen; ++j) {
|
||||
i = vnode.children[j];
|
||||
@@ -274,13 +263,8 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
invokeDestroyHook(ch);
|
||||
listeners = cbs.remove.length + 1;
|
||||
rm = createRmCb(ch.elm as Node, listeners);
|
||||
for (i = 0, iLen = cbs.remove.length; i < iLen; ++i)
|
||||
cbs.remove[i](ch, rm);
|
||||
if (
|
||||
isDef((i = ch.data)) &&
|
||||
isDef((i = i.hook)) &&
|
||||
isDef((i = i.remove))
|
||||
) {
|
||||
for (i = 0, iLen = cbs.remove.length; i < iLen; ++i) cbs.remove[i](ch, rm);
|
||||
if (isDef((i = ch.data)) && isDef((i = i.hook)) && isDef((i = i.remove))) {
|
||||
i(ch, rm);
|
||||
} else {
|
||||
rm();
|
||||
@@ -342,11 +326,7 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
} else if (sameVnode(oldEndVnode, newStartVnode)) {
|
||||
// Vnode moved left
|
||||
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
|
||||
api.insertBefore(
|
||||
parentElm,
|
||||
oldEndVnode.elm as Node,
|
||||
oldStartVnode.elm as Node
|
||||
);
|
||||
api.insertBefore(parentElm, oldEndVnode.elm as Node, oldStartVnode.elm as Node);
|
||||
oldEndVnode = oldCh[--oldEndIdx];
|
||||
newStartVnode = newCh[++newStartIdx];
|
||||
} else {
|
||||
@@ -373,11 +353,7 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
} else {
|
||||
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
|
||||
oldCh[idxInOld] = undefined as any;
|
||||
api.insertBefore(
|
||||
parentElm,
|
||||
elmToMove.elm as Node,
|
||||
oldStartVnode.elm as Node
|
||||
);
|
||||
api.insertBefore(parentElm, elmToMove.elm as Node, oldStartVnode.elm as Node);
|
||||
}
|
||||
newStartVnode = newCh[++newStartIdx];
|
||||
}
|
||||
@@ -386,31 +362,16 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
|
||||
if (oldStartIdx > oldEndIdx) {
|
||||
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
|
||||
addVnodes(
|
||||
parentElm,
|
||||
before,
|
||||
newCh,
|
||||
newStartIdx,
|
||||
newEndIdx,
|
||||
insertedVnodeQueue
|
||||
);
|
||||
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
|
||||
} else {
|
||||
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function patchVnode(
|
||||
oldVnode: VNode,
|
||||
vnode: VNode,
|
||||
insertedVnodeQueue: VNodeQueue
|
||||
) {
|
||||
function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
|
||||
let i: any, iLen: number, hook: any;
|
||||
if (
|
||||
isDef((i = vnode.data)) &&
|
||||
isDef((hook = i.hook)) &&
|
||||
isDef((i = hook.prepatch))
|
||||
) {
|
||||
if (isDef((i = vnode.data)) && isDef((hook = i.hook)) && isDef((i = hook.prepatch))) {
|
||||
i(oldVnode, vnode);
|
||||
}
|
||||
const elm = (vnode.elm = oldVnode.elm as Node);
|
||||
@@ -418,20 +379,14 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
let ch = vnode.children;
|
||||
if (oldVnode === vnode) return;
|
||||
if (vnode.data !== undefined) {
|
||||
for (i = 0, iLen = cbs.update.length; i < iLen; ++i)
|
||||
cbs.update[i](oldVnode, vnode);
|
||||
for (i = 0, iLen = cbs.update.length; i < iLen; ++i) cbs.update[i](oldVnode, vnode);
|
||||
i = vnode.data.hook;
|
||||
if (isDef(i) && isDef((i = i.update))) i(oldVnode, vnode);
|
||||
}
|
||||
if (isUndef(vnode.text)) {
|
||||
if (isDef(oldCh) && isDef(ch)) {
|
||||
if (oldCh !== ch)
|
||||
updateChildren(
|
||||
elm,
|
||||
oldCh as Array<VNode>,
|
||||
ch as Array<VNode>,
|
||||
insertedVnodeQueue
|
||||
);
|
||||
updateChildren(elm, oldCh as Array<VNode>, ch as Array<VNode>, insertedVnodeQueue);
|
||||
} else if (isDef(ch)) {
|
||||
if (isDef(oldVnode.text)) api.setTextContent(elm, "");
|
||||
addVnodes(
|
||||
@@ -443,23 +398,13 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
insertedVnodeQueue
|
||||
);
|
||||
} else if (isDef(oldCh)) {
|
||||
removeVnodes(
|
||||
elm,
|
||||
oldCh as Array<VNode>,
|
||||
0,
|
||||
(oldCh as Array<VNode>).length - 1
|
||||
);
|
||||
removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
|
||||
} else if (isDef(oldVnode.text)) {
|
||||
api.setTextContent(elm, "");
|
||||
}
|
||||
} else if (oldVnode.text !== vnode.text) {
|
||||
if (isDef(oldCh)) {
|
||||
removeVnodes(
|
||||
elm,
|
||||
oldCh as Array<VNode>,
|
||||
0,
|
||||
(oldCh as Array<VNode>).length - 1
|
||||
);
|
||||
removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
|
||||
}
|
||||
api.setTextContent(elm, vnode.text as string);
|
||||
}
|
||||
@@ -517,21 +462,13 @@ interface DOMAPI {
|
||||
createElementNS: (namespaceURI: string, qualifiedName: string) => Element;
|
||||
createTextNode: (text: string) => Text;
|
||||
createComment: (text: string) => Comment;
|
||||
insertBefore: (
|
||||
parentNode: Node,
|
||||
newNode: Node,
|
||||
referenceNode: Node | null
|
||||
) => void;
|
||||
insertBefore: (parentNode: Node, newNode: Node, referenceNode: Node | null) => void;
|
||||
removeChild: (node: Node, child: Node) => void;
|
||||
appendChild: (node: Node, child: Node) => void;
|
||||
parentNode: (node: Node) => Node;
|
||||
nextSibling: (node: Node) => Node;
|
||||
tagName: (elm: Element) => string;
|
||||
setTextContent: (node: Node, text: string | null) => void;
|
||||
getTextContent: (node: Node) => string | null;
|
||||
isElement: (node: Node) => node is Element;
|
||||
isText: (node: Node) => node is Text;
|
||||
isComment: (node: Node) => node is Comment;
|
||||
}
|
||||
|
||||
function createElement(tagName: any): HTMLElement {
|
||||
@@ -550,11 +487,7 @@ function createComment(text: string): Comment {
|
||||
return document.createComment(text);
|
||||
}
|
||||
|
||||
function insertBefore(
|
||||
parentNode: Node,
|
||||
newNode: Node,
|
||||
referenceNode: Node | null
|
||||
): void {
|
||||
function insertBefore(parentNode: Node, newNode: Node, referenceNode: Node | null): void {
|
||||
parentNode.insertBefore(newNode, referenceNode);
|
||||
}
|
||||
|
||||
@@ -582,22 +515,6 @@ function setTextContent(node: Node, text: string | null): void {
|
||||
node.textContent = text;
|
||||
}
|
||||
|
||||
function getTextContent(node: Node): string | null {
|
||||
return node.textContent;
|
||||
}
|
||||
|
||||
function isElement(node: Node): node is Element {
|
||||
return node.nodeType === 1;
|
||||
}
|
||||
|
||||
function isText(node: Node): node is Text {
|
||||
return node.nodeType === 3;
|
||||
}
|
||||
|
||||
function isComment(node: Node): node is Comment {
|
||||
return node.nodeType === 8;
|
||||
}
|
||||
|
||||
const htmlDomApi = {
|
||||
createElement,
|
||||
createElementNS,
|
||||
@@ -609,11 +526,7 @@ const htmlDomApi = {
|
||||
parentNode,
|
||||
nextSibling,
|
||||
tagName,
|
||||
setTextContent,
|
||||
getTextContent,
|
||||
isElement,
|
||||
isText,
|
||||
isComment
|
||||
setTextContent
|
||||
} as DOMAPI;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -651,21 +564,17 @@ type VNodeChildElement = VNode | string | number | undefined | null;
|
||||
type ArrayOrElement<T> = T | T[];
|
||||
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 {
|
||||
data.ns = "http://www.w3.org/2000/svg";
|
||||
if (sel !== "foreignObject" && children !== undefined) {
|
||||
for (let i = 0, iLen = children.length; i < iLen; ++i) {
|
||||
let childData = children[i].data;
|
||||
const child = children[i];
|
||||
if (child === null) {
|
||||
continue;
|
||||
}
|
||||
let childData = child.data;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -704,245 +613,8 @@ export function h(sel: any, b?: any, c?: any): VNode {
|
||||
if (children !== undefined) {
|
||||
for (i = 0, iLen = children.length; i < iLen; ++i) {
|
||||
if (primitive(children[i]))
|
||||
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);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// module/props.ts
|
||||
//------------------------------------------------------------------------------
|
||||
type Props = Record<string, any>;
|
||||
|
||||
function updateProps(oldVnode: VNode, vnode: VNode): void {
|
||||
var key: string,
|
||||
cur: any,
|
||||
old: any,
|
||||
elm = vnode.elm,
|
||||
oldProps = (oldVnode.data as VNodeData).props,
|
||||
props = (vnode.data as VNodeData).props;
|
||||
|
||||
if (!oldProps && !props) return;
|
||||
if (oldProps === props) return;
|
||||
oldProps = oldProps || {};
|
||||
props = props || {};
|
||||
|
||||
for (key in oldProps) {
|
||||
if (!props[key]) {
|
||||
delete (elm as any)[key];
|
||||
}
|
||||
}
|
||||
for (key in props) {
|
||||
cur = props[key];
|
||||
old = oldProps[key];
|
||||
if (old !== cur && (key !== "value" || (elm as any)[key] !== cur)) {
|
||||
(elm as any)[key] = cur;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const propsModule = {
|
||||
create: updateProps,
|
||||
update: updateProps
|
||||
} as Module;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// module/module.ts
|
||||
//------------------------------------------------------------------------------
|
||||
interface Module {
|
||||
pre: PreHook;
|
||||
create: CreateHook;
|
||||
update: UpdateHook;
|
||||
destroy: DestroyHook;
|
||||
remove: RemoveHook;
|
||||
post: PostHook;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// module/eventlisteners.ts
|
||||
//------------------------------------------------------------------------------
|
||||
type On = {
|
||||
[N in keyof HTMLElementEventMap]?: (ev: HTMLElementEventMap[N]) => void
|
||||
} & {
|
||||
[event: string]: EventListener;
|
||||
};
|
||||
|
||||
function invokeHandler(handler: any, vnode?: VNode, event?: Event): void {
|
||||
if (typeof handler === "function") {
|
||||
// call function handler
|
||||
handler.call(vnode, event, vnode);
|
||||
} else if (typeof handler === "object") {
|
||||
// call handler with arguments
|
||||
if (typeof handler[0] === "function") {
|
||||
// special case for single argument for performance
|
||||
if (handler.length === 2) {
|
||||
handler[0].call(vnode, handler[1], event, vnode);
|
||||
} else {
|
||||
var args = handler.slice(1);
|
||||
args.push(event);
|
||||
args.push(vnode);
|
||||
handler[0].apply(vnode, args);
|
||||
}
|
||||
} else {
|
||||
// call multiple handlers
|
||||
for (let i = 0, iLen = handler.length; i < iLen; i++) {
|
||||
invokeHandler(handler[i], vnode, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleEvent(event: Event, vnode: VNode) {
|
||||
var name = event.type,
|
||||
on = (vnode.data as VNodeData).on;
|
||||
|
||||
// call event handler(s) if exists
|
||||
if (on && on[name]) {
|
||||
invokeHandler(on[name], vnode, event);
|
||||
}
|
||||
}
|
||||
|
||||
function createListener() {
|
||||
return function handler(event: Event) {
|
||||
handleEvent(event, (handler as any).vnode);
|
||||
};
|
||||
}
|
||||
|
||||
function updateEventListeners(oldVnode: VNode, vnode?: VNode): void {
|
||||
var oldOn = (oldVnode.data as VNodeData).on,
|
||||
oldListener = (oldVnode as any).listener,
|
||||
oldElm: Element = oldVnode.elm as Element,
|
||||
on = vnode && (vnode.data as VNodeData).on,
|
||||
elm: Element = (vnode && vnode.elm) as Element,
|
||||
name: string;
|
||||
|
||||
// optimization for reused immutable handlers
|
||||
if (oldOn === on) {
|
||||
return;
|
||||
}
|
||||
|
||||
// remove existing listeners which no longer used
|
||||
if (oldOn && oldListener) {
|
||||
// if element changed or deleted we remove all existing listeners unconditionally
|
||||
if (!on) {
|
||||
for (name in oldOn) {
|
||||
// remove listener if element was changed or existing listeners removed
|
||||
oldElm.removeEventListener(name, oldListener, false);
|
||||
}
|
||||
} else {
|
||||
for (name in oldOn) {
|
||||
// remove listener if existing listener removed
|
||||
if (!on[name]) {
|
||||
oldElm.removeEventListener(name, oldListener, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add new listeners which has not already attached
|
||||
if (on) {
|
||||
// reuse existing listener or create new
|
||||
var listener = ((vnode as any).listener =
|
||||
(oldVnode as any).listener || createListener());
|
||||
// update vnode for listener
|
||||
listener.vnode = vnode;
|
||||
|
||||
// if element changed or added we add all needed listeners unconditionally
|
||||
if (!oldOn) {
|
||||
for (name in on) {
|
||||
// add listener if element was changed or new listeners added
|
||||
elm.addEventListener(name, listener, false);
|
||||
}
|
||||
} else {
|
||||
for (name in on) {
|
||||
// add listener if new listener added
|
||||
if (!oldOn[name]) {
|
||||
elm.addEventListener(name, listener, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const eventListenersModule = {
|
||||
create: updateEventListeners,
|
||||
update: updateEventListeners,
|
||||
destroy: updateEventListeners
|
||||
} as Module;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// attributes.ts
|
||||
//------------------------------------------------------------------------------
|
||||
type Attrs = Record<string, string | number | boolean>;
|
||||
|
||||
const xlinkNS = "http://www.w3.org/1999/xlink";
|
||||
const xmlNS = "http://www.w3.org/XML/1998/namespace";
|
||||
const colonChar = 58;
|
||||
const xChar = 120;
|
||||
|
||||
function updateAttrs(oldVnode: VNode, vnode: VNode): void {
|
||||
var key: string,
|
||||
elm: Element = vnode.elm as Element,
|
||||
oldAttrs = (oldVnode.data as VNodeData).attrs,
|
||||
attrs = (vnode.data as VNodeData).attrs;
|
||||
|
||||
if (!oldAttrs && !attrs) return;
|
||||
if (oldAttrs === attrs) return;
|
||||
oldAttrs = oldAttrs || {};
|
||||
attrs = attrs || {};
|
||||
|
||||
// update modified attributes, add new attributes
|
||||
for (key in attrs) {
|
||||
const cur = attrs[key];
|
||||
const old = oldAttrs[key];
|
||||
if (old !== cur) {
|
||||
if (cur === true) {
|
||||
elm.setAttribute(key, "");
|
||||
} else if (cur === false) {
|
||||
elm.removeAttribute(key);
|
||||
} else {
|
||||
if (key.charCodeAt(0) !== xChar) {
|
||||
elm.setAttribute(key, cur);
|
||||
} else if (key.charCodeAt(3) === colonChar) {
|
||||
// Assume xml namespace
|
||||
elm.setAttributeNS(xmlNS, key, cur);
|
||||
} else if (key.charCodeAt(5) === colonChar) {
|
||||
// Assume xlink namespace
|
||||
elm.setAttributeNS(xlinkNS, key, cur);
|
||||
} else {
|
||||
elm.setAttribute(key, cur);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// remove removed attributes
|
||||
// use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value)
|
||||
// the other option is to remove all attributes with value == undefined
|
||||
for (key in oldAttrs) {
|
||||
if (!(key in attrs)) {
|
||||
elm.removeAttribute(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const attrsModule = {
|
||||
create: updateAttrs,
|
||||
update: updateAttrs
|
||||
} as Module;
|
||||
|
||||
export const patch = init([eventListenersModule, attrsModule, propsModule]);
|
||||
@@ -1,37 +1,42 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`animations t-transition combined with t-widget 1`] = `
|
||||
exports[`animations t-transition combined with component 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let utils = this.utils;
|
||||
let utils = this.constructor.utils;
|
||||
let QWeb = this.constructor;
|
||||
let parent = context;
|
||||
let owner = context;
|
||||
var h = this.utils.h;
|
||||
var h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
//WIDGET
|
||||
//COMPONENT
|
||||
let def3;
|
||||
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let def3;
|
||||
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
|
||||
let props4 = {};
|
||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
|
||||
w4.destroy();
|
||||
w4 = false
|
||||
if (w4 && w4.__owl__.currentFiber && !w4.__owl__.vnode) {
|
||||
if (utils.shallowEqual(props4, w4.__owl__.currentFiber.props)) {
|
||||
def3 = w4.__owl__.currentFiber.promise;
|
||||
} else {
|
||||
w4.destroy();
|
||||
w4 = false;
|
||||
}
|
||||
}
|
||||
if (!w4) {
|
||||
let widgetKey4 = \`Child\`;
|
||||
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
|
||||
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
|
||||
w4 = new W4(owner, props4);
|
||||
context.__owl__.cmap[4] = w4.__owl__.id;
|
||||
def3 = w4._prepare();
|
||||
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn.elm, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
|
||||
let componentKey4 = \`Child\`;
|
||||
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4]|| context['Child'];
|
||||
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
||||
w4 = new W4(parent, props4);
|
||||
parent.__owl__.cmap[4] = w4.__owl__.id;
|
||||
def3 = w4.__prepare(extra.fiber, undefined, undefined);
|
||||
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.elm, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
||||
utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
||||
} else {
|
||||
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
||||
def3 = def3 || w4.__updateProps(props4, extra.fiber, undefined, undefined);
|
||||
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
||||
}
|
||||
extra.promises.push(def3);
|
||||
@@ -39,39 +44,44 @@ exports[`animations t-transition combined with t-widget 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`animations t-transition combined with t-widget and t-if 1`] = `
|
||||
exports[`animations t-transition combined with t-component and t-if 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let utils = this.utils;
|
||||
let utils = this.constructor.utils;
|
||||
let QWeb = this.constructor;
|
||||
let parent = context;
|
||||
let owner = context;
|
||||
var h = this.utils.h;
|
||||
var h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
if (context['state'].display) {
|
||||
//WIDGET
|
||||
//COMPONENT
|
||||
let def3;
|
||||
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let def3;
|
||||
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
|
||||
let props4 = {};
|
||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
|
||||
w4.destroy();
|
||||
w4 = false
|
||||
if (w4 && w4.__owl__.currentFiber && !w4.__owl__.vnode) {
|
||||
if (utils.shallowEqual(props4, w4.__owl__.currentFiber.props)) {
|
||||
def3 = w4.__owl__.currentFiber.promise;
|
||||
} else {
|
||||
w4.destroy();
|
||||
w4 = false;
|
||||
}
|
||||
}
|
||||
if (!w4) {
|
||||
let widgetKey4 = \`Child\`;
|
||||
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
|
||||
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
|
||||
w4 = new W4(owner, props4);
|
||||
context.__owl__.cmap[4] = w4.__owl__.id;
|
||||
def3 = w4._prepare();
|
||||
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn.elm, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
|
||||
let componentKey4 = \`Child\`;
|
||||
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4]|| context['Child'];
|
||||
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
||||
w4 = new W4(parent, props4);
|
||||
parent.__owl__.cmap[4] = w4.__owl__.id;
|
||||
def3 = w4.__prepare(extra.fiber, undefined, undefined);
|
||||
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.elm, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
||||
utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
||||
} else {
|
||||
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
||||
def3 = def3 || w4.__updateProps(props4, extra.fiber, undefined, undefined);
|
||||
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
||||
}
|
||||
extra.promises.push(def3);
|
||||
@@ -83,15 +93,16 @@ exports[`animations t-transition combined with t-widget and t-if 1`] = `
|
||||
exports[`animations t-transition with no delay/duration 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
var h = this.utils.h;
|
||||
let utils = this.constructor.utils;
|
||||
var h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('span', p1, c1);
|
||||
p1.hook = {
|
||||
insert: vn => {
|
||||
this.utils.transitionInsert(vn.elm, 'jupiler');
|
||||
utils.transitionInsert(vn, 'jupiler');
|
||||
},
|
||||
remove: (vn, rm) => {
|
||||
this.utils.transitionRemove(vn.elm, 'jupiler', rm);
|
||||
utils.transitionRemove(vn, 'jupiler', rm);
|
||||
},
|
||||
};
|
||||
c1.push({text: \`blue\`});
|
||||
@@ -102,15 +113,16 @@ exports[`animations t-transition with no delay/duration 1`] = `
|
||||
exports[`animations t-transition, on a simple node (insert) 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
var h = this.utils.h;
|
||||
let utils = this.constructor.utils;
|
||||
var h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('span', p1, c1);
|
||||
p1.hook = {
|
||||
insert: vn => {
|
||||
this.utils.transitionInsert(vn.elm, 'chimay');
|
||||
utils.transitionInsert(vn, 'chimay');
|
||||
},
|
||||
remove: (vn, rm) => {
|
||||
this.utils.transitionRemove(vn.elm, 'chimay', rm);
|
||||
utils.transitionRemove(vn, 'chimay', rm);
|
||||
},
|
||||
};
|
||||
c1.push({text: \`blue\`});
|
||||
|
||||
@@ -1,276 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`class and style attributes with t-widget dynamic t-att-style is properly added and updated on widget root el 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let utils = this.utils;
|
||||
let QWeb = this.constructor;
|
||||
let owner = context;
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
//WIDGET
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let def3;
|
||||
const _5 = context['state'].style;
|
||||
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
|
||||
let props4 = {};
|
||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
|
||||
w4.destroy();
|
||||
w4 = false
|
||||
}
|
||||
if (!w4) {
|
||||
let widgetKey4 = \`child\`;
|
||||
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
|
||||
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
|
||||
w4 = new W4(owner, props4);
|
||||
context.__owl__.cmap[4] = w4.__owl__.id;
|
||||
def3 = w4._prepare();
|
||||
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.style = _5}};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 {
|
||||
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
||||
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};w4.el.style=_5;let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
||||
}
|
||||
extra.promises.push(def3);
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`class and style attributes with t-widget t-att-class is properly added/removed on widget root el 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let utils = this.utils;
|
||||
let QWeb = this.constructor;
|
||||
let owner = context;
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
//WIDGET
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let def3;
|
||||
const _5 = {a: context['state'].a,b: context['state'].b};
|
||||
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
|
||||
let props4 = {};
|
||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
|
||||
w4.destroy();
|
||||
w4 = false
|
||||
}
|
||||
if (!w4) {
|
||||
let widgetKey4 = \`child\`;
|
||||
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
|
||||
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
|
||||
w4 = new W4(owner, props4);
|
||||
context.__owl__.cmap[4] = w4.__owl__.id;
|
||||
def3 = w4._prepare();
|
||||
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){for (let k in _5) {
|
||||
if (_5[k]) {
|
||||
vn.elm.classList.add(k);
|
||||
}
|
||||
}}};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 {
|
||||
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
||||
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let cl=w4.el.classList;for (let k in _5) {if (_5[k]) {cl.add(k)} else {cl.remove(k)}}let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
||||
}
|
||||
extra.promises.push(def3);
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`composition sub widgets with some state rendered in a loop 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let utils = this.utils;
|
||||
let QWeb = this.constructor;
|
||||
let owner = context;
|
||||
context = Object.create(context);
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
var _2 = context['state'].numbers;
|
||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||
if (typeof _2 === 'number') { _2 = Array.from(Array(_2).keys())}
|
||||
var _3 = _2 instanceof Array ? _2 : Object.keys(_2);
|
||||
var _length3 = _3.length;
|
||||
var _4 = _2 instanceof Array ? _2 : Object.values(_2);
|
||||
for (let i = 0; i < _length3; i++) {
|
||||
context.number_first = i === 0;
|
||||
context.number_last = i === _length3 - 1;
|
||||
context.number_parity = i % 2 === 0 ? 'even' : 'odd';
|
||||
context.number_index = i;
|
||||
context.number = _3[i];
|
||||
context.number_value = _4[i];
|
||||
//WIDGET
|
||||
let key8 = context['number'];
|
||||
let _5_index = c1.length;
|
||||
c1.push(null);
|
||||
let def6;
|
||||
let w7 = key8 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[key8]] : false;
|
||||
let props7 = {};
|
||||
if (w7 && w7.__owl__.renderPromise && !w7.__owl__.vnode && props7 !== w7.__owl__.renderProps) {
|
||||
w7.destroy();
|
||||
w7 = false
|
||||
}
|
||||
if (!w7) {
|
||||
let widgetKey7 = \`ChildWidget\`;
|
||||
let W7 = context.widgets && context.widgets[widgetKey7] || QWeb.widgets[widgetKey7];
|
||||
if (!W7) {throw new Error('Cannot find the definition of widget \\"' + widgetKey7 + '\\"')}
|
||||
w7 = new W7(owner, props7);
|
||||
context.__owl__.cmap[key8] = w7.__owl__.id;
|
||||
def6 = w7._prepare();
|
||||
def6 = def6.then(vnode=>{let pvnode=h(vnode.sel, {key: key8, hook: {insert(vn) {let nvn=w7._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w7.destroy();}}});c1[_5_index]=pvnode;w7.__owl__.pvnode = pvnode;});
|
||||
} else {
|
||||
def6 = w7._updateProps(props7, extra.forceUpdate, extra.patchQueue);
|
||||
def6 = def6.then(()=>{if (w7.__owl__.isDestroyed) {return};let pvnode=w7.__owl__.pvnode;c1[_5_index]=pvnode;});
|
||||
}
|
||||
extra.promises.push(def6);
|
||||
}
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`composition t-widget with dynamic value 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let utils = this.utils;
|
||||
let QWeb = this.constructor;
|
||||
let owner = context;
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
//WIDGET
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let def3;
|
||||
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
|
||||
let props4 = {};
|
||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
|
||||
w4.destroy();
|
||||
w4 = false
|
||||
}
|
||||
if (!w4) {
|
||||
let widgetKey4 = context['state'].widget;
|
||||
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
|
||||
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
|
||||
w4 = new W4(owner, props4);
|
||||
context.__owl__.cmap[4] = w4.__owl__.id;
|
||||
def3 = w4._prepare();
|
||||
def3 = def3.then(vnode=>{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 {
|
||||
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;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`composition t-widget with dynamic value 2 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let utils = this.utils;
|
||||
let QWeb = this.constructor;
|
||||
let owner = context;
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
//WIDGET
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let def3;
|
||||
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
|
||||
let props4 = {};
|
||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
|
||||
w4.destroy();
|
||||
w4 = false
|
||||
}
|
||||
if (!w4) {
|
||||
let widgetKey4 = \`Widget\${context['state'].widget}\`;
|
||||
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
|
||||
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
|
||||
w4 = new W4(owner, props4);
|
||||
context.__owl__.cmap[4] = w4.__owl__.id;
|
||||
def3 = w4._prepare();
|
||||
def3 = def3.then(vnode=>{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 {
|
||||
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;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let utils = this.utils;
|
||||
let QWeb = this.constructor;
|
||||
let owner = context;
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
//WIDGET
|
||||
let key5 = 'somestring';
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let def3;
|
||||
let w4 = key5 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[key5]] : false;
|
||||
let props4 = {flag: context['state'].flag};
|
||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
|
||||
w4.destroy();
|
||||
w4 = false
|
||||
}
|
||||
if (!w4) {
|
||||
let widgetKey4 = \`child\`;
|
||||
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
|
||||
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
|
||||
w4 = new W4(owner, props4);
|
||||
context.__owl__.cmap[key5] = w4.__owl__.id;
|
||||
def3 = w4._prepare();
|
||||
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: key5, 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 {
|
||||
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;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`random stuff/miscellaneous t-props should not be undefined (snapshotting) 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let utils = this.utils;
|
||||
let QWeb = this.constructor;
|
||||
let owner = context;
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
//WIDGET
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let def3;
|
||||
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
|
||||
let props4 = {};
|
||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
|
||||
w4.destroy();
|
||||
w4 = false
|
||||
}
|
||||
if (!w4) {
|
||||
let widgetKey4 = \`child\`;
|
||||
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
|
||||
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
|
||||
w4 = new W4(owner, props4);
|
||||
context.__owl__.cmap[4] = w4.__owl__.id;
|
||||
def3 = w4._prepare();
|
||||
def3 = def3.then(vnode=>{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 {
|
||||
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;
|
||||
}"
|
||||
`;
|
||||
@@ -1,13 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
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>"`;
|
||||
@@ -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>"`;
|
||||
@@ -1,33 +0,0 @@
|
||||
function anonymous(context, extra) {
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
if (context['state'].display) {
|
||||
//WIDGET
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let def3;
|
||||
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
|
||||
let props4 = {};
|
||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
|
||||
w4.destroy();
|
||||
w4 = false
|
||||
}
|
||||
if (!w4) {
|
||||
let W4 = context.widgets['Child'];
|
||||
if (!W4) {throw new Error(`Cannot find the definition of widget "Child"`)}
|
||||
w4 = new W4(owner, props4);
|
||||
context.__owl__.cmap[4] = w4.__owl__.id;
|
||||
def3 = w4._prepare();
|
||||
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert: (vn) => {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;this.utils.transitionInsert(vn.elm, 'chimay');},remove: () => {},destroy: (vn) => {let finalize = () => {
|
||||
w4.destroy();
|
||||
};
|
||||
this.utils.transitionRemove(vn.elm, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
+128
-39
@@ -1,10 +1,10 @@
|
||||
import { Component, Env } from "../src/component";
|
||||
import { QWeb } from "../src/qweb_core";
|
||||
import { Component, Env } from "../src/component/component";
|
||||
import { QWeb } from "../src/qweb/index";
|
||||
import { useState, useRef } from "../src/hooks";
|
||||
import {
|
||||
makeDeferred,
|
||||
makeTestFixture,
|
||||
makeTestEnv,
|
||||
nextTick,
|
||||
patchNextFrame,
|
||||
renderToDOM,
|
||||
unpatchNextFrame
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
// - fixture: a div, appended to the DOM, intended to be the target of dom
|
||||
// manipulations. Note that it is removed after each test.
|
||||
// - qweb: a new QWeb instance
|
||||
// - env: a WEnv, necessary to create new widgets
|
||||
// - env: a WEnv, necessary to create new components
|
||||
// - cssEl: a stylesheet injected into the dom
|
||||
|
||||
let fixture: HTMLElement;
|
||||
@@ -36,7 +36,7 @@ afterEach(() => {
|
||||
fixture.remove();
|
||||
});
|
||||
|
||||
class Widget extends Component<any, any, any> {}
|
||||
class Widget extends Component<any, any> {}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Tests
|
||||
@@ -106,7 +106,7 @@ describe("animations", () => {
|
||||
`<div><span t-if="!state.hide" t-transition="chimay">blue</span></div>`
|
||||
);
|
||||
class TestWidget extends Widget {
|
||||
state = { hide: false };
|
||||
state = useState({ hide: false });
|
||||
}
|
||||
const widget = new TestWidget(env);
|
||||
|
||||
@@ -148,7 +148,8 @@ describe("animations", () => {
|
||||
`<div><span t-ref="span" t-transition="chimay">blue</span></div>`
|
||||
);
|
||||
class TestWidget extends Widget {
|
||||
state = { hide: false };
|
||||
state = useState({ hide: false });
|
||||
span = useRef("span");
|
||||
}
|
||||
const widget = new TestWidget(env);
|
||||
|
||||
@@ -163,25 +164,22 @@ describe("animations", () => {
|
||||
});
|
||||
await widget.mount(fixture);
|
||||
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");
|
||||
await def; // wait for the mocked repaint to be done
|
||||
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||
expect(spanNode.className).toBe("");
|
||||
});
|
||||
|
||||
test("t-transition combined with t-widget", async () => {
|
||||
test("t-transition combined with component", async () => {
|
||||
expect.assertions(5);
|
||||
|
||||
env.qweb.addTemplate(
|
||||
"Parent",
|
||||
`<div><t t-widget="Child" t-transition="chimay"/></div>`
|
||||
);
|
||||
env.qweb.addTemplate("Parent", `<div><Child t-transition="chimay"/></div>`);
|
||||
env.qweb.addTemplate("Child", `<span>blue</span>`);
|
||||
class Parent extends Widget {
|
||||
widgets = { Child: Child };
|
||||
}
|
||||
class Child extends Widget {}
|
||||
class Parent extends Widget {
|
||||
static components = { Child: Child };
|
||||
}
|
||||
const widget = new Parent(env);
|
||||
|
||||
let def = makeDeferred();
|
||||
@@ -209,19 +207,19 @@ describe("animations", () => {
|
||||
expect(fixture.innerHTML).toBe('<div><span class="">blue</span></div>');
|
||||
});
|
||||
|
||||
test("t-transition combined with t-widget and t-if", async () => {
|
||||
test("t-transition combined with t-component and t-if", async () => {
|
||||
expect.assertions(8);
|
||||
|
||||
env.qweb.addTemplate(
|
||||
"Parent",
|
||||
`<div><t t-if="state.display" t-widget="Child" t-transition="chimay"/></div>`
|
||||
`<div><t t-if="state.display" t-component="Child" t-transition="chimay"/></div>`
|
||||
);
|
||||
env.qweb.addTemplate("Child", `<span>blue</span>`);
|
||||
class Parent extends Widget {
|
||||
widgets = { Child: Child };
|
||||
state = { display: true };
|
||||
}
|
||||
class Child extends Widget {}
|
||||
class Parent extends Widget {
|
||||
static components = { Child: Child };
|
||||
state = useState({ display: true });
|
||||
}
|
||||
const widget = new Parent(env);
|
||||
|
||||
let def = makeDeferred();
|
||||
@@ -253,11 +251,11 @@ describe("animations", () => {
|
||||
widget.state.display = false;
|
||||
patchNextFrame(cb => {
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="chimay-leave chimay-leave-active">blue</span></div>'
|
||||
'<div><span class="chimay-leave chimay-leave-active" data-owl-key="4">blue</span></div>'
|
||||
);
|
||||
cb();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="chimay-leave-active chimay-leave-to">blue</span></div>'
|
||||
'<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="4">blue</span></div>'
|
||||
);
|
||||
def.resolve();
|
||||
});
|
||||
@@ -266,25 +264,116 @@ describe("animations", () => {
|
||||
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>`
|
||||
test("t-transition, remove and re-add before transitionend", async () => {
|
||||
expect.assertions(11);
|
||||
|
||||
env.qweb.addTemplates(
|
||||
`<templates>
|
||||
<div t-name="Parent">
|
||||
<button t-on-click="toggle">Toggle</button>
|
||||
<span t-if="state.flag" t-transition="chimay">blue</span>
|
||||
</div>
|
||||
</templates>`
|
||||
);
|
||||
class TestWidget extends Widget {
|
||||
state = { flag: false };
|
||||
f() {}
|
||||
class Parent extends Widget {
|
||||
state = useState({ flag: false });
|
||||
|
||||
toggle() {
|
||||
this.state.flag = !this.state.flag;
|
||||
}
|
||||
}
|
||||
const widget = new TestWidget(env);
|
||||
widget.f = jest.fn();
|
||||
|
||||
const widget = new Parent(env);
|
||||
await widget.mount(fixture);
|
||||
let button = widget.el!.querySelector("button");
|
||||
|
||||
patchNextFrame(cb => cb());
|
||||
expect(widget.f).toHaveBeenCalledTimes(0);
|
||||
let def = makeDeferred();
|
||||
let phase = "enter";
|
||||
patchNextFrame(cb => {
|
||||
let spans = fixture.querySelectorAll("span");
|
||||
expect(spans.length).toBe(1);
|
||||
expect(spans[0].className).toBe(`chimay-${phase} chimay-${phase}-active`);
|
||||
cb();
|
||||
expect(spans[0].className).toBe(`chimay-${phase}-active chimay-${phase}-to`);
|
||||
def.resolve();
|
||||
});
|
||||
|
||||
widget.state.flag = true;
|
||||
await nextTick();
|
||||
expect(widget.f).toHaveBeenCalledTimes(1);
|
||||
unpatchNextFrame();
|
||||
// click display the span
|
||||
button!.click();
|
||||
await def; // wait for the mocked repaint to be done
|
||||
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>');
|
||||
|
||||
// click to remove the span, and click again to re-add it before transitionend
|
||||
def = makeDeferred();
|
||||
phase = "leave";
|
||||
button!.click();
|
||||
|
||||
await def; // wait for the mocked repaint to be done
|
||||
def = makeDeferred();
|
||||
phase = "enter";
|
||||
button!.click();
|
||||
|
||||
await def; // wait for the mocked repaint to be done
|
||||
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>');
|
||||
});
|
||||
|
||||
test("t-transition combined with t-component, remove and re-add before transitionend", async () => {
|
||||
expect.assertions(11);
|
||||
|
||||
env.qweb.addTemplates(
|
||||
`<templates>
|
||||
<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 {
|
||||
static components = { Child };
|
||||
state = useState({ flag: false });
|
||||
|
||||
toggle() {
|
||||
this.state.flag = !this.state.flag;
|
||||
}
|
||||
}
|
||||
|
||||
const widget = new Parent(env);
|
||||
await widget.mount(fixture);
|
||||
let button = widget.el!.querySelector("button");
|
||||
|
||||
let def = makeDeferred();
|
||||
let phase = "enter";
|
||||
patchNextFrame(cb => {
|
||||
let spans = fixture.querySelectorAll("span");
|
||||
expect(spans.length).toBe(1);
|
||||
expect(spans[0].className).toBe(`chimay-${phase} chimay-${phase}-active`);
|
||||
cb();
|
||||
expect(spans[0].className).toBe(`chimay-${phase}-active chimay-${phase}-to`);
|
||||
def.resolve();
|
||||
});
|
||||
|
||||
// click display the span
|
||||
button!.click();
|
||||
await def; // wait for the mocked repaint to be done
|
||||
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>');
|
||||
|
||||
// click to remove the span, and click again to re-add it before transitionend
|
||||
def = makeDeferred();
|
||||
phase = "leave";
|
||||
button!.click();
|
||||
|
||||
await def; // wait for the mocked repaint to be done
|
||||
def = makeDeferred();
|
||||
phase = "enter";
|
||||
button!.click();
|
||||
|
||||
await def; // wait for the mocked repaint to be done
|
||||
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>');
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,48 @@
|
||||
// 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`] = `
|
||||
"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);
|
||||
//COMPONENT
|
||||
let def3;
|
||||
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let props4 = {message:1};
|
||||
if (w4 && w4.__owl__.currentFiber && !w4.__owl__.vnode) {
|
||||
if (utils.shallowEqual(props4, w4.__owl__.currentFiber.props)) {
|
||||
def3 = w4.__owl__.currentFiber.promise;
|
||||
} else {
|
||||
w4.destroy();
|
||||
w4 = false;
|
||||
}
|
||||
}
|
||||
if (!w4) {
|
||||
let componentKey4 = \`Child\`;
|
||||
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4]|| context['Child'];
|
||||
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(extra.fiber, undefined, undefined);
|
||||
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.fiber, undefined, undefined);
|
||||
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
||||
}
|
||||
extra.promises.push(def3);
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
import { Component, Env } from "../src/component";
|
||||
import { makeTestFixture, makeTestEnv } from "./helpers";
|
||||
import { QWeb } from "../src";
|
||||
import { Component, Env } from "../../src/component/component";
|
||||
import { makeTestFixture, makeTestEnv } from "../helpers";
|
||||
import { QWeb } from "../../src/qweb";
|
||||
import { xml } from "../../src/tags";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Setup and helpers
|
||||
@@ -22,7 +23,7 @@ afterEach(() => {
|
||||
QWeb.dev = dev;
|
||||
});
|
||||
|
||||
class Widget extends Component<any, any, any> {}
|
||||
class Widget extends Component<any, any> {}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Tests
|
||||
@@ -31,6 +32,7 @@ describe("props validation", () => {
|
||||
test("validation is only done in dev mode", async () => {
|
||||
class TestWidget extends Widget {
|
||||
static props = ["message"];
|
||||
static template = xml`<div>hey</div>`;
|
||||
}
|
||||
|
||||
QWeb.dev = true;
|
||||
@@ -45,27 +47,15 @@ describe("props validation", () => {
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
test("props validation is also done on update props", async () => {
|
||||
expect.assertions(1);
|
||||
class TestWidget extends Widget {
|
||||
static props = ["message"];
|
||||
}
|
||||
const w = new TestWidget(env, { message: "bottle" });
|
||||
try {
|
||||
await w._updateProps({});
|
||||
} catch (e) {
|
||||
expect(e.message).toBe("Missing props 'message' (widget 'TestWidget')");
|
||||
}
|
||||
});
|
||||
|
||||
test("props: list of strings", async () => {
|
||||
class TestWidget extends Widget {
|
||||
static props = ["message"];
|
||||
static template = xml`<div>hey</div>`;
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env);
|
||||
}).toThrow("Missing props 'message' (widget 'TestWidget')");
|
||||
}).toThrow("Missing props 'message' (component 'TestWidget')");
|
||||
});
|
||||
|
||||
test("validate simple types", async () => {
|
||||
@@ -80,6 +70,7 @@ describe("props validation", () => {
|
||||
|
||||
for (let test of Tests) {
|
||||
let TestWidget = class extends Widget {
|
||||
static template = xml`<div>hey</div>`;
|
||||
static props = { p: test.type };
|
||||
};
|
||||
|
||||
@@ -93,7 +84,7 @@ describe("props validation", () => {
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: test.ko });
|
||||
}).toThrow("Props 'p' of invalid type in widget");
|
||||
}).toThrow("Props 'p' of invalid type in component");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -109,6 +100,7 @@ describe("props validation", () => {
|
||||
|
||||
for (let test of Tests) {
|
||||
let TestWidget = class extends Widget {
|
||||
static template = xml`<div>hey</div>`;
|
||||
static props = { p: { type: test.type } };
|
||||
};
|
||||
|
||||
@@ -122,13 +114,13 @@ describe("props validation", () => {
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: test.ko });
|
||||
}).toThrow("Props 'p' of invalid type in widget");
|
||||
}).toThrow("Props 'p' of invalid type in component");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
test("can validate a prop with multiple types", async () => {
|
||||
let TestWidget = class extends Widget {
|
||||
static template = xml`<div>hey</div>`;
|
||||
static props = { p: [String, Boolean] };
|
||||
};
|
||||
|
||||
@@ -139,28 +131,28 @@ describe("props validation", () => {
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: 1 });
|
||||
}).toThrow("Props 'p' of invalid type in widget");
|
||||
}).toThrow("Props 'p' of invalid type in component");
|
||||
});
|
||||
|
||||
test("can validate an optional props", async () => {
|
||||
let TestWidget = class extends Widget {
|
||||
static template = xml`<div>hey</div>`;
|
||||
static props = { p: { type: String, optional: true } };
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: "hey" });
|
||||
new TestWidget(env, { });
|
||||
new TestWidget(env, {});
|
||||
}).not.toThrow();
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: 1 });
|
||||
}).toThrow();
|
||||
|
||||
});
|
||||
|
||||
|
||||
test("can validate an array with given primitive type", async () => {
|
||||
let TestWidget = class extends Widget {
|
||||
static template = xml`<div>hey</div>`;
|
||||
static props = { p: { type: Array, element: String } };
|
||||
};
|
||||
|
||||
@@ -180,6 +172,7 @@ describe("props validation", () => {
|
||||
|
||||
test("can validate an array with multiple sub element types", async () => {
|
||||
let TestWidget = class extends Widget {
|
||||
static template = xml`<div>hey</div>`;
|
||||
static props = { p: { type: Array, element: [String, Boolean] } };
|
||||
};
|
||||
|
||||
@@ -196,6 +189,7 @@ describe("props validation", () => {
|
||||
|
||||
test("can validate an object with simple shape", async () => {
|
||||
let TestWidget = class extends Widget {
|
||||
static template = xml`<div>hey</div>`;
|
||||
static props = {
|
||||
p: { type: Object, shape: { id: Number, url: String } }
|
||||
};
|
||||
@@ -217,12 +211,13 @@ describe("props validation", () => {
|
||||
|
||||
test("can validate recursively complicated prop def", async () => {
|
||||
let TestWidget = class extends Widget {
|
||||
static template = xml`<div>hey</div>`;
|
||||
static props = {
|
||||
p: {
|
||||
type: Object,
|
||||
shape: {
|
||||
id: Number,
|
||||
url: [Boolean, {type: Array, element: Number}],
|
||||
url: [Boolean, { type: Array, element: Number }]
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -230,21 +225,109 @@ describe("props validation", () => {
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: { id: 1, url: true } });
|
||||
new TestWidget(env, { p: { id: 1, url: [12]} });
|
||||
new TestWidget(env, { p: { id: 1, url: [12] } });
|
||||
}).not.toThrow();
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: { id: 1, url: [12, true]} });
|
||||
new TestWidget(env, { p: { id: 1, url: [12, true] } });
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
test("props are validated in dev mode (code snapshot)", async () => {
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="App">
|
||||
<Child message="1"/>
|
||||
</div>
|
||||
<div t-name="Child"><t t-esc="props.message"/></div>
|
||||
</templates>`);
|
||||
|
||||
class Child extends Widget {
|
||||
static props = ["message"];
|
||||
}
|
||||
class App extends Widget {
|
||||
static components = { Child };
|
||||
}
|
||||
const app = new App(env);
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
|
||||
// need to make sure there are 2 call to update props. one at component
|
||||
// creation, and one at update time.
|
||||
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("props: list of strings with optional props", async () => {
|
||||
class TestWidget extends Widget {
|
||||
static props = ["message", "someProp?"];
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
QWeb.utils.validateProps(TestWidget, { someProp: 1 });
|
||||
}).toThrow();
|
||||
expect(() => {
|
||||
QWeb.utils.validateProps(TestWidget, { message: 1 });
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
test("props: can be defined with a boolean", async () => {
|
||||
class TestWidget extends Widget {
|
||||
static props = { message: true };
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
QWeb.utils.validateProps(TestWidget, {});
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
test("props: extra props cause an error", async () => {
|
||||
class TestWidget extends Widget {
|
||||
static props = ["message"];
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
QWeb.utils.validateProps(TestWidget, { message: 1, flag: true });
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
test("props: extra props cause an error, part 2", async () => {
|
||||
class TestWidget extends Widget {
|
||||
static props = { message: true };
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
QWeb.utils.validateProps(TestWidget, { message: 1, flag: true });
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
test("props: optional prop do not cause an error", async () => {
|
||||
class TestWidget extends Widget {
|
||||
static props = ["message?"];
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
QWeb.utils.validateProps(TestWidget, { message: 1 });
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
test("optional prop do not cause an error if value is undefined", async () => {
|
||||
class TestWidget extends Widget {
|
||||
static props = { message: { type: String, optional: true } };
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
QWeb.utils.validateProps(TestWidget, { message: undefined });
|
||||
}).not.toThrow();
|
||||
expect(() => {
|
||||
QWeb.utils.validateProps(TestWidget, { message: null });
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("default props", () => {
|
||||
test("can set default values", async () => {
|
||||
class TestWidget extends Widget {
|
||||
static defaultProps = {p: 4}
|
||||
static defaultProps = { p: 4 };
|
||||
static template = xml`<div>hey</div>`;
|
||||
}
|
||||
|
||||
const w = new TestWidget(env, {});
|
||||
@@ -253,11 +336,20 @@ describe("default props", () => {
|
||||
|
||||
test("default values are also set whenever component is updated", async () => {
|
||||
class TestWidget extends Widget {
|
||||
static defaultProps = {p: 4}
|
||||
static defaultProps = { p: 4 };
|
||||
}
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="TestWidget"><t t-esc="props.p"/></div>
|
||||
</templates>`);
|
||||
|
||||
const w = new TestWidget(env, {p: 1});
|
||||
await w._updateProps({});
|
||||
const w = new TestWidget(env, { p: 1 });
|
||||
await w.mount(fixture);
|
||||
expect(fixture.innerHTML).toMatchSnapshot();
|
||||
const fiber = w.__createFiber(false, undefined, undefined, undefined);
|
||||
await w.__updateProps({}, fiber);
|
||||
await w.render();
|
||||
expect(w.props.p).toBe(4);
|
||||
expect(fixture.innerHTML).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,176 @@
|
||||
import { makeTestEnv, makeTestFixture, nextTick } from "./helpers";
|
||||
import { Component, Env } 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.
|
||||
// - env: a WEnv, necessary to create new components
|
||||
|
||||
let fixture: HTMLElement;
|
||||
let env: Env;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = makeTestFixture();
|
||||
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(env);
|
||||
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(env);
|
||||
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(env);
|
||||
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("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(env);
|
||||
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(env);
|
||||
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(env);
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { EventBus } from "../src/event_bus";
|
||||
import { EventBus } from "../../src/core/event_bus";
|
||||
|
||||
describe("event bus behaviour", () => {
|
||||
test("can subscribe and be notified", () => {
|
||||
@@ -23,9 +23,7 @@ describe("event bus behaviour", () => {
|
||||
test("throw error if callback is undefined", () => {
|
||||
expect.assertions(1);
|
||||
const bus = new EventBus();
|
||||
expect(() => bus.on("event", {}, <any>undefined)).toThrow(
|
||||
`Missing callback`
|
||||
);
|
||||
expect(() => bus.on("event", {}, <any>undefined)).toThrow(`Missing callback`);
|
||||
});
|
||||
|
||||
test("can unsubscribe", () => {
|
||||
@@ -0,0 +1,466 @@
|
||||
import { Observer } from "../../src/core/observer";
|
||||
import { nextMicroTick } from "../helpers";
|
||||
|
||||
describe("observer", () => {
|
||||
test("properly observe objects", () => {
|
||||
const observer = new Observer();
|
||||
const obj: any = observer.observe({});
|
||||
|
||||
expect(typeof obj).toBe("object");
|
||||
expect(observer.revNumber(obj)).toBe(1);
|
||||
expect(observer.deepRevNumber(obj)).toBe(1);
|
||||
expect(observer.rev).toBe(1);
|
||||
|
||||
const obj2: any = observer.observe({ a: 1 });
|
||||
expect(observer.revNumber(obj2)).toBe(1);
|
||||
expect(observer.revNumber(obj)).toBe(1);
|
||||
expect(observer.deepRevNumber(obj)).toBe(1);
|
||||
expect(observer.rev).toBe(1);
|
||||
|
||||
obj2.a = 2;
|
||||
|
||||
expect(observer.revNumber(obj)).toBe(1);
|
||||
expect(observer.revNumber(obj2)).toBe(2);
|
||||
expect(observer.rev).toBe(2);
|
||||
|
||||
expect(obj2).toEqual({
|
||||
a: 2
|
||||
});
|
||||
});
|
||||
|
||||
test("properly handle null or undefined", () => {
|
||||
const observer = new Observer();
|
||||
const obj: any = observer.observe({ a: null, b: undefined });
|
||||
|
||||
expect(observer.revNumber(obj)).toBe(1);
|
||||
expect(observer.deepRevNumber(obj)).toBe(1);
|
||||
expect(observer.rev).toBe(1);
|
||||
|
||||
obj.a = 3;
|
||||
expect(observer.revNumber(obj)).toBe(2);
|
||||
expect(observer.deepRevNumber(obj)).toBe(2);
|
||||
expect(observer.rev).toBe(2);
|
||||
|
||||
obj.b = 5;
|
||||
expect(observer.revNumber(obj)).toBe(3);
|
||||
expect(observer.deepRevNumber(obj)).toBe(3);
|
||||
expect(observer.rev).toBe(3);
|
||||
|
||||
obj.a = null;
|
||||
obj.b = undefined;
|
||||
expect(observer.revNumber(obj)).toBe(5);
|
||||
expect(observer.deepRevNumber(obj)).toBe(5);
|
||||
expect(observer.rev).toBe(5);
|
||||
expect(obj).toEqual({
|
||||
a: null,
|
||||
b: undefined
|
||||
});
|
||||
});
|
||||
|
||||
test("properly handle dates", () => {
|
||||
const observer = new Observer();
|
||||
const date = new Date();
|
||||
const obj: any = observer.observe({ date });
|
||||
|
||||
expect(observer.revNumber(obj)).toBe(1);
|
||||
expect(observer.deepRevNumber(obj)).toBe(1);
|
||||
expect(observer.rev).toBe(1);
|
||||
expect(typeof obj.date.getFullYear()).toBe("number");
|
||||
expect(obj.date).toBe(date);
|
||||
|
||||
obj.date = new Date();
|
||||
|
||||
expect(observer.revNumber(obj)).toBe(2);
|
||||
expect(observer.deepRevNumber(obj)).toBe(2);
|
||||
expect(observer.rev).toBe(2);
|
||||
expect(obj.date).not.toBe(date);
|
||||
});
|
||||
|
||||
test("can change values in array", () => {
|
||||
const observer = new Observer();
|
||||
const obj: any = observer.observe({ arr: [1, 2] });
|
||||
|
||||
expect(Array.isArray(obj.arr)).toBe(true);
|
||||
expect(observer.revNumber(obj.arr)).toBe(1);
|
||||
expect(observer.deepRevNumber(obj.arr)).toBe(1);
|
||||
expect(observer.rev).toBe(1);
|
||||
|
||||
obj.arr[0] = "nope";
|
||||
expect(observer.revNumber(obj.arr)).toBe(2);
|
||||
expect(observer.deepRevNumber(obj.arr)).toBe(2);
|
||||
expect(observer.revNumber(obj)).toBe(1);
|
||||
expect(observer.deepRevNumber(obj)).toBe(2);
|
||||
expect(observer.rev).toBe(2);
|
||||
});
|
||||
|
||||
test("various object property changes", () => {
|
||||
const observer = new Observer();
|
||||
const obj: any = observer.observe({ a: 1 });
|
||||
|
||||
expect(observer.revNumber(obj)).toBe(1);
|
||||
expect(observer.deepRevNumber(obj)).toBe(1);
|
||||
expect(observer.rev).toBe(1);
|
||||
|
||||
obj.a = 2;
|
||||
|
||||
expect(observer.revNumber(obj)).toBe(2);
|
||||
expect(observer.deepRevNumber(obj)).toBe(2);
|
||||
expect(observer.rev).toBe(2);
|
||||
|
||||
// same value again
|
||||
obj.a = 2;
|
||||
expect(observer.revNumber(obj)).toBe(2);
|
||||
expect(observer.deepRevNumber(obj)).toBe(2);
|
||||
expect(observer.rev).toBe(2);
|
||||
|
||||
obj.a = 3;
|
||||
expect(observer.revNumber(obj)).toBe(3);
|
||||
expect(observer.deepRevNumber(obj)).toBe(3);
|
||||
expect(observer.rev).toBe(3);
|
||||
});
|
||||
|
||||
test("properly observe arrays", () => {
|
||||
const observer = new Observer();
|
||||
const arr: any = observer.observe([]);
|
||||
|
||||
expect(Array.isArray(arr)).toBe(true);
|
||||
expect(arr.length).toBe(0);
|
||||
expect(observer.revNumber(arr)).toBe(1);
|
||||
expect(observer.deepRevNumber(arr)).toBe(1);
|
||||
expect(observer.rev).toBe(1);
|
||||
|
||||
arr.push(1);
|
||||
expect(observer.revNumber(arr)).toBe(2);
|
||||
expect(observer.deepRevNumber(arr)).toBe(2);
|
||||
expect(observer.rev).toBe(2);
|
||||
expect(arr.length).toBe(1);
|
||||
expect(arr).toEqual([1]);
|
||||
|
||||
arr.splice(1, 0, "hey");
|
||||
expect(observer.revNumber(arr)).toBe(3);
|
||||
expect(observer.deepRevNumber(arr)).toBe(3);
|
||||
expect(observer.rev).toBe(3);
|
||||
expect(arr).toEqual([1, "hey"]);
|
||||
expect(arr.length).toBe(2);
|
||||
|
||||
arr.unshift("lindemans");
|
||||
//it generates 3 primitive operations
|
||||
expect(observer.revNumber(arr)).toBe(6);
|
||||
expect(observer.deepRevNumber(arr)).toBe(6);
|
||||
expect(observer.rev).toBe(6);
|
||||
expect(arr).toEqual(["lindemans", 1, "hey"]);
|
||||
expect(arr.length).toBe(3);
|
||||
|
||||
arr.reverse();
|
||||
//it generates 2 primitive operations
|
||||
expect(observer.revNumber(arr)).toBe(8);
|
||||
expect(observer.deepRevNumber(arr)).toBe(8);
|
||||
expect(observer.rev).toBe(8);
|
||||
expect(arr).toEqual(["hey", 1, "lindemans"]);
|
||||
expect(arr.length).toBe(3);
|
||||
|
||||
arr.pop(); // one set, one delete
|
||||
expect(observer.revNumber(arr)).toBe(10);
|
||||
expect(observer.deepRevNumber(arr)).toBe(10);
|
||||
expect(observer.rev).toBe(10);
|
||||
expect(arr).toEqual(["hey", 1]);
|
||||
expect(arr.length).toBe(2);
|
||||
|
||||
arr.shift(); // 2 sets, 1 delete
|
||||
expect(observer.revNumber(arr)).toBe(13);
|
||||
expect(observer.deepRevNumber(arr)).toBe(13);
|
||||
expect(observer.rev).toBe(13);
|
||||
expect(arr).toEqual([1]);
|
||||
expect(arr.length).toBe(1);
|
||||
});
|
||||
|
||||
test("object pushed into arrays are observed", () => {
|
||||
const observer = new Observer();
|
||||
const arr: any = observer.observe([]);
|
||||
expect(observer.rev).toBe(1);
|
||||
|
||||
arr.push({ kriek: 5 });
|
||||
expect(observer.rev).toBe(2);
|
||||
expect(observer.revNumber(arr)).toBe(2);
|
||||
expect(observer.revNumber(arr[0])).toBe(2);
|
||||
|
||||
arr[0].kriek = 6;
|
||||
|
||||
expect(observer.rev).toBe(3);
|
||||
expect(observer.revNumber(arr)).toBe(2);
|
||||
expect(observer.deepRevNumber(arr)).toBe(3);
|
||||
expect(observer.revNumber(arr[0])).toBe(3);
|
||||
});
|
||||
|
||||
test("set new property on observed object", async () => {
|
||||
const observer = new Observer();
|
||||
observer.notifyCB = jest.fn();
|
||||
const state: any = observer.observe({ a: 1 });
|
||||
expect(observer.notifyCB).toBeCalledTimes(0);
|
||||
expect(observer.rev).toBe(1);
|
||||
expect(observer.revNumber(state)).toBe(1);
|
||||
|
||||
state.b = 8;
|
||||
|
||||
await nextMicroTick();
|
||||
|
||||
expect(observer.notifyCB).toBeCalledTimes(1);
|
||||
expect(observer.rev).toBe(2);
|
||||
expect(observer.revNumber(state)).toBe(2);
|
||||
|
||||
expect(state.b).toBe(8);
|
||||
});
|
||||
|
||||
test("delete property from observed object", async () => {
|
||||
const observer = new Observer();
|
||||
observer.notifyCB = jest.fn();
|
||||
const state: any = observer.observe({ a: 1, b: 8 });
|
||||
observer.observe(state);
|
||||
|
||||
expect(observer.notifyCB).toBeCalledTimes(0);
|
||||
expect(observer.rev).toBe(1);
|
||||
expect(observer.revNumber(state)).toBe(1);
|
||||
|
||||
delete state.b;
|
||||
await nextMicroTick();
|
||||
|
||||
expect(observer.notifyCB).toBeCalledTimes(1);
|
||||
expect(observer.rev).toBe(2);
|
||||
expect(observer.revNumber(state)).toBe(2);
|
||||
|
||||
expect(state).toEqual({ a: 1 });
|
||||
});
|
||||
|
||||
test("set element in observed array", async () => {
|
||||
const observer = new Observer();
|
||||
observer.notifyCB = jest.fn();
|
||||
const state: any = observer.observe(["a"]);
|
||||
|
||||
expect(observer.rev).toBe(1);
|
||||
expect(observer.revNumber(state)).toBe(1);
|
||||
expect(observer.deepRevNumber(state)).toBe(1);
|
||||
expect(observer.notifyCB).toBeCalledTimes(0);
|
||||
|
||||
state[1] = "b";
|
||||
|
||||
await nextMicroTick();
|
||||
|
||||
expect(observer.rev).toBe(2);
|
||||
expect(observer.revNumber(state)).toBe(2);
|
||||
expect(observer.deepRevNumber(state)).toBe(2);
|
||||
expect(observer.notifyCB).toBeCalledTimes(1);
|
||||
|
||||
expect(state).toEqual(["a", "b"]);
|
||||
});
|
||||
|
||||
test("properly observe arrays in object", () => {
|
||||
const observer = new Observer();
|
||||
const state: any = observer.observe({ arr: [] });
|
||||
|
||||
expect(observer.rev).toBe(1);
|
||||
expect(observer.revNumber(state.arr)).toBe(1);
|
||||
expect(observer.deepRevNumber(state.arr)).toBe(1);
|
||||
expect(state.arr.length).toBe(0);
|
||||
|
||||
state.arr.push(1);
|
||||
expect(observer.rev).toBe(2);
|
||||
expect(observer.revNumber(state.arr)).toBe(2);
|
||||
expect(observer.deepRevNumber(state.arr)).toBe(2);
|
||||
expect(state.arr.length).toBe(1);
|
||||
});
|
||||
|
||||
test("properly observe objects in array", () => {
|
||||
const observer = new Observer();
|
||||
const state: any = observer.observe({ arr: [{ something: 1 }] });
|
||||
observer.observe(state);
|
||||
|
||||
expect(observer.rev).toBe(1);
|
||||
expect(observer.revNumber(state.arr)).toBe(1);
|
||||
expect(observer.revNumber(state.arr[0])).toBe(1);
|
||||
|
||||
state.arr[0].something = 2;
|
||||
expect(observer.rev).toBe(2);
|
||||
expect(observer.revNumber(state.arr)).toBe(1);
|
||||
expect(observer.revNumber(state.arr[0])).toBe(2);
|
||||
});
|
||||
|
||||
test("properly observe objects in object", () => {
|
||||
const observer = new Observer();
|
||||
const state: any = observer.observe({ a: { b: 1 } });
|
||||
|
||||
expect(observer.rev).toBe(1);
|
||||
expect(observer.revNumber(state)).toBe(1);
|
||||
expect(observer.revNumber(state.a)).toBe(1);
|
||||
|
||||
state.a.b = 2;
|
||||
expect(observer.rev).toBe(2);
|
||||
expect(observer.revNumber(state)).toBe(1);
|
||||
expect(observer.revNumber(state.a)).toBe(2);
|
||||
});
|
||||
|
||||
test("reobserve new object values", () => {
|
||||
const observer = new Observer();
|
||||
const obj: any = observer.observe({ a: 1 });
|
||||
|
||||
expect(observer.rev).toBe(1);
|
||||
expect(observer.revNumber(obj)).toBe(1);
|
||||
|
||||
obj.a = { b: 2 };
|
||||
|
||||
expect(observer.rev).toBe(2);
|
||||
expect(observer.revNumber(obj)).toBe(2);
|
||||
expect(observer.revNumber(obj.a)).toBe(2);
|
||||
obj.a.b = 3;
|
||||
expect(observer.rev).toBe(3);
|
||||
expect(observer.revNumber(obj)).toBe(2);
|
||||
expect(observer.revNumber(obj.a)).toBe(3);
|
||||
});
|
||||
|
||||
test("deep observe misc changes", () => {
|
||||
const observer = new Observer();
|
||||
const state: any = observer.observe({ o: { a: 1 }, arr: [1], n: 13 });
|
||||
expect(observer.revNumber(state)).toBe(1);
|
||||
expect(observer.deepRevNumber(state)).toBe(1);
|
||||
|
||||
state.o.a = 2;
|
||||
expect(observer.rev).toBe(2);
|
||||
expect(observer.revNumber(state)).toBe(1);
|
||||
expect(observer.deepRevNumber(state)).toBe(2);
|
||||
|
||||
state.arr.push(2);
|
||||
expect(observer.rev).toBe(3);
|
||||
expect(observer.revNumber(state)).toBe(1);
|
||||
expect(observer.deepRevNumber(state)).toBe(3);
|
||||
|
||||
state.n = 155;
|
||||
expect(observer.rev).toBe(4);
|
||||
expect(observer.revNumber(state)).toBe(2);
|
||||
expect(observer.deepRevNumber(state)).toBe(4);
|
||||
});
|
||||
|
||||
test("properly handle already observed state", () => {
|
||||
const observer = new Observer();
|
||||
const obj1: any = observer.observe({ a: 1 });
|
||||
const obj2: any = observer.observe({ b: 1 });
|
||||
|
||||
expect(observer.revNumber(obj1)).toBe(1);
|
||||
expect(observer.revNumber(obj2)).toBe(1);
|
||||
|
||||
obj1.a = 2;
|
||||
obj2.b = 3;
|
||||
expect(observer.revNumber(obj1)).toBe(2);
|
||||
expect(observer.revNumber(obj2)).toBe(2);
|
||||
|
||||
obj2.b = obj1;
|
||||
expect(observer.revNumber(obj1)).toBe(2);
|
||||
expect(observer.revNumber(obj2)).toBe(3);
|
||||
});
|
||||
|
||||
test("can set a property more than once", () => {
|
||||
const observer = new Observer();
|
||||
const obj: any = observer.observe({});
|
||||
|
||||
expect(observer.revNumber(obj)).toBe(1);
|
||||
expect(observer.deepRevNumber(obj)).toBe(1);
|
||||
expect(observer.rev).toBe(1);
|
||||
|
||||
obj.aku = "always finds annoying problems";
|
||||
expect(observer.revNumber(obj)).toBe(2);
|
||||
expect(observer.deepRevNumber(obj)).toBe(2);
|
||||
expect(observer.rev).toBe(2);
|
||||
|
||||
obj.aku = "always finds good problems";
|
||||
|
||||
expect(observer.revNumber(obj)).toBe(3);
|
||||
expect(observer.deepRevNumber(obj)).toBe(3);
|
||||
expect(observer.rev).toBe(3);
|
||||
});
|
||||
|
||||
test("properly handle swapping elements", () => {
|
||||
const observer = new Observer();
|
||||
const obj: any = observer.observe({ a: { arr: [] }, b: 1 });
|
||||
|
||||
// swap a and b
|
||||
const b = obj.b;
|
||||
obj.b = obj.a;
|
||||
obj.a = b;
|
||||
expect(observer.rev).toBe(3);
|
||||
|
||||
// push something into array to make sure it works
|
||||
obj.b.arr.push("blanche");
|
||||
expect(observer.rev).toBe(4);
|
||||
});
|
||||
|
||||
test("properly handle assigning observed obj containing array", () => {
|
||||
const observer = new Observer();
|
||||
const obj: any = observer.observe({ a: { arr: [], val: "test" } });
|
||||
|
||||
expect(observer.rev).toBe(1);
|
||||
obj.a = { ...obj.a, val: "test2" };
|
||||
expect(observer.rev).toBe(2);
|
||||
|
||||
// push something into array to make sure it works
|
||||
obj.a.arr.push("blanche");
|
||||
expect(observer.rev).toBe(3);
|
||||
});
|
||||
|
||||
test("accept cycles in observed state", () => {
|
||||
const observer = new Observer();
|
||||
let obj1: any = {};
|
||||
let obj2: any = { b: obj1, key: 1 };
|
||||
obj1.a = obj2;
|
||||
obj1 = observer.observe(obj1);
|
||||
obj2 = obj1.a;
|
||||
|
||||
expect(observer.revNumber(obj1)).toBe(1);
|
||||
expect(observer.revNumber(obj2)).toBe(1);
|
||||
|
||||
obj2.key = 3;
|
||||
expect(observer.revNumber(obj1)).toBe(1);
|
||||
expect(observer.revNumber(obj2)).toBe(2);
|
||||
});
|
||||
|
||||
test("call callback when state is changed", async () => {
|
||||
const observer = new Observer();
|
||||
observer.notifyCB = jest.fn();
|
||||
const obj: any = observer.observe({ a: 1, b: { c: 2 }, d: [{ e: 3 }], f: 4 });
|
||||
|
||||
expect(observer.notifyCB).toBeCalledTimes(0);
|
||||
|
||||
obj.a = 2;
|
||||
await nextMicroTick();
|
||||
expect(observer.notifyCB).toBeCalledTimes(1);
|
||||
|
||||
obj.b.c = 3;
|
||||
await nextMicroTick();
|
||||
expect(observer.notifyCB).toBeCalledTimes(2);
|
||||
|
||||
obj.d[0].e = 5;
|
||||
await nextMicroTick();
|
||||
expect(observer.notifyCB).toBeCalledTimes(3);
|
||||
|
||||
obj.a = 111;
|
||||
obj.f = 222;
|
||||
await nextMicroTick();
|
||||
expect(observer.notifyCB).toBeCalledTimes(4);
|
||||
});
|
||||
|
||||
test("throw error when state is mutated in object if allowMutation=false", async () => {
|
||||
const observer = new Observer();
|
||||
observer.allowMutations = false;
|
||||
const obj: any = observer.observe({ a: 1 });
|
||||
expect(() => {
|
||||
obj.a = 2;
|
||||
}).toThrow('Observed state cannot be changed here! (key: "a", val: "2")');
|
||||
});
|
||||
|
||||
test("throw error when state is mutated in array if allowMutation=false", async () => {
|
||||
const observer = new Observer();
|
||||
observer.allowMutations = false;
|
||||
const obj: any = observer.observe({ a: [1] });
|
||||
|
||||
expect(() => {
|
||||
obj.a.push(2);
|
||||
}).toThrow('Observed state cannot be changed here! (key: "1", val: "2")');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
+53
-10
@@ -1,9 +1,31 @@
|
||||
import { Env } from "../src/component";
|
||||
import { EvalContext, QWeb, UTILS } from "../src/qweb_core";
|
||||
import { Env } from "../src/component/component";
|
||||
import { EvalContext, QWeb } from "../src/qweb/qweb";
|
||||
import { patch } from "../src/vdom";
|
||||
import "../src/qweb_directives";
|
||||
import "../src/qweb_extensions";
|
||||
import "../src/qweb/base_directives";
|
||||
import "../src/qweb/extensions";
|
||||
import "../src/component/directive";
|
||||
|
||||
// Some static cleanup
|
||||
let nextSlotId;
|
||||
let slots;
|
||||
let nextId;
|
||||
let TEMPLATES;
|
||||
|
||||
beforeEach(() => {
|
||||
nextSlotId = QWeb.nextSlotId;
|
||||
slots = Object.assign({}, QWeb.slots);
|
||||
nextId = QWeb.nextId;
|
||||
TEMPLATES = Object.assign({}, QWeb.TEMPLATES);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
QWeb.nextSlotId = nextSlotId;
|
||||
QWeb.slots = slots;
|
||||
QWeb.nextId = nextId;
|
||||
QWeb.TEMPLATES = TEMPLATES;
|
||||
});
|
||||
|
||||
// helpers
|
||||
export function nextMicroTick(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
@@ -68,26 +90,47 @@ export function renderToDOM(
|
||||
return result.elm as HTMLElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a template to an html string. The big difference with the
|
||||
* renderToString method in QWeb is that we use the renderToDom method, which
|
||||
* snapshots the resulting template function. Doing so gives us a large body
|
||||
* of reference to evaluate changes in the generated QWeb code.
|
||||
*
|
||||
* Note that the result of renderToString is guaranteed to be the same as the
|
||||
* one from QWeb.
|
||||
*/
|
||||
export function renderToString(
|
||||
qweb: QWeb,
|
||||
t: string,
|
||||
context: EvalContext = {}
|
||||
context: EvalContext = {},
|
||||
extra?: any
|
||||
): string {
|
||||
const node = renderToDOM(qweb, t, context);
|
||||
return node instanceof Text ? node.textContent! : node.outerHTML;
|
||||
const node = renderToDOM(qweb, t, context, extra);
|
||||
const result = node instanceof Text ? node.textContent! : node.outerHTML;
|
||||
if (result !== qweb.renderToString(t, context, extra)) {
|
||||
throw new Error("HTML string returned by renderToString helper does not match QWeb render");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// hereafter, we define two helpers to patch/unpatch the nextFrame utils. This
|
||||
// is useful for animations tests, as we hook before repaints to trigger
|
||||
// animations (thanks to requestAnimationFrame). Patching nextFrame allows to
|
||||
// simulate calls to this hook. One must not forget to unpatch afterwards.
|
||||
let nextFrame = UTILS.nextFrame;
|
||||
let nextFrame = QWeb.utils.nextFrame;
|
||||
export function patchNextFrame(f: Function) {
|
||||
UTILS.nextFrame = (cb: () => void) => {
|
||||
QWeb.utils.nextFrame = (cb: () => void) => {
|
||||
setTimeout(() => f(cb));
|
||||
};
|
||||
}
|
||||
|
||||
export function unpatchNextFrame() {
|
||||
UTILS.nextFrame = nextFrame;
|
||||
QWeb.utils.nextFrame = nextFrame;
|
||||
}
|
||||
|
||||
export async function editInput(input: HTMLInputElement | HTMLTextAreaElement, value: string) {
|
||||
input.value = value;
|
||||
input.dispatchEvent(new Event("input"));
|
||||
input.dispatchEvent(new Event("change"));
|
||||
return nextTick();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,493 @@
|
||||
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();
|
||||
});
|
||||
|
||||
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(env);
|
||||
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(env) {
|
||||
super(env);
|
||||
useMyHook();
|
||||
}
|
||||
}
|
||||
const component = new MyComponent(env);
|
||||
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(env) {
|
||||
super(env);
|
||||
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(env);
|
||||
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(env) {
|
||||
super(env);
|
||||
useMyHook();
|
||||
}
|
||||
mounted() {
|
||||
steps.push("comp:mounted");
|
||||
}
|
||||
willUnmount() {
|
||||
steps.push("comp:willunmount");
|
||||
}
|
||||
}
|
||||
const component = new MyComponent(env);
|
||||
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(env) {
|
||||
super(env);
|
||||
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(env);
|
||||
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(env) {
|
||||
super(env);
|
||||
useMyHook(1);
|
||||
useMyHook(2);
|
||||
}
|
||||
}
|
||||
const component = new MyComponent(env);
|
||||
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(env);
|
||||
await counter.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><button>0</button></div>");
|
||||
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(env);
|
||||
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("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(env) {
|
||||
super(env);
|
||||
useMyHook();
|
||||
}
|
||||
}
|
||||
|
||||
const component = new MyComponent(env);
|
||||
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(env) {
|
||||
super(env);
|
||||
useMyHook();
|
||||
}
|
||||
willPatch() {
|
||||
steps.push("comp:willPatch");
|
||||
}
|
||||
patched() {
|
||||
steps.push("comp:patched");
|
||||
}
|
||||
}
|
||||
const component = new MyComponent(env);
|
||||
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(env) {
|
||||
super(env);
|
||||
useMyHook(1);
|
||||
useMyHook(2);
|
||||
}
|
||||
}
|
||||
const component = new MyComponent(env);
|
||||
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(env) {
|
||||
super(env);
|
||||
useAutofocus("input2");
|
||||
}
|
||||
}
|
||||
|
||||
const component = new SomeComponent(env);
|
||||
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(env) {
|
||||
super(env);
|
||||
useAutofocus("input2");
|
||||
}
|
||||
}
|
||||
|
||||
const component = new SomeComponent(env);
|
||||
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(env) {
|
||||
super(env);
|
||||
useSubEnv({ val: 3 });
|
||||
}
|
||||
}
|
||||
const component = new TestComponent(env);
|
||||
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(env) {
|
||||
super(env);
|
||||
useSubEnv({ val: 5 });
|
||||
}
|
||||
}
|
||||
|
||||
class Parent extends Component<any, any> {
|
||||
static template = xml`<div><t t-esc="env.val"/><Child/></div>`;
|
||||
static components = { Child };
|
||||
constructor(env) {
|
||||
super(env);
|
||||
useSubEnv({ val: 3 });
|
||||
}
|
||||
}
|
||||
const component = new Parent(env);
|
||||
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(env);
|
||||
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"]);
|
||||
});
|
||||
});
|
||||
@@ -1,323 +0,0 @@
|
||||
import { Observer } from "../src/observer";
|
||||
import { nextMicroTick } from "./helpers";
|
||||
|
||||
describe("observer", () => {
|
||||
test("properly observe objects", () => {
|
||||
const observer = new Observer();
|
||||
const obj: any = {};
|
||||
|
||||
observer.observe(obj);
|
||||
expect(obj.__owl__.rev).toBe(1);
|
||||
expect(observer.rev).toBe(1);
|
||||
|
||||
const ob2: any = { a: 1 };
|
||||
observer.observe(ob2);
|
||||
expect(ob2.__owl__.rev).toBe(1);
|
||||
ob2.a = 2;
|
||||
expect(observer.rev).toBe(2);
|
||||
expect(ob2.__owl__.rev).toBe(2);
|
||||
|
||||
ob2.b = 3;
|
||||
expect(observer.rev).toBe(2);
|
||||
expect(ob2.__owl__.rev).toBe(2);
|
||||
|
||||
observer.set(ob2, "b", 4);
|
||||
expect(observer.rev).toBe(3);
|
||||
expect(ob2.__owl__.rev).toBe(3);
|
||||
});
|
||||
|
||||
test("properly handle null or undefined", () => {
|
||||
const observer = new Observer();
|
||||
const obj: any = { a: null, b: undefined };
|
||||
|
||||
observer.observe(obj);
|
||||
expect(obj.__owl__.rev).toBe(1);
|
||||
expect(observer.rev).toBe(1);
|
||||
|
||||
obj.a = 3;
|
||||
expect(obj.__owl__.rev).toBe(2);
|
||||
|
||||
obj.b = 5;
|
||||
expect(obj.__owl__.rev).toBe(3);
|
||||
|
||||
obj.a = null;
|
||||
obj.b = undefined;
|
||||
expect(obj.__owl__.rev).toBe(5);
|
||||
});
|
||||
|
||||
test("can change values in array", () => {
|
||||
const observer = new Observer();
|
||||
const obj: any = { arr: [1, 2] };
|
||||
|
||||
observer.observe(obj);
|
||||
expect(obj.arr.__owl__.rev).toBe(1);
|
||||
expect(observer.rev).toBe(1);
|
||||
|
||||
obj.arr[0] = "nope";
|
||||
expect(obj.arr.__owl__.rev).toBe(1);
|
||||
expect(observer.rev).toBe(1);
|
||||
|
||||
observer.set(obj.arr, 0, "yep");
|
||||
expect(obj.arr.__owl__.rev).toBe(2);
|
||||
expect(observer.rev).toBe(2);
|
||||
});
|
||||
|
||||
test("various object property changes", () => {
|
||||
const observer = new Observer();
|
||||
const obj: any = { a: 1 };
|
||||
observer.observe(obj);
|
||||
expect(obj.__owl__.rev).toBe(1);
|
||||
obj.a = 2;
|
||||
expect(observer.rev).toBe(2);
|
||||
expect(obj.__owl__.rev).toBe(2);
|
||||
|
||||
// same value again
|
||||
obj.a = 2;
|
||||
expect(observer.rev).toBe(2);
|
||||
expect(obj.__owl__.rev).toBe(2);
|
||||
|
||||
obj.a = 3;
|
||||
expect(observer.rev).toBe(3);
|
||||
expect(obj.__owl__.rev).toBe(3);
|
||||
});
|
||||
|
||||
test("properly observe arrays", () => {
|
||||
const observer = new Observer();
|
||||
const arr: any = [];
|
||||
observer.observe(arr);
|
||||
expect(arr.__owl__.rev).toBe(1);
|
||||
expect(observer.rev).toBe(1);
|
||||
expect(arr.length).toBe(0);
|
||||
|
||||
arr.push(1);
|
||||
expect(arr.__owl__.rev).toBe(2);
|
||||
expect(observer.rev).toBe(2);
|
||||
expect(arr.length).toBe(1);
|
||||
|
||||
arr.splice(1, 0, "hey");
|
||||
expect(arr.__owl__.rev).toBe(3);
|
||||
expect(observer.rev).toBe(3);
|
||||
expect(arr.length).toBe(2);
|
||||
|
||||
arr.unshift("lindemans");
|
||||
expect(arr.__owl__.rev).toBe(4);
|
||||
|
||||
arr.reverse();
|
||||
expect(arr.__owl__.rev).toBe(5);
|
||||
|
||||
arr.pop();
|
||||
expect(arr.__owl__.rev).toBe(6);
|
||||
|
||||
arr.shift();
|
||||
expect(arr.__owl__.rev).toBe(7);
|
||||
|
||||
arr.sort();
|
||||
expect(arr.__owl__.rev).toBe(8);
|
||||
|
||||
expect(arr).toEqual([1]);
|
||||
});
|
||||
|
||||
test("object pushed into arrays are observed", () => {
|
||||
const observer = new Observer();
|
||||
const arr: any = [];
|
||||
observer.observe(arr);
|
||||
expect(observer.rev).toBe(1);
|
||||
|
||||
arr.push({ kriek: 5 });
|
||||
expect(observer.rev).toBe(2);
|
||||
expect(arr.__owl__.rev).toBe(2);
|
||||
expect(arr[0].__owl__.rev).toBe(2);
|
||||
|
||||
arr[0].kriek = 6;
|
||||
expect(observer.rev).toBe(3);
|
||||
expect(arr.__owl__.rev).toBe(2);
|
||||
expect(arr[0].__owl__.rev).toBe(3);
|
||||
});
|
||||
|
||||
test("properly observe arrays in object", () => {
|
||||
const observer = new Observer();
|
||||
const state: any = { arr: [] };
|
||||
observer.observe(state);
|
||||
expect(state.arr.__owl__.rev).toBe(1);
|
||||
expect(observer.rev).toBe(1);
|
||||
expect(state.arr.length).toBe(0);
|
||||
|
||||
state.arr.push(1);
|
||||
expect(state.arr.__owl__.rev).toBe(2);
|
||||
expect(observer.rev).toBe(2);
|
||||
expect(state.arr.length).toBe(1);
|
||||
});
|
||||
|
||||
test("properly observe objects in array", () => {
|
||||
const observer = new Observer();
|
||||
const state: any = { arr: [{ something: 1 }] };
|
||||
observer.observe(state);
|
||||
expect(state.arr.__owl__.rev).toBe(1);
|
||||
expect(state.arr[0].__owl__.rev).toBe(1);
|
||||
|
||||
state.arr[0].something = 2;
|
||||
expect(state.arr.__owl__.rev).toBe(1);
|
||||
expect(state.arr[0].__owl__.rev).toBe(2);
|
||||
});
|
||||
|
||||
test("properly observe objects in object", () => {
|
||||
const observer = new Observer();
|
||||
const state: any = { a: { b: 1 } };
|
||||
observer.observe(state);
|
||||
expect(state.__owl__.rev).toBe(1);
|
||||
expect(state.a.__owl__.rev).toBe(1);
|
||||
|
||||
state.a.b = 2;
|
||||
expect(state.__owl__.rev).toBe(1);
|
||||
expect(state.a.__owl__.rev).toBe(2);
|
||||
});
|
||||
|
||||
test("reobserve new object values", () => {
|
||||
const observer = new Observer();
|
||||
const obj: any = { a: 1 };
|
||||
observer.observe(obj);
|
||||
expect(obj.__owl__.rev).toBe(1);
|
||||
obj.a = { b: 2 };
|
||||
expect(observer.rev).toBe(2);
|
||||
expect(obj.__owl__.rev).toBe(2);
|
||||
|
||||
// we start at 2 because it is the new observer rev number
|
||||
expect(obj.a.__owl__.rev).toBe(2);
|
||||
|
||||
obj.a.b = 3;
|
||||
expect(observer.rev).toBe(3);
|
||||
expect(obj.__owl__.rev).toBe(2);
|
||||
expect(obj.a.__owl__.rev).toBe(3);
|
||||
});
|
||||
|
||||
test("deep observe misc changes", () => {
|
||||
const observer = new Observer();
|
||||
const state: any = { o: { a: 1 }, arr: [1], n: 13 };
|
||||
observer.observe(state);
|
||||
expect(state.__owl__.rev).toBe(1);
|
||||
expect(state.__owl__.deepRev).toBe(1);
|
||||
|
||||
state.o.a = 2;
|
||||
expect(observer.rev).toBe(2);
|
||||
expect(state.__owl__.rev).toBe(1);
|
||||
expect(state.__owl__.deepRev).toBe(2);
|
||||
|
||||
state.arr.push(2);
|
||||
expect(state.__owl__.rev).toBe(1);
|
||||
expect(state.__owl__.deepRev).toBe(3);
|
||||
|
||||
state.n = 155;
|
||||
expect(state.__owl__.rev).toBe(2);
|
||||
expect(state.__owl__.deepRev).toBe(4);
|
||||
});
|
||||
|
||||
test("properly handle already observed state", () => {
|
||||
const observer = new Observer();
|
||||
const obj1: any = { a: 1 };
|
||||
const obj2: any = { b: 1 };
|
||||
observer.observe(obj1);
|
||||
observer.observe(obj2);
|
||||
expect(obj1.__owl__.rev).toBe(1);
|
||||
expect(obj2.__owl__.rev).toBe(1);
|
||||
|
||||
obj1.a = 2;
|
||||
obj2.b = 3;
|
||||
expect(obj1.__owl__.rev).toBe(2);
|
||||
expect(obj2.__owl__.rev).toBe(2);
|
||||
|
||||
obj2.b = obj1;
|
||||
expect(obj1.__owl__.rev).toBe(2);
|
||||
expect(obj2.__owl__.rev).toBe(3);
|
||||
});
|
||||
|
||||
test("properly handle swapping elements", () => {
|
||||
const observer = new Observer();
|
||||
const obj: any = { a: { arr: [] }, b: 1 };
|
||||
observer.observe(obj);
|
||||
|
||||
// swap a and b
|
||||
const b = obj.b;
|
||||
obj.b = obj.a;
|
||||
obj.a = b;
|
||||
expect(observer.rev).toBe(3);
|
||||
|
||||
// push something into array to make sure it works
|
||||
obj.b.arr.push("blanche");
|
||||
expect(observer.rev).toBe(4);
|
||||
});
|
||||
|
||||
test("properly handle assigning observed obj containing array", () => {
|
||||
const observer = new Observer();
|
||||
const obj: any = { a: { arr: [], val: "test" } };
|
||||
observer.observe(obj);
|
||||
|
||||
obj.a = { ...obj.a, val: "test2" };
|
||||
expect(observer.rev).toBe(2);
|
||||
|
||||
// push something into array to make sure it works
|
||||
obj.a.arr.push("blanche");
|
||||
expect(observer.rev).toBe(3);
|
||||
});
|
||||
|
||||
test("accept cycles in observed state", () => {
|
||||
const observer = new Observer();
|
||||
const obj1: any = {};
|
||||
const obj2: any = { b: obj1, key: 1 };
|
||||
obj1.a = obj2;
|
||||
observer.observe(obj1);
|
||||
expect(obj1.__owl__.rev).toBe(1);
|
||||
expect(obj2.__owl__.rev).toBe(1);
|
||||
|
||||
obj2.key = 3;
|
||||
expect(obj1.__owl__.rev).toBe(1);
|
||||
expect(obj2.__owl__.rev).toBe(2);
|
||||
});
|
||||
|
||||
test("call callback when state is changed", async () => {
|
||||
const observer = new Observer();
|
||||
observer.notifyCB = jest.fn();
|
||||
const obj: any = { a: 1, b: { c: 2 }, d: [{ e: 3 }], f: 4 };
|
||||
|
||||
observer.observe(obj);
|
||||
expect(observer.notifyCB).toBeCalledTimes(0);
|
||||
|
||||
obj.a = 2;
|
||||
await nextMicroTick();
|
||||
expect(observer.notifyCB).toBeCalledTimes(1);
|
||||
|
||||
obj.b.c = 3;
|
||||
await nextMicroTick();
|
||||
expect(observer.notifyCB).toBeCalledTimes(2);
|
||||
|
||||
obj.d[0].e = 5;
|
||||
await nextMicroTick();
|
||||
expect(observer.notifyCB).toBeCalledTimes(3);
|
||||
|
||||
obj.a = 111;
|
||||
obj.f = 222;
|
||||
await nextMicroTick();
|
||||
expect(observer.notifyCB).toBeCalledTimes(4);
|
||||
});
|
||||
|
||||
test("throw error when state is mutated in object if allowMutation=false", async () => {
|
||||
const observer = new Observer();
|
||||
observer.allowMutations = false;
|
||||
const obj: any = { a: 1 };
|
||||
observer.observe(obj);
|
||||
|
||||
expect(() => {
|
||||
obj.a = 2;
|
||||
}).toThrow('Observed state cannot be changed here! (key: "a", val: "2")');
|
||||
});
|
||||
|
||||
test("throw error when state is mutated in array if allowMutation=false", async () => {
|
||||
const observer = new Observer();
|
||||
observer.allowMutations = false;
|
||||
const obj: any = { a: [1] };
|
||||
observer.observe(obj);
|
||||
|
||||
expect(() => {
|
||||
obj.a.push(2);
|
||||
}).toThrow("Array cannot be changed here");
|
||||
});
|
||||
});
|
||||
+731
-300
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
import { QWeb } from "../src/qweb_core";
|
||||
import { normalize, renderToDOM, renderToString, trim } from "./helpers";
|
||||
import { QWeb } from "../../src/qweb/index";
|
||||
import { normalize, renderToDOM, renderToString, trim, nextTick } from "../helpers";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Setup and helpers
|
||||
@@ -11,6 +11,7 @@ import { normalize, renderToDOM, renderToString, trim } from "./helpers";
|
||||
let qweb: QWeb;
|
||||
|
||||
beforeEach(() => {
|
||||
QWeb.TEMPLATES = {};
|
||||
qweb = new QWeb();
|
||||
});
|
||||
|
||||
@@ -24,6 +25,16 @@ describe("static templates", () => {
|
||||
expect(renderToString(qweb, "test")).toBe("hello vdom");
|
||||
});
|
||||
|
||||
test("simple dynamic value", () => {
|
||||
qweb.addTemplate("test", '<t><t t-esc="text"/></t>');
|
||||
expect(renderToString(qweb, "test", { text: "hello vdom" })).toBe("hello vdom");
|
||||
});
|
||||
|
||||
test("simple string, with some dynamic value", () => {
|
||||
qweb.addTemplate("test", '<t>hello <t t-esc="text"/></t>');
|
||||
expect(renderToString(qweb, "test", { text: "vdom" })).toBe("hello vdom");
|
||||
});
|
||||
|
||||
test("empty div", () => {
|
||||
qweb.addTemplate("test", "<div></div>");
|
||||
expect(renderToString(qweb, "test")).toBe("<div></div>");
|
||||
@@ -38,13 +49,16 @@ describe("static templates", () => {
|
||||
qweb.addTemplate("test", "<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", () => {
|
||||
test("invalid xml", () => {
|
||||
expect(() => qweb.addTemplate("test", "<div>")).toThrow(
|
||||
"Invalid XML in template"
|
||||
);
|
||||
expect(() => qweb.addTemplate("test", "<div>")).toThrow("Invalid XML in template");
|
||||
});
|
||||
|
||||
test("template with text node and tag", () => {
|
||||
@@ -61,6 +75,7 @@ describe("error handling", () => {
|
||||
|
||||
test("cannot add twice the same template", () => {
|
||||
qweb.addTemplate("test", `<t></t>`);
|
||||
expect(() => qweb.addTemplate("test", "<div/>", true)).not.toThrow("already defined");
|
||||
expect(() => qweb.addTemplate("test", "<div/>")).toThrow("already defined");
|
||||
});
|
||||
|
||||
@@ -79,29 +94,14 @@ describe("error handling", () => {
|
||||
|
||||
test("nice error when t-on is evaluated with a missing event", () => {
|
||||
qweb.addTemplate("templatename", `<div t-on="somemethod"></div>`);
|
||||
expect(() =>
|
||||
qweb.render("templatename", { someMethod() {} }, { handlers: [] })
|
||||
).toThrow("Missing event name with t-on directive");
|
||||
});
|
||||
|
||||
test("error when compiled code is invalid", () => {
|
||||
qweb.addTemplate(
|
||||
"templatename",
|
||||
`<div t-att-hey="}/^function invalid{{>'"></div>`
|
||||
);
|
||||
expect(() => qweb.render("templatename")).toThrow(
|
||||
"Invalid generated code while compiling template 'templatename': Unexpected token }"
|
||||
expect(() => qweb.render("templatename", { someMethod() {} }, { handlers: [] })).toThrow(
|
||||
"Missing event name with t-on directive"
|
||||
);
|
||||
});
|
||||
|
||||
test("error when unknown directive", () => {
|
||||
qweb.addTemplate(
|
||||
"templatename",
|
||||
`<div t-best-beer="rochefort 10">test</div>`
|
||||
);
|
||||
expect(() => qweb.render("templatename")).toThrow(
|
||||
"Unknown QWeb directive: 't-best-beer'"
|
||||
);
|
||||
qweb.addTemplate("templatename", `<div t-best-beer="rochefort 10">test</div>`);
|
||||
expect(() => qweb.render("templatename")).toThrow("Unknown QWeb directive: 't-best-beer'");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -118,9 +118,7 @@ describe("t-esc", () => {
|
||||
|
||||
test.skip("escaping", () => {
|
||||
qweb.addTemplate("test", `<span><t t-esc="var"/></span>`);
|
||||
expect(renderToString(qweb, "test", { var: "<ok>" })).toBe(
|
||||
"<span><ok></span>"
|
||||
);
|
||||
expect(renderToString(qweb, "test", { var: "<ok>" })).toBe("<span><ok></span>");
|
||||
});
|
||||
|
||||
test("escaping on a node", () => {
|
||||
@@ -152,9 +150,7 @@ describe("t-raw", () => {
|
||||
|
||||
test("not escaping", () => {
|
||||
qweb.addTemplate("test", `<div><t t-raw="var"/></div>`);
|
||||
expect(renderToString(qweb, "test", { var: "<ok></ok>" })).toBe(
|
||||
"<div><ok></ok></div>"
|
||||
);
|
||||
expect(renderToString(qweb, "test", { var: "<ok></ok>" })).toBe("<div><ok></ok></div>");
|
||||
});
|
||||
|
||||
test("t-raw and another sibling node", () => {
|
||||
@@ -167,39 +163,28 @@ describe("t-raw", () => {
|
||||
|
||||
describe("t-set", () => {
|
||||
test("set from attribute literal", () => {
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div><t t-set="value" t-value="'ok'"/><t t-esc="value"/></div>`
|
||||
);
|
||||
qweb.addTemplate("test", `<div><t t-set="value" t-value="'ok'"/><t t-esc="value"/></div>`);
|
||||
expect(renderToString(qweb, "test")).toBe("<div>ok</div>");
|
||||
});
|
||||
|
||||
test("t-set and t-if", () => {
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div >
|
||||
`<div>
|
||||
<t t-set="v" t-value="value"/>
|
||||
<t t-if="v === 'ok'">grimbergen</t>
|
||||
</div>`
|
||||
);
|
||||
expect(renderToString(qweb, "test", { value: "ok" })).toBe(
|
||||
"<div>grimbergen</div>"
|
||||
);
|
||||
expect(renderToString(qweb, "test", { value: "ok" })).toBe("<div>grimbergen</div>");
|
||||
});
|
||||
|
||||
test("set from body literal", () => {
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<t><t t-set="value">ok</t><t t-esc="value"/></t>`
|
||||
);
|
||||
qweb.addTemplate("test", `<t><t t-set="value">ok</t><t t-esc="value"/></t>`);
|
||||
expect(renderToString(qweb, "test")).toBe("ok");
|
||||
});
|
||||
|
||||
test("set from attribute lookup", () => {
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div><t t-set="stuff" t-value="value"/><t t-esc="stuff"/></div>`
|
||||
);
|
||||
qweb.addTemplate("test", `<div><t t-set="stuff" t-value="value"/><t t-esc="stuff"/></div>`);
|
||||
expect(renderToString(qweb, "test", { value: "ok" })).toBe("<div>ok</div>");
|
||||
});
|
||||
|
||||
@@ -231,18 +216,12 @@ describe("t-set", () => {
|
||||
});
|
||||
|
||||
test("value priority", () => {
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div><t t-set="value" t-value="1">2</t><t t-esc="value"/></div>`
|
||||
);
|
||||
qweb.addTemplate("test", `<div><t t-set="value" t-value="1">2</t><t t-esc="value"/></div>`);
|
||||
expect(renderToString(qweb, "test")).toBe("<div>1</div>");
|
||||
});
|
||||
|
||||
test("evaluate value expression", () => {
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div><t t-set="value" t-value="1 + 2"/><t t-esc="value"/></div>`
|
||||
);
|
||||
qweb.addTemplate("test", `<div><t t-set="value" t-value="1 + 2"/><t t-esc="value"/></div>`);
|
||||
expect(renderToString(qweb, "test")).toBe("<div>3</div>");
|
||||
});
|
||||
|
||||
@@ -267,25 +246,19 @@ describe("t-set", () => {
|
||||
"test",
|
||||
`<div><t t-set="value" t-value="somevariable + 2"/><t t-esc="value"/></div>`
|
||||
);
|
||||
expect(renderToString(qweb, "test", { somevariable: 43 })).toBe(
|
||||
"<div>45</div>"
|
||||
);
|
||||
expect(renderToString(qweb, "test", { somevariable: 43 })).toBe("<div>45</div>");
|
||||
});
|
||||
});
|
||||
|
||||
describe("t-if", () => {
|
||||
test("boolean value true condition", () => {
|
||||
qweb.addTemplate("test", `<div><t t-if="condition">ok</t></div>`);
|
||||
expect(renderToString(qweb, "test", { condition: true })).toBe(
|
||||
"<div>ok</div>"
|
||||
);
|
||||
expect(renderToString(qweb, "test", { condition: true })).toBe("<div>ok</div>");
|
||||
});
|
||||
|
||||
test("boolean value false condition", () => {
|
||||
qweb.addTemplate("test", `<div><t t-if="condition">ok</t></div>`);
|
||||
expect(renderToString(qweb, "test", { condition: false })).toBe(
|
||||
"<div></div>"
|
||||
);
|
||||
expect(renderToString(qweb, "test", { condition: false })).toBe("<div></div>");
|
||||
});
|
||||
|
||||
test("boolean value condition missing", () => {
|
||||
@@ -302,9 +275,7 @@ describe("t-if", () => {
|
||||
<t t-else="">beer</t></div>
|
||||
`
|
||||
);
|
||||
expect(renderToString(qweb, "test", { color: "red" })).toBe(
|
||||
"<div>red is dead</div>"
|
||||
);
|
||||
expect(renderToString(qweb, "test", { color: "red" })).toBe("<div>red is dead</div>");
|
||||
});
|
||||
|
||||
test("boolean value condition else", () => {
|
||||
@@ -330,9 +301,7 @@ describe("t-if", () => {
|
||||
`
|
||||
);
|
||||
const result = trim(renderToString(qweb, "test", { condition: false }));
|
||||
expect(result).toBe(
|
||||
"<div><span>begin</span>fail-else<span>end</span></div>"
|
||||
);
|
||||
expect(result).toBe("<div><span>begin</span>fail-else<span>end</span></div>");
|
||||
});
|
||||
|
||||
test("can use some boolean operators in expressions", () => {
|
||||
@@ -357,9 +326,7 @@ describe("t-if", () => {
|
||||
m: 5,
|
||||
n: 2
|
||||
};
|
||||
expect(normalize(renderToString(qweb, "test", context))).toBe(
|
||||
"<div>andormgtnlt</div>"
|
||||
);
|
||||
expect(normalize(renderToString(qweb, "test", context))).toBe("<div>andormgtnlt</div>");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -478,16 +445,13 @@ describe("attributes", () => {
|
||||
});
|
||||
|
||||
test("format expression, other format", () => {
|
||||
qweb.addTemplate("test", `<div t-attf-foo="#{value + 37}"/>`);
|
||||
qweb.addTemplate("test", `<div t-attf-foo="{{value + 37}}"/>`);
|
||||
const result = renderToString(qweb, "test", { value: 5 });
|
||||
expect(result).toBe(`<div foo="42"></div>`);
|
||||
});
|
||||
|
||||
test("format multiple", () => {
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div t-attf-foo="a {{value1}} is {{value2}} of {{value3}} ]"/>`
|
||||
);
|
||||
qweb.addTemplate("test", `<div t-attf-foo="a {{value1}} is {{value2}} of {{value3}} ]"/>`);
|
||||
const result = renderToString(qweb, "test", {
|
||||
value1: 0,
|
||||
value2: 1,
|
||||
@@ -522,11 +486,14 @@ describe("attributes", () => {
|
||||
expect(result).toBe(`<div class="hello world"></div>`);
|
||||
});
|
||||
|
||||
test("class and t-attf-class with ternary operation", () => {
|
||||
qweb.addTemplate("test", `<div class="hello" t-attf-class="{{value ? 'world' : ''}}"/>`);
|
||||
const result = renderToString(qweb, "test", { value: true });
|
||||
expect(result).toBe(`<div class="hello world"></div>`);
|
||||
});
|
||||
|
||||
test("t-att-class with object", () => {
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div class="static" t-att-class="{a: b, c: d, e: f}"/>`
|
||||
);
|
||||
qweb.addTemplate("test", `<div class="static" t-att-class="{a: b, c: d, e: f}"/>`);
|
||||
const result = renderToString(qweb, "test", { b: true, d: false, f: true });
|
||||
expect(result).toBe(`<div class="static a e"></div>`);
|
||||
});
|
||||
@@ -540,6 +507,13 @@ describe("t-call (template calling", () => {
|
||||
expect(renderToString(qweb, "caller")).toBe(expected);
|
||||
});
|
||||
|
||||
test("t-call with t-if", () => {
|
||||
qweb.addTemplate("sub", "<span>ok</span>");
|
||||
qweb.addTemplate("caller", '<div><t t-if="flag" t-call="sub"/></div>');
|
||||
const expected = "<div><span>ok</span></div>";
|
||||
expect(renderToString(qweb, "caller", { flag: true })).toBe(expected);
|
||||
});
|
||||
|
||||
test("t-call not allowed on a non t node", () => {
|
||||
qweb.addTemplate("_basic-callee", "<t>ok</t>");
|
||||
qweb.addTemplate("caller", '<div t-call="_basic-callee"/>');
|
||||
@@ -555,10 +529,7 @@ describe("t-call (template calling", () => {
|
||||
|
||||
test("with unused setbody", () => {
|
||||
qweb.addTemplate("_basic-callee", "<div>ok</div>");
|
||||
qweb.addTemplate(
|
||||
"caller",
|
||||
'<t t-call="_basic-callee"><t t-set="qux" t-value="3"/></t>'
|
||||
);
|
||||
qweb.addTemplate("caller", '<t t-call="_basic-callee"><t t-set="qux" t-value="3"/></t>');
|
||||
const expected = "<div>ok</div>";
|
||||
expect(renderToString(qweb, "caller")).toBe(expected);
|
||||
});
|
||||
@@ -608,6 +579,103 @@ describe("t-call (template calling", () => {
|
||||
const expected = "<div>ok</div>";
|
||||
expect(trim(renderToString(qweb, "caller"))).toBe(expected);
|
||||
});
|
||||
|
||||
test("call with several sub nodes on same line", () => {
|
||||
qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="SubTemplate">
|
||||
<t t-raw="0"/>
|
||||
</div>
|
||||
|
||||
<div t-name="main">
|
||||
<t t-call="SubTemplate">
|
||||
<span>hey</span> <span>yay</span>
|
||||
</t>
|
||||
</div>
|
||||
</templates>
|
||||
`);
|
||||
const expected = "<div><div><span>hey</span> <span>yay</span></div></div>";
|
||||
expect(renderToString(qweb, "main")).toBe(expected);
|
||||
});
|
||||
|
||||
test("recursive template, part 1", () => {
|
||||
qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="recursive">
|
||||
<span>hey</span>
|
||||
<t t-if="false">
|
||||
<t t-call="recursive"/>
|
||||
</t>
|
||||
</div>
|
||||
</templates>
|
||||
`);
|
||||
const expected = "<div><span>hey</span></div>";
|
||||
expect(renderToString(qweb, "recursive")).toBe(expected);
|
||||
const recursiveFn = Object.values(qweb.recursiveFns)[0];
|
||||
expect(recursiveFn.toString()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("recursive template, part 2", () => {
|
||||
qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="Parent">
|
||||
<t t-call="nodeTemplate">
|
||||
<t t-set="node" t-value="root"/>
|
||||
</t>
|
||||
</div>
|
||||
<div t-name="nodeTemplate">
|
||||
<p><t t-esc="node.val"/></p>
|
||||
<t t-foreach="node.children or []" t-as="subtree">
|
||||
<t t-call="nodeTemplate">
|
||||
<t t-set="node" t-value="subtree"/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
`);
|
||||
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>";
|
||||
expect(renderToString(qweb, "Parent", { root }, { fiber: { vars: {}, scope: {} } })).toBe(
|
||||
expected
|
||||
);
|
||||
const recursiveFn = Object.values(qweb.recursiveFns)[0];
|
||||
expect(recursiveFn.toString()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("recursive template, part 3", () => {
|
||||
qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="Parent">
|
||||
<t t-call="nodeTemplate">
|
||||
<t t-set="node" t-value="root"/>
|
||||
</t>
|
||||
</div>
|
||||
<div t-name="nodeTemplate">
|
||||
<p><t t-esc="node.val"/></p>
|
||||
<t t-foreach="node.children or []" t-as="subtree">
|
||||
<t t-call="nodeTemplate">
|
||||
<t t-set="node" t-value="subtree"/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
`);
|
||||
const root = { val: "a", children: [{ val: "b", children: [{ val: "d" }] }, { val: "c" }] };
|
||||
const expected =
|
||||
"<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 }, { fiber: {} })).toBe(expected);
|
||||
const recursiveFn = Object.values(qweb.recursiveFns)[0];
|
||||
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", () => {
|
||||
@@ -644,25 +712,13 @@ describe("foreach", () => {
|
||||
"test",
|
||||
`
|
||||
<div>
|
||||
<t t-foreach="5" t-as="elem">
|
||||
-<t t-if="elem_first"> first</t><t t-if="elem_last"> last</t> (<t t-esc="elem_parity"/>)
|
||||
<t t-foreach="Array(5)" t-as="elem">
|
||||
-<t t-if="elem_first"> first</t><t t-if="elem_last"> last</t> (<t t-esc="elem_index"/>)
|
||||
</t>
|
||||
</div>`
|
||||
);
|
||||
const result = trim(renderToString(qweb, "test"));
|
||||
const expected = `<div>-first(even)-(odd)-(even)-(odd)-last(even)</div>`;
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
test("iterate, integer param", () => {
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div><t t-foreach="3" t-as="item">
|
||||
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/>]
|
||||
</t></div>`
|
||||
);
|
||||
const result = trim(renderToString(qweb, "test"));
|
||||
const expected = `<div>[0:00][1:11][2:22]</div>`;
|
||||
const expected = `<div>-first(0)-(1)-(2)-(3)-last(4)</div>`;
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
@@ -672,14 +728,12 @@ describe("foreach", () => {
|
||||
`
|
||||
<div>
|
||||
<t t-foreach="value" t-as="item">
|
||||
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/> - <t t-esc="item_parity"/>]
|
||||
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/>]
|
||||
</t>
|
||||
</div>`
|
||||
);
|
||||
const result = trim(
|
||||
renderToString(qweb, "test", { value: { a: 1, b: 2, c: 3 } })
|
||||
);
|
||||
const expected = `<div>[0:a1-even][1:b2-odd][2:c3-even]</div>`;
|
||||
const result = trim(renderToString(qweb, "test", { value: { a: 1, b: 2, c: 3 } }));
|
||||
const expected = `<div>[0:a1][1:b2][2:c3]</div>`;
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
@@ -724,15 +778,9 @@ describe("foreach", () => {
|
||||
|
||||
describe("misc", () => {
|
||||
test("global", () => {
|
||||
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-asc-toto",
|
||||
`<div t-raw="toto">toto default</div>`
|
||||
);
|
||||
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-asc-toto", `<div t-raw="toto">toto default</div>`);
|
||||
qweb.addTemplate(
|
||||
"caller",
|
||||
`
|
||||
@@ -845,10 +893,7 @@ describe("t-on", () => {
|
||||
});
|
||||
|
||||
test("can bind handlers with object arguments", () => {
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<button t-on-click="add({val: 5})">Click</button>`
|
||||
);
|
||||
qweb.addTemplate("test", `<button t-on-click="add({val: 5})">Click</button>`);
|
||||
let a = 1;
|
||||
const node = renderToDOM(
|
||||
qweb,
|
||||
@@ -866,10 +911,7 @@ describe("t-on", () => {
|
||||
|
||||
test("can bind handlers with empty object", () => {
|
||||
expect.assertions(2);
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<button t-on-click="doSomething({})">Click</button>`
|
||||
);
|
||||
qweb.addTemplate("test", `<button t-on-click="doSomething({})">Click</button>`);
|
||||
const node = renderToDOM(
|
||||
qweb,
|
||||
"test",
|
||||
@@ -883,12 +925,9 @@ describe("t-on", () => {
|
||||
(<HTMLElement>node).click();
|
||||
});
|
||||
|
||||
test("can bind handlers with empty object (with non empty inner string", () => {
|
||||
test("can bind handlers with empty object (with non empty inner string)", () => {
|
||||
expect.assertions(2);
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<button t-on-click="doSomething({ })">Click</button>`
|
||||
);
|
||||
qweb.addTemplate("test", `<button t-on-click="doSomething({ })">Click</button>`);
|
||||
const node = renderToDOM(
|
||||
qweb,
|
||||
"test",
|
||||
@@ -1047,20 +1086,92 @@ describe("t-on", () => {
|
||||
|
||||
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 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", () => {
|
||||
test("can get a ref on a node", () => {
|
||||
qweb.addTemplate("test", `<div><span t-ref="myspan"/></div>`);
|
||||
let refs: any = {};
|
||||
renderToDOM(qweb, "test", { refs });
|
||||
renderToDOM(qweb, "test", { __owl__: { refs } });
|
||||
expect(refs.myspan.tagName).toBe("SPAN");
|
||||
});
|
||||
|
||||
test("can get a dynamic ref on a node", () => {
|
||||
qweb.addTemplate("test", `<div><span t-ref="myspan{{id}}"/></div>`);
|
||||
let refs: any = {};
|
||||
renderToDOM(qweb, "test", { refs, id: 3 });
|
||||
renderToDOM(qweb, "test", { id: 3, __owl__: { refs } });
|
||||
expect(refs.myspan3.tagName).toBe("SPAN");
|
||||
});
|
||||
|
||||
@@ -1075,7 +1186,7 @@ describe("t-ref", () => {
|
||||
</div>`
|
||||
);
|
||||
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"]);
|
||||
});
|
||||
});
|
||||
@@ -1108,7 +1219,7 @@ describe("loading templates", () => {
|
||||
test("does not crash if string does not have templates", () => {
|
||||
const data = "";
|
||||
qweb.addTemplates(data);
|
||||
expect(Object.keys(qweb.templates)).toEqual(["default"]);
|
||||
expect(Object.keys(qweb.templates)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1188,13 +1299,10 @@ describe("whitespace handling", () => {
|
||||
|
||||
describe("t-key", () => {
|
||||
test("can use t-key directive on a node", () => {
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div t-key="beer.id"><t t-esc="beer.name"/></div>`
|
||||
qweb.addTemplate("test", `<div t-key="beer.id"><t t-esc="beer.name"/></div>`);
|
||||
expect(renderToString(qweb, "test", { beer: { id: 12, name: "Chimay Rouge" } })).toBe(
|
||||
"<div>Chimay Rouge</div>"
|
||||
);
|
||||
expect(
|
||||
renderToString(qweb, "test", { beer: { id: 12, name: "Chimay Rouge" } })
|
||||
).toBe("<div>Chimay Rouge</div>");
|
||||
});
|
||||
|
||||
test("t-key directive in a list", () => {
|
||||
@@ -1245,3 +1353,57 @@ describe("debugging", () => {
|
||||
console.log = consoleLog;
|
||||
});
|
||||
});
|
||||
|
||||
describe("update on event bus", () => {
|
||||
test("two consecutive forceUpdates only causes one listener update", async () => {
|
||||
const fn = jest.fn();
|
||||
qweb.on("update", null, fn);
|
||||
qweb.forceUpdate();
|
||||
qweb.forceUpdate();
|
||||
expect(fn).toBeCalledTimes(0);
|
||||
await nextTick();
|
||||
expect(fn).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("global template registration", () => {
|
||||
test("can register template globally", () => {
|
||||
expect.assertions(5);
|
||||
let qweb = new QWeb();
|
||||
try {
|
||||
qweb.render("mytemplate");
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch("Template mytemplate does not exist");
|
||||
}
|
||||
expect(qweb.templates.mytemplate).toBeUndefined();
|
||||
|
||||
QWeb.registerTemplate("mytemplate", "<div>global</div>");
|
||||
expect(qweb.templates.mytemplate).toBeDefined();
|
||||
const vnode = qweb.render("mytemplate");
|
||||
expect(vnode.sel).toBe("div");
|
||||
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>`
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,158 @@
|
||||
import { compileExpr, tokenize } from "../../src/qweb/expression_parser";
|
||||
|
||||
describe("tokenizer", () => {
|
||||
test("simple tokens", () => {
|
||||
expect(tokenize("1.3")).toEqual([{ type: "VALUE", value: "1.3" }]);
|
||||
|
||||
expect(tokenize("{}")).toEqual([
|
||||
{ type: "LEFT_BRACE", value: "{" },
|
||||
{ type: "RIGHT_BRACE", value: "}" }
|
||||
]);
|
||||
expect(tokenize("{ }}")).toEqual([
|
||||
{ type: "LEFT_BRACE", value: "{" },
|
||||
{ type: "RIGHT_BRACE", value: "}" },
|
||||
{ type: "RIGHT_BRACE", value: "}" }
|
||||
]);
|
||||
expect(tokenize("a")).toEqual([{ type: "SYMBOL", value: "a" }]);
|
||||
expect(tokenize("true")).toEqual([{ type: "SYMBOL", value: "true" }]);
|
||||
expect(tokenize("abcde")).toEqual([{ type: "SYMBOL", value: "abcde" }]);
|
||||
expect(tokenize("_ab2")).toEqual([{ type: "SYMBOL", value: "_ab2" }]);
|
||||
expect(tokenize("$ab2")).toEqual([{ type: "SYMBOL", value: "$ab2" }]);
|
||||
expect(tokenize("ABC")).toEqual([{ type: "SYMBOL", value: "ABC" }]);
|
||||
|
||||
expect(tokenize("{a: 2}")).toEqual([
|
||||
{ type: "LEFT_BRACE", value: "{" },
|
||||
{ type: "SYMBOL", value: "a" },
|
||||
{ type: "COLON", value: ":" },
|
||||
{ type: "VALUE", value: "2" },
|
||||
{ type: "RIGHT_BRACE", value: "}" }
|
||||
]);
|
||||
expect(tokenize("a,")).toEqual([{ type: "SYMBOL", value: "a" }, { type: "COMMA", value: "," }]);
|
||||
expect(tokenize("][")).toEqual([
|
||||
{ type: "RIGHT_BRACKET", value: "]" },
|
||||
{ type: "LEFT_BRACKET", value: "[" }
|
||||
]);
|
||||
});
|
||||
|
||||
test("various operators", () => {
|
||||
expect(tokenize(">= <= < > !== !=")).toEqual([
|
||||
{ type: "OPERATOR", value: ">=" },
|
||||
{ type: "OPERATOR", value: "<=" },
|
||||
{ type: "OPERATOR", value: "<" },
|
||||
{ type: "OPERATOR", value: ">" },
|
||||
{ type: "OPERATOR", value: "!==" },
|
||||
{ type: "OPERATOR", value: "!=" }
|
||||
]);
|
||||
});
|
||||
|
||||
test("strings", () => {
|
||||
expect(() => tokenize("'")).toThrow("Invalid expression");
|
||||
expect(() => tokenize("'\\")).toThrow("Invalid expression");
|
||||
expect(() => tokenize("'\\'")).toThrow("Invalid expression");
|
||||
expect(tokenize("'hello ged'")).toEqual([{ type: "VALUE", value: "'hello ged'" }]);
|
||||
expect(tokenize("'hello \\'ged\\''")).toEqual([{ type: "VALUE", value: "'hello \\'ged\\''" }]);
|
||||
|
||||
expect(() => tokenize('"')).toThrow("Invalid expression");
|
||||
expect(() => tokenize('"\\"')).toThrow("Invalid expression");
|
||||
expect(tokenize('"hello ged"')).toEqual([{ type: "VALUE", value: '"hello ged"' }]);
|
||||
expect(tokenize('"hello ged"}')).toEqual([
|
||||
{ type: "VALUE", value: '"hello ged"' },
|
||||
{ type: "RIGHT_BRACE", value: "}" }
|
||||
]);
|
||||
expect(tokenize('"hello \\"ged\\""')).toEqual([{ type: "VALUE", value: '"hello \\"ged\\""' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("expression evaluation", () => {
|
||||
test("simple static values", () => {
|
||||
expect(compileExpr("1", {})).toBe("1");
|
||||
expect(compileExpr("1 ", {})).toBe("1");
|
||||
expect(compileExpr("'some string#/, {' ", {})).toBe("'some string#/, {'");
|
||||
expect(compileExpr("{ } ", {})).toBe("{}");
|
||||
expect(compileExpr("{a: 1} ", {})).toBe("{a:1}");
|
||||
expect(compileExpr("{a: 1, b: 2 } ", {})).toBe("{a:1,b:2}");
|
||||
expect(compileExpr("[] ", {})).toBe("[]");
|
||||
expect(compileExpr("[1] ", {})).toBe("[1]");
|
||||
expect(compileExpr("['1', '2'] ", {})).toBe("['1','2']");
|
||||
expect(compileExpr("['1', \"2\"] ", {})).toBe("['1',\"2\"]");
|
||||
});
|
||||
|
||||
test("various types of 'words'", () => {
|
||||
expect(compileExpr("true", {})).toBe("true");
|
||||
expect(compileExpr("false", {})).toBe("false");
|
||||
expect(compileExpr("debugger", {})).toBe("debugger");
|
||||
});
|
||||
|
||||
test("parenthesis", () => {
|
||||
expect(compileExpr("(1)", {})).toBe("(1)");
|
||||
expect(compileExpr("a*(1 +3)", {})).toBe("context['a']*(1+3)");
|
||||
});
|
||||
|
||||
test("objects and sub objects", () => {
|
||||
expect(compileExpr("{a:{b:1}} ", {})).toBe("{a:{b:1}}");
|
||||
});
|
||||
|
||||
test("replacing variables", () => {
|
||||
expect(compileExpr("a", {})).toBe("context['a']");
|
||||
expect(compileExpr("a", { a: { id: "_3", expr: "" } })).toBe("_3");
|
||||
});
|
||||
|
||||
test("arrays and objects", () => {
|
||||
expect(compileExpr("[{b:1}] ", {})).toBe("[{b:1}]");
|
||||
expect(compileExpr("{a: []} ", {})).toBe("{a:[]}");
|
||||
expect(compileExpr("[{b:1, c: [1, {d: {e: 3}} ]}] ", {})).toBe("[{b:1,c:[1,{d:{e:3}}]}]");
|
||||
});
|
||||
|
||||
test("dot operator", () => {
|
||||
expect(compileExpr("a.b", {})).toBe("context['a'].b");
|
||||
expect(compileExpr("a.b.c", {})).toBe("context['a'].b.c");
|
||||
});
|
||||
|
||||
test("various unary operators", () => {
|
||||
expect(compileExpr("!flag", {})).toBe("!context['flag']");
|
||||
expect(compileExpr("-3", {})).toBe("-3");
|
||||
expect(compileExpr("-a", {})).toBe("-context['a']");
|
||||
});
|
||||
|
||||
test("various binary operators", () => {
|
||||
expect(compileExpr("color == 'black'", {})).toBe("context['color']=='black'");
|
||||
expect(compileExpr("a || b", {})).toBe("context['a']||context['b']");
|
||||
expect(compileExpr("color === 'black'", {})).toBe("context['color']==='black'");
|
||||
expect(compileExpr("'li_'+item", {})).toBe("'li_'+context['item']");
|
||||
expect(compileExpr("state.val > 1", {})).toBe("context['state'].val>1");
|
||||
});
|
||||
|
||||
test("boolean operations", () => {
|
||||
expect(compileExpr("a && b", {})).toBe("context['a']&&context['b']");
|
||||
});
|
||||
|
||||
test("ternary operators", () => {
|
||||
expect(compileExpr("a ? b: '2'", {})).toBe("context['a']?context['b']:'2'");
|
||||
expect(compileExpr("a ? b: (c or '2') ", {})).toBe(
|
||||
"context['a']?context['b']:(context['c']||'2')"
|
||||
);
|
||||
expect(compileExpr("a ? {test:c}: [1,u]", {})).toBe(
|
||||
"context['a']?{test:context['c']}:[1,context['u']]"
|
||||
);
|
||||
});
|
||||
|
||||
test("word replacement", () => {
|
||||
expect(compileExpr("a or b", {})).toBe("context['a']||context['b']");
|
||||
expect(compileExpr("a and b", {})).toBe("context['a']&&context['b']");
|
||||
});
|
||||
|
||||
test("function calls", () => {
|
||||
expect(compileExpr("a()", {})).toBe("context['a']()");
|
||||
expect(compileExpr("a(1)", {})).toBe("context['a'](1)");
|
||||
expect(compileExpr("a(1,2)", {})).toBe("context['a'](1,2)");
|
||||
expect(compileExpr("a(1,2,{a:[a]})", {})).toBe("context['a'](1,2,{a:[context['a']]})");
|
||||
expect(compileExpr("'x'.toUpperCase()", {})).toBe("'x'.toUpperCase()");
|
||||
expect(compileExpr("'x'.toUpperCase({a: 3})", {})).toBe("'x'.toUpperCase({a:3})");
|
||||
expect(compileExpr("'x'.toUpperCase(a)", { a: { id: "_v5", expr: "" } })).toBe(
|
||||
"'x'.toUpperCase(_v5)"
|
||||
);
|
||||
expect(compileExpr("'x'.toUpperCase({b: a})", { a: { id: "_v5", expr: "" } })).toBe(
|
||||
"'x'.toUpperCase({b:_v5})"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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);
|
||||
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;
|
||||
}"
|
||||
`;
|
||||
@@ -0,0 +1,45 @@
|
||||
// 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']) {
|
||||
//COMPONENT
|
||||
let key4 = 'key' + context['env'].router.currentRouteName;
|
||||
let def2;
|
||||
let templateId5 = key4;
|
||||
let w3 = templateId5 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId5]] : false;
|
||||
let vn6 = {};
|
||||
result = vn6;
|
||||
let props3 = Object.assign({}, context['env'].router.currentParams);
|
||||
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
|
||||
if (utils.shallowEqual(props3, w3.__owl__.currentFiber.props)) {
|
||||
def2 = w3.__owl__.currentFiber.promise;
|
||||
} else {
|
||||
w3.destroy();
|
||||
w3 = false;
|
||||
}
|
||||
}
|
||||
if (!w3) {
|
||||
let componentKey3 = \`routeComponent\`;
|
||||
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['routeComponent'];
|
||||
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
|
||||
w3 = new W3(parent, props3);
|
||||
parent.__owl__.cmap[templateId5] = w3.__owl__.id;
|
||||
def2 = w3.__prepare(extra.fiber, undefined, undefined);
|
||||
def2 = def2.then(vnode=>{if (w3.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: templateId5, hook: {insert(vn) {let nvn=w3.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}});utils.defineProxy(vn6, pvnode);w3.__owl__.pvnode = pvnode;});
|
||||
} else {
|
||||
def2 = def2 || w3.__updateProps(props3, extra.fiber, undefined, undefined);
|
||||
def2 = def2.then(()=>{if (w3.__owl__.isDestroyed) {return};let pvnode=w3.__owl__.pvnode;utils.defineProxy(vn6, pvnode);});
|
||||
}
|
||||
extra.promises.push(def2);
|
||||
}
|
||||
return result;
|
||||
}"
|
||||
`;
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Component } from "../../src/component/component";
|
||||
import { Link } from "../../src/router/link";
|
||||
import { RouterEnv } from "../../src/router/router";
|
||||
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
|
||||
import { TestRouter } from "./test_router";
|
||||
|
||||
describe("Link component", () => {
|
||||
let fixture: HTMLElement;
|
||||
let env: RouterEnv;
|
||||
let router: TestRouter | null = null;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = makeTestFixture();
|
||||
env = <RouterEnv>makeTestEnv();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.remove();
|
||||
if (router) {
|
||||
router.destroy();
|
||||
}
|
||||
router = null;
|
||||
});
|
||||
|
||||
test("can render simple cases", async () => {
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="App">
|
||||
<Link to="'about'">About</Link>
|
||||
</div>
|
||||
</templates>
|
||||
`);
|
||||
class App extends Component<any, any> {
|
||||
static components = { Link: Link };
|
||||
}
|
||||
|
||||
const routes = [{ name: "about", path: "/about" }, { name: "users", path: "/users" }];
|
||||
|
||||
router = new TestRouter(env, routes, { mode: "history" });
|
||||
router.navigate({ to: "users" });
|
||||
const app = new App(env);
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe('<div><a href="/about">About</a></div>');
|
||||
|
||||
expect(window.location.pathname).toBe("/users");
|
||||
fixture.querySelector("a")!.click();
|
||||
await nextTick();
|
||||
expect(window.location.pathname).toBe("/about");
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><a href="/about" class="router-link-active">About</a></div>'
|
||||
);
|
||||
|
||||
expect(env.qweb.templates[Link.template].fn.toString()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("do not redirect if right clicking", async () => {
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="App">
|
||||
<Link to="'about'">About</Link>
|
||||
</div>
|
||||
</templates>
|
||||
`);
|
||||
class App extends Component<any, any> {
|
||||
static components = { Link: Link };
|
||||
}
|
||||
|
||||
const routes = [{ name: "about", path: "/about" }, { name: "users", path: "/users" }];
|
||||
|
||||
router = new TestRouter(env, routes, { mode: "history" });
|
||||
router.navigate({ to: "users" });
|
||||
const app = new App(env);
|
||||
await app.mount(fixture);
|
||||
|
||||
expect(window.location.pathname).toBe("/users");
|
||||
var evt = new MouseEvent("click", {
|
||||
button: 1
|
||||
});
|
||||
|
||||
fixture.querySelector("a")!.dispatchEvent(evt);
|
||||
await nextTick();
|
||||
expect(window.location.pathname).toBe("/users");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
import { Component } from "../../src/component/component";
|
||||
import { RouterEnv } from "../../src/router/router";
|
||||
import { RouteComponent } from "../../src/router/route_component";
|
||||
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
|
||||
import { TestRouter } from "./test_router";
|
||||
|
||||
describe("RouteComponent", () => {
|
||||
let fixture: HTMLElement;
|
||||
let env: RouterEnv;
|
||||
let router: TestRouter | null = null;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = makeTestFixture();
|
||||
env = <RouterEnv>makeTestEnv();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.remove();
|
||||
if (router) {
|
||||
router.destroy();
|
||||
}
|
||||
router = null;
|
||||
});
|
||||
|
||||
test("can render simple cases", async () => {
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="App">
|
||||
<RouteComponent />
|
||||
</div>
|
||||
<span t-name="About">About</span>
|
||||
<span t-name="Users">Users</span>
|
||||
</templates>
|
||||
`);
|
||||
class About extends Component<any, any> {}
|
||||
class Users extends Component<any, any> {}
|
||||
class App extends Component<any, any> {
|
||||
static components = { RouteComponent };
|
||||
}
|
||||
|
||||
const routes = [
|
||||
{ name: "about", path: "/about", component: About },
|
||||
{ name: "users", path: "/users", component: Users }
|
||||
];
|
||||
|
||||
router = new TestRouter(env, routes, { mode: "history" });
|
||||
await router.navigate({ to: "about" });
|
||||
const app = new App(env);
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><span>About</span></div>");
|
||||
|
||||
await router.navigate({ to: "users" });
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><span>Users</span></div>");
|
||||
expect(env.qweb.templates[RouteComponent.template].fn.toString()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("can render parameterized route", async () => {
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="App">
|
||||
<RouteComponent />
|
||||
</div>
|
||||
<span t-name="Book">Book <t t-esc="props.title"/></span>
|
||||
</templates>
|
||||
`);
|
||||
class Book extends Component<any, any> {}
|
||||
class App extends Component<any, any> {
|
||||
static components = { RouteComponent };
|
||||
}
|
||||
|
||||
const routes = [{ name: "book", path: "/book/{{title}}", component: Book }];
|
||||
router = new TestRouter(env, routes, { mode: "history" });
|
||||
await router.navigate({ to: "book", params: { title: "1984" } });
|
||||
const app = new App(env);
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><span>Book 1984</span></div>");
|
||||
});
|
||||
|
||||
test("can render parameterized route with suffixes", async () => {
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="App">
|
||||
<RouteComponent />
|
||||
</div>
|
||||
<span t-name="Book">Book <t t-esc="props.title"/>|<t t-esc="incVal"/></span>
|
||||
</templates>
|
||||
`);
|
||||
class Book extends Component<any, any> {
|
||||
get incVal() {
|
||||
return this.props.val + 1;
|
||||
}
|
||||
}
|
||||
class App extends Component<any, any> {
|
||||
static components = { RouteComponent };
|
||||
}
|
||||
|
||||
const routes = [{ name: "book", path: "/book/{{title}}/{{val.number}}", component: Book }];
|
||||
router = new TestRouter(env, routes, { mode: "history" });
|
||||
await router.navigate({ to: "book", params: { title: "1984", val: "123" } });
|
||||
const app = new App(env);
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><span>Book 1984|124</span></div>");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,258 @@
|
||||
import { Destination, RouterEnv, Route } from "../../src/router/router";
|
||||
import { makeTestEnv, nextTick } from "../helpers";
|
||||
import { TestRouter } from "./test_router";
|
||||
|
||||
let env: RouterEnv;
|
||||
let router: TestRouter | null = null;
|
||||
|
||||
beforeEach(() => {
|
||||
env = <RouterEnv>makeTestEnv();
|
||||
window.history.pushState({}, "/", "/");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (router) {
|
||||
router.destroy();
|
||||
}
|
||||
router = null;
|
||||
});
|
||||
|
||||
describe("router miscellaneous", () => {
|
||||
test("validate routes shape", () => {
|
||||
expect(() => {
|
||||
router = new TestRouter(env, [
|
||||
{ name: "someroute", path: "/some/path", redirect: { abc: "hey" } as Destination }
|
||||
]);
|
||||
}).toThrow(`Invalid destination: {"abc":"hey"}`);
|
||||
});
|
||||
|
||||
test("navigate to same route but with different params should trigger update", async () => {
|
||||
router = new TestRouter(env, [{ name: "users", path: "/users/{{id}}" }]);
|
||||
env.qweb.forceUpdate = jest.fn();
|
||||
await router.navigate({ to: "users", params: { id: 3 } });
|
||||
expect(window.location.pathname).toBe("/users/3");
|
||||
expect(env.qweb.forceUpdate).toHaveBeenCalledTimes(1);
|
||||
|
||||
await router.navigate({ to: "users", params: { id: 5 } });
|
||||
expect(window.location.pathname).toBe("/users/5");
|
||||
expect(env.qweb.forceUpdate).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test("changing url to same route but with different params should trigger update (hash mode)", async () => {
|
||||
env.qweb.forceUpdate = jest.fn();
|
||||
router = new TestRouter(env, [{ name: "users", path: "/users/{{id}}" }], { mode: "hash" });
|
||||
await router.start();
|
||||
await router.navigate({ to: "users", params: { id: 3 } });
|
||||
expect(window.location.hash).toBe("#/users/3");
|
||||
expect(env.qweb.forceUpdate).toHaveBeenCalledTimes(1);
|
||||
|
||||
window.location.hash = "/users/5";
|
||||
window.dispatchEvent(new Event("hashchange"));
|
||||
await nextTick();
|
||||
expect(window.location.hash).toBe("#/users/5");
|
||||
expect(env.qweb.forceUpdate).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("routeToPath", () => {
|
||||
test("simple non parameterized path", () => {
|
||||
router = new TestRouter(env, []);
|
||||
expect(router["routeToPath"]({ path: "/abc" } as Route, {})).toBe("/abc");
|
||||
expect(router["routeToPath"]({ path: "/abc/def" } as Route, {})).toBe("/abc/def");
|
||||
expect(router["routeToPath"]({ path: "/abc" } as Route, { val: 12 })).toBe("/abc");
|
||||
});
|
||||
|
||||
test("simple parameterized path", () => {
|
||||
router = new TestRouter(env, []);
|
||||
expect(router["routeToPath"]({ path: "/abc/{{def}}" } as Route, { def: 34 })).toBe("/abc/34");
|
||||
});
|
||||
|
||||
test("simple non parameterized path, mode = hash", () => {
|
||||
router = new TestRouter(env, [], { mode: "hash" });
|
||||
expect(router["routeToPath"]({ path: "/abc" } as Route, {})).toBe("#/abc");
|
||||
expect(router["routeToPath"]({ path: "/abc/def" } as Route, {})).toBe("#/abc/def");
|
||||
expect(router["routeToPath"]({ path: "/abc" } as Route, { val: 12 })).toBe("#/abc");
|
||||
});
|
||||
|
||||
test("simple parameterized path, mode=hash", () => {
|
||||
router = new TestRouter(env, [], { mode: "hash" });
|
||||
expect(router["routeToPath"]({ path: "/abc/{{def}}" } as Route, { def: 34 })).toBe("#/abc/34");
|
||||
});
|
||||
});
|
||||
|
||||
describe("destToPath", () => {
|
||||
test("validate destination shape", () => {
|
||||
router = new TestRouter(env, [{ name: "someroute", path: "/some/path" }]);
|
||||
expect(() => {
|
||||
router!.destToPath({ abc: 123 } as Destination);
|
||||
}).toThrow('Invalid destination: {"abc":123}');
|
||||
|
||||
expect(() => {
|
||||
router!.destToPath({ to: "someroute" } as Destination);
|
||||
}).not.toThrow();
|
||||
|
||||
expect(() => {
|
||||
router!.destToPath({ path: "/someroute", to: "otherroute" } as Destination);
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getRouteParams", () => {
|
||||
test("properly match simple routes", () => {
|
||||
router = new TestRouter(env, []);
|
||||
// simple route
|
||||
expect(router["getRouteParams"]({ path: "/home" } as Route, "/home")).toEqual({});
|
||||
|
||||
// no match
|
||||
expect(router["getRouteParams"]({ path: "/home" } as Route, "/otherpath")).toEqual(false);
|
||||
|
||||
// fallback route
|
||||
expect(router["getRouteParams"]({ path: "*" } as Route, "somepath")).toEqual({});
|
||||
});
|
||||
|
||||
test("properly match simple routes, mode hash", () => {
|
||||
router = new TestRouter(env, [], { mode: "hash" });
|
||||
// simple route
|
||||
expect(router["getRouteParams"]({ path: "/home" } as Route, "#/home")).toEqual({});
|
||||
|
||||
// no match
|
||||
expect(router["getRouteParams"]({ path: "/home" } as Route, "#/otherpath")).toEqual(false);
|
||||
|
||||
// fallback route
|
||||
expect(router["getRouteParams"]({ path: "*" } as Route, "#/somepath")).toEqual({});
|
||||
});
|
||||
|
||||
test("match some parameterized routes", () => {
|
||||
router = new TestRouter(env, []);
|
||||
expect(router["getRouteParams"]({ path: "/invoices/{{id}}" } as Route, "/invoices/3")).toEqual({
|
||||
id: "3"
|
||||
});
|
||||
});
|
||||
|
||||
test("match some parameterized routes, mode hash", () => {
|
||||
router = new TestRouter(env, [], { mode: "hash" });
|
||||
expect(router["getRouteParams"]({ path: "/invoices/{{id}}" } as Route, "#/invoices/3")).toEqual(
|
||||
{
|
||||
id: "3"
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("can convert to number if needed", () => {
|
||||
router = new TestRouter(env, []);
|
||||
expect(
|
||||
router["getRouteParams"]({ path: "/invoices/{{id.number}}" } as Route, "/invoices/3")
|
||||
).toEqual({
|
||||
id: 3
|
||||
});
|
||||
});
|
||||
|
||||
test("can convert to number if needed, mode: hash", () => {
|
||||
router = new TestRouter(env, [], { mode: "hash" });
|
||||
expect(
|
||||
router["getRouteParams"]({ path: "/invoices/{{id.number}}" } as Route, "#/invoices/3")
|
||||
).toEqual({
|
||||
id: 3
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("redirect", () => {
|
||||
test("can redirect to other route", async () => {
|
||||
router = new TestRouter(env, [
|
||||
{ name: "routea", path: "/some/path" },
|
||||
{ name: "routeb", path: "/some/other/path", redirect: { to: "routea" } }
|
||||
]);
|
||||
|
||||
await router.start();
|
||||
expect(window.location.pathname).toBe("/");
|
||||
await router.navigate({ to: "routeb" });
|
||||
expect(window.location.pathname).toBe("/some/path");
|
||||
expect(router.currentRouteName).toBe("routea");
|
||||
});
|
||||
|
||||
test("can redirect to other path", async () => {
|
||||
router = new TestRouter(env, [
|
||||
{ name: "routea", path: "/some/path" },
|
||||
{ name: "routeb", path: "/some/other/path", redirect: { path: "/some/path" } }
|
||||
]);
|
||||
router.navigate = jest.fn(router.navigate);
|
||||
|
||||
await router.start();
|
||||
await router.navigate({ to: "routeb" });
|
||||
|
||||
// once because we ask for it, once because it is redirected
|
||||
// expect(router.navigate).toBeCalledTimes(2);
|
||||
expect(window.location.pathname).toBe("/some/path");
|
||||
expect(router.currentRouteName).toBe("routea");
|
||||
});
|
||||
});
|
||||
|
||||
describe("beforeRouteEnter", () => {
|
||||
test("navigation guard is called and properly handle return true", async () => {
|
||||
expect(window.location.pathname).not.toBe("/some/path");
|
||||
const guard = jest.fn(() => true);
|
||||
router = new TestRouter(env, [{ name: "route", path: "/some/path", beforeRouteEnter: guard }]);
|
||||
|
||||
await router.start();
|
||||
await router.navigate({ to: "route" });
|
||||
expect(guard).toBeCalledTimes(1);
|
||||
expect(window.location.pathname).toBe("/some/path");
|
||||
expect(router.currentRouteName).toBe("route");
|
||||
});
|
||||
|
||||
test("navigation is cancelled if guard return false", async () => {
|
||||
expect(window.location.pathname).toBe("/");
|
||||
const guard = jest.fn(() => false);
|
||||
router = new TestRouter(env, [
|
||||
{ name: "routea", path: "/some/patha" },
|
||||
{ name: "routeb", path: "/some/pathb", beforeRouteEnter: guard }
|
||||
]);
|
||||
|
||||
await router.start();
|
||||
await router.navigate({ to: "routea" });
|
||||
expect(window.location.pathname).toBe("/some/patha");
|
||||
const result = await router.navigate({ to: "routeb" });
|
||||
expect(result).toBe(false);
|
||||
expect(guard).toBeCalledTimes(1);
|
||||
expect(window.location.pathname).toBe("/some/patha");
|
||||
expect(router.currentRouteName).toBe("routea");
|
||||
});
|
||||
|
||||
test("navigation is redirected if guard decides so", async () => {
|
||||
expect(window.location.pathname).toBe("/");
|
||||
const guard = jest.fn(() => {
|
||||
return { to: "routec" };
|
||||
});
|
||||
router = new TestRouter(env, [
|
||||
{ name: "routea", path: "/some/patha" },
|
||||
{ name: "routeb", path: "/some/pathb", beforeRouteEnter: guard },
|
||||
{ name: "routec", path: "/some/pathc" }
|
||||
]);
|
||||
|
||||
await router.start();
|
||||
const result = await router.navigate({ to: "routea" });
|
||||
expect(result).toBe(true);
|
||||
expect(window.location.pathname).toBe("/some/patha");
|
||||
await router.navigate({ to: "routeb" });
|
||||
expect(guard).toBeCalledTimes(1);
|
||||
expect(window.location.pathname).toBe("/some/pathc");
|
||||
expect(router.currentRouteName).toBe("routec");
|
||||
});
|
||||
|
||||
test("navigation is initially redirected if guard decides so", async () => {
|
||||
expect(window.location.pathname).toBe("/");
|
||||
const guard = jest.fn(() => {
|
||||
return { to: "otherroute" };
|
||||
});
|
||||
router = new TestRouter(env, [
|
||||
{ name: "landing", path: "/", beforeRouteEnter: guard },
|
||||
{ name: "otherroute", path: "/some/other/route" }
|
||||
]);
|
||||
|
||||
expect(window.location.pathname).toBe("/");
|
||||
|
||||
await router.start();
|
||||
expect(window.location.pathname).toBe("/some/other/route");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Router } from "../../src/router/router";
|
||||
import { QWeb } from "../../src/qweb/index";
|
||||
|
||||
export class TestRouter extends Router {
|
||||
destroy() {
|
||||
window.removeEventListener("popstate", (this as any)._listener);
|
||||
|
||||
// remove component defined inroutes
|
||||
for (let key in QWeb.components) {
|
||||
if (key.startsWith("__component__")) {
|
||||
delete QWeb.components[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+89
-714
@@ -1,136 +1,95 @@
|
||||
import { Component, Env } from "../src/component";
|
||||
import { connect, Store } from "../src/store";
|
||||
import {
|
||||
makeTestFixture,
|
||||
makeTestEnv,
|
||||
nextMicroTick,
|
||||
nextTick
|
||||
} from "./helpers";
|
||||
import { Env } from "../src/component/component";
|
||||
import { Store, Getter } from "../src/store";
|
||||
import { nextTick, nextMicroTick } from "./helpers";
|
||||
|
||||
describe("basic use", () => {
|
||||
test("commit a mutation", () => {
|
||||
const state = { n: 1 };
|
||||
const mutations = {
|
||||
inc({ state }, delta) {
|
||||
state.n += delta;
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, mutations });
|
||||
|
||||
expect(store.state.n).toBe(1);
|
||||
store.commit("inc", 14);
|
||||
expect(store.state.n).toBe(15);
|
||||
});
|
||||
|
||||
test("dispatch an action", () => {
|
||||
const state = { n: 1 };
|
||||
const mutations = {
|
||||
const actions = {
|
||||
inc({ state }, delta) {
|
||||
state.n += delta;
|
||||
}
|
||||
};
|
||||
const actions = {
|
||||
inc({ commit }, delta) {
|
||||
commit("inc", delta);
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, mutations, actions });
|
||||
const store = new Store({ state, actions });
|
||||
|
||||
expect(store.state.n).toBe(1);
|
||||
store.dispatch("inc", 14);
|
||||
expect(store.state.n).toBe(15);
|
||||
});
|
||||
|
||||
test("modifying state outside of mutations trigger error", () => {
|
||||
const state = { n: 1 };
|
||||
test("dispatch an action with positional arguments", () => {
|
||||
const state = { n1: 1, n2: 1, n3: 1 };
|
||||
|
||||
const actions = {
|
||||
inc({ state }) {
|
||||
state.n++;
|
||||
batchInc({ state }, delta1, delta2, delta3) {
|
||||
state.n1 += delta1;
|
||||
state.n2 += delta2;
|
||||
state.n3 += delta3;
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, mutations: {}, actions });
|
||||
const store = new Store({ state, actions });
|
||||
|
||||
expect(() => store.dispatch("inc")).toThrow();
|
||||
expect(() => (store.state.n = 15)).toThrow();
|
||||
expect(store.state.n1).toBe(1);
|
||||
expect(store.state.n2).toBe(1);
|
||||
expect(store.state.n3).toBe(1);
|
||||
store.dispatch("batchInc", 14, 30, 88);
|
||||
expect(store.state.n1).toBe(15);
|
||||
expect(store.state.n2).toBe(31);
|
||||
expect(store.state.n3).toBe(89);
|
||||
});
|
||||
|
||||
test("can dispatch an action in an action", () => {
|
||||
const state = { n: 1 };
|
||||
const mutations = {
|
||||
|
||||
const actions = {
|
||||
inc({ state }, delta) {
|
||||
state.n += delta;
|
||||
}
|
||||
};
|
||||
const actions = {
|
||||
inc({ commit }, delta) {
|
||||
commit("inc", delta);
|
||||
},
|
||||
inc100({ dispatch }) {
|
||||
dispatch("inc", 100);
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, mutations, actions });
|
||||
const store = new Store({ state, actions });
|
||||
|
||||
expect(store.state.n).toBe(1);
|
||||
store.dispatch("inc100");
|
||||
expect(store.state.n).toBe(101);
|
||||
});
|
||||
|
||||
test("can commit a mutation in a mutation", () => {
|
||||
test("return data from dispatching an action", () => {
|
||||
const state = { n: 1 };
|
||||
const mutations = {
|
||||
inc({ state }) {
|
||||
state.n++;
|
||||
},
|
||||
inc10({ commit }) {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
commit("inc");
|
||||
}
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, mutations });
|
||||
|
||||
expect(store.state.n).toBe(1);
|
||||
store.commit("inc10");
|
||||
expect(store.state.n).toBe(11);
|
||||
});
|
||||
|
||||
test("return data from committing a mutation", () => {
|
||||
const state = { n: 1 };
|
||||
const mutations = {
|
||||
const actions = {
|
||||
inc({ state }) {
|
||||
return ++state.n;
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, mutations });
|
||||
const store = new Store({ state, actions });
|
||||
|
||||
expect(store.state.n).toBe(1);
|
||||
const res = store.commit("inc");
|
||||
const res = store.dispatch("inc");
|
||||
expect(store.state.n).toBe(2);
|
||||
expect(res).toBe(2);
|
||||
});
|
||||
|
||||
test("dispatch allow synchronizing between actions", async () => {
|
||||
const state = { n: 1 };
|
||||
const mutations = {
|
||||
const actions = {
|
||||
inc({ state }, delta) {
|
||||
state.n += delta;
|
||||
},
|
||||
setN({ state }, n) {
|
||||
state.n = n;
|
||||
}
|
||||
};
|
||||
const actions = {
|
||||
async dosomething({ commit, dispatch }) {
|
||||
await dispatch("setTo10");
|
||||
commit("inc", 3);
|
||||
},
|
||||
async setTo10({ commit }) {
|
||||
async dosomething({ dispatch }) {
|
||||
await dispatch("setTo10");
|
||||
dispatch("inc", 3);
|
||||
},
|
||||
async setTo10({ dispatch }) {
|
||||
await Promise.resolve();
|
||||
commit("setN", 10);
|
||||
dispatch("setN", 10);
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, mutations, actions });
|
||||
const store = new Store({ state, actions });
|
||||
|
||||
expect(store.state.n).toBe(1);
|
||||
store.dispatch("dosomething");
|
||||
@@ -139,6 +98,34 @@ describe("basic use", () => {
|
||||
expect(store.state.n).toBe(13);
|
||||
});
|
||||
|
||||
test("return data from dispatching an action", async () => {
|
||||
const state = { n: 1 };
|
||||
const actions = {
|
||||
inc({ state }, delta) {
|
||||
state.n += delta;
|
||||
},
|
||||
setN({ state }, n) {
|
||||
state.n = n;
|
||||
},
|
||||
async dosomething({ dispatch }) {
|
||||
const val = await dispatch("setTo10");
|
||||
dispatch("inc", val);
|
||||
},
|
||||
async setTo10({ dispatch }) {
|
||||
await Promise.resolve();
|
||||
dispatch("setN", 10);
|
||||
return 5;
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, actions });
|
||||
|
||||
expect(store.state.n).toBe(1);
|
||||
store.dispatch("dosomething");
|
||||
expect(store.state.n).toBe(1);
|
||||
await nextTick();
|
||||
expect(store.state.n).toBe(15);
|
||||
});
|
||||
|
||||
test("env is given to actions", () => {
|
||||
expect.assertions(1);
|
||||
const someEnv = <Env>{};
|
||||
@@ -152,24 +139,6 @@ describe("basic use", () => {
|
||||
store.dispatch("someaction");
|
||||
});
|
||||
|
||||
test("set function is given to mutations", async () => {
|
||||
let updateCounter = 0;
|
||||
const state = { bertinchamps: "brune" };
|
||||
const mutations = {
|
||||
addInfo({ state, set }) {
|
||||
set(state, "chouffe", "blonde");
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, mutations, actions: {} });
|
||||
store.on("update", null, () => updateCounter++);
|
||||
|
||||
expect(updateCounter).toBe(0);
|
||||
store.commit("addInfo");
|
||||
await nextMicroTick();
|
||||
expect(updateCounter).toBe(1);
|
||||
expect(store.state).toEqual({ bertinchamps: "brune", chouffe: "blonde" });
|
||||
});
|
||||
|
||||
test("can have getters from store", async () => {
|
||||
const state = {
|
||||
beers: {
|
||||
@@ -186,7 +155,7 @@ describe("basic use", () => {
|
||||
}
|
||||
}
|
||||
};
|
||||
const getters = {
|
||||
const getters: { [key: string]: Getter } = {
|
||||
beerTasterName({ state }, beerID) {
|
||||
return state.tasters[state.beers[beerID].tasterID].name;
|
||||
},
|
||||
@@ -194,7 +163,7 @@ describe("basic use", () => {
|
||||
return state.beers[1].name;
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, mutations: {}, actions: {}, getters });
|
||||
const store = new Store({ state, actions: {}, getters });
|
||||
expect(store.getters).toBeDefined();
|
||||
expect((<any>store.getters).bestBeerName()).toBe("bertinchamps");
|
||||
expect((<any>store.getters).beerTasterName(1)).toBe("aaron");
|
||||
@@ -232,46 +201,10 @@ describe("basic use", () => {
|
||||
expect(getters.beerTasterName(1)).toBe("aaron");
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, mutations: {}, actions, getters });
|
||||
const store = new Store({ state, actions, getters });
|
||||
store.dispatch("action");
|
||||
});
|
||||
|
||||
test("getters given to mutations", async () => {
|
||||
expect.assertions(3);
|
||||
const state = {
|
||||
beers: {
|
||||
1: {
|
||||
id: 1,
|
||||
name: "bertinchamps",
|
||||
tasterID: 1
|
||||
}
|
||||
},
|
||||
tasters: {
|
||||
1: {
|
||||
id: 1,
|
||||
name: "aaron"
|
||||
}
|
||||
}
|
||||
};
|
||||
const getters = {
|
||||
beerTasterName({ state }, beerID) {
|
||||
return state.tasters[state.beers[beerID].tasterID].name;
|
||||
},
|
||||
bestBeerName({ state }) {
|
||||
return state.beers[1].name;
|
||||
}
|
||||
};
|
||||
const mutations = {
|
||||
mutation({ getters }) {
|
||||
expect(getters).toBeDefined();
|
||||
expect(getters.bestBeerName()).toBe("bertinchamps");
|
||||
expect(getters.beerTasterName(1)).toBe("aaron");
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, mutations, actions: {}, getters });
|
||||
store.commit("mutation");
|
||||
});
|
||||
|
||||
test("can use getters inside a getter", () => {
|
||||
const getters = {
|
||||
a({ getters }) {
|
||||
@@ -284,7 +217,7 @@ describe("basic use", () => {
|
||||
return `c${i}`;
|
||||
}
|
||||
};
|
||||
const store = new Store({ getters });
|
||||
const store = new Store({ getters, state: {}, actions: {} });
|
||||
|
||||
expect(store.getters.a()).toBe("bc1");
|
||||
});
|
||||
@@ -292,55 +225,54 @@ describe("basic use", () => {
|
||||
|
||||
describe("advanced state properties", () => {
|
||||
test("state in the store is reference equal after mutation", async () => {
|
||||
const state = {};
|
||||
const mutations = {
|
||||
const actions = {
|
||||
donothing() {}
|
||||
};
|
||||
const store = new Store({ state, mutations });
|
||||
expect(store.state).toBe(state);
|
||||
store.commit("donothing");
|
||||
const store = new Store({ state: {}, actions });
|
||||
const state = store.state;
|
||||
store.dispatch("donothing");
|
||||
expect(store.state).toBe(state);
|
||||
});
|
||||
|
||||
test("can use array properties in mutations", () => {
|
||||
expect.assertions(3);
|
||||
const state = { a: [1, 2, 3] };
|
||||
const mutations = {
|
||||
const actions = {
|
||||
m({ state }) {
|
||||
expect(state.a.length).toBe(3);
|
||||
const l = state.a.push(53);
|
||||
expect(l).toBe(4);
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, mutations });
|
||||
store.commit("m");
|
||||
const store = new Store({ state, actions });
|
||||
store.dispatch("m");
|
||||
expect(store.state.a).toEqual([1, 2, 3, 53]);
|
||||
});
|
||||
|
||||
test("can use object assign in store", async () => {
|
||||
const mutations = {
|
||||
const actions = {
|
||||
dosomething({ state }) {
|
||||
Object.assign(state.westmalle, { a: 3, b: 4 });
|
||||
}
|
||||
};
|
||||
const store = new Store({
|
||||
state: { westmalle: { a: 1, b: 2 } },
|
||||
mutations
|
||||
actions
|
||||
});
|
||||
store.commit("dosomething");
|
||||
store.dispatch("dosomething");
|
||||
expect(store.state.westmalle).toEqual({ a: 3, b: 4 });
|
||||
});
|
||||
|
||||
test("aku reactive store state 1", async () => {
|
||||
const mutations = {
|
||||
const actions = {
|
||||
inc({ state }) {
|
||||
state.counter++;
|
||||
}
|
||||
};
|
||||
const state = { counter: 0 };
|
||||
const store = new Store({ state, mutations });
|
||||
const store = new Store({ state, actions });
|
||||
expect(store.state.counter).toBe(0);
|
||||
store.commit("inc", {});
|
||||
store.dispatch("inc", {});
|
||||
expect(store.state.counter).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -349,17 +281,17 @@ describe("updates triggered by the store", () => {
|
||||
test("multiple commits trigger one update", async () => {
|
||||
let updateCounter = 0;
|
||||
const state = { n: 1 };
|
||||
const mutations = {
|
||||
const actions = {
|
||||
inc({ state }, delta) {
|
||||
state.n += delta;
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, mutations });
|
||||
const store = new Store({ state, actions });
|
||||
store.on("update", null, () => updateCounter++);
|
||||
|
||||
store.commit("inc", 14);
|
||||
store.dispatch("inc", 14);
|
||||
expect(updateCounter).toBe(0);
|
||||
store.commit("inc", 50);
|
||||
store.dispatch("inc", 50);
|
||||
expect(updateCounter).toBe(0);
|
||||
await nextMicroTick();
|
||||
expect(updateCounter).toBe(1);
|
||||
@@ -368,7 +300,7 @@ describe("updates triggered by the store", () => {
|
||||
test("empty commits do not trigger updates", async () => {
|
||||
let updateCounter = 0;
|
||||
const state = { n: 1 };
|
||||
const mutations = {
|
||||
const actions = {
|
||||
inc({ state }, delta) {
|
||||
state.n += delta;
|
||||
},
|
||||
@@ -378,576 +310,19 @@ describe("updates triggered by the store", () => {
|
||||
state.n = val;
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, mutations });
|
||||
const store = new Store({ state, actions });
|
||||
store.on("update", null, () => updateCounter++);
|
||||
|
||||
store.commit("noop");
|
||||
store.dispatch("noop");
|
||||
await nextMicroTick();
|
||||
expect(updateCounter).toBe(0);
|
||||
|
||||
store.commit("inc", 50);
|
||||
store.dispatch("inc", 50);
|
||||
await nextMicroTick();
|
||||
expect(updateCounter).toBe(1);
|
||||
|
||||
store.commit("noop2");
|
||||
store.dispatch("noop2");
|
||||
await nextMicroTick();
|
||||
expect(updateCounter).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("connecting a component to store", () => {
|
||||
let fixture: HTMLElement;
|
||||
let env: Env;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = makeTestFixture();
|
||||
env = makeTestEnv();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.remove();
|
||||
});
|
||||
|
||||
test("connecting a component works", async () => {
|
||||
env.qweb.addTemplate(
|
||||
"App",
|
||||
`
|
||||
<div>
|
||||
<t t-foreach="props.todos" t-as="todo" t-key="todo">
|
||||
<t t-widget="Todo" t-props="todo"/>
|
||||
</t>
|
||||
</div>`
|
||||
);
|
||||
env.qweb.addTemplate("Todo", `<span><t t-esc="props.msg"/></span>`);
|
||||
class App extends Component<any, any, any> {
|
||||
widgets = { Todo };
|
||||
}
|
||||
class Todo extends Component<any, any, any> {}
|
||||
const state = { todos: [] };
|
||||
const mutations = {
|
||||
addTodo({ state }, msg) {
|
||||
state.todos.push({ msg });
|
||||
}
|
||||
};
|
||||
function mapStateToProps(s) {
|
||||
return { todos: s.todos };
|
||||
}
|
||||
const TodoApp = connect(mapStateToProps)(App);
|
||||
const store = new Store({ state, mutations });
|
||||
(<any>env).store = store;
|
||||
const app = new TodoApp(env);
|
||||
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toMatchSnapshot();
|
||||
|
||||
store.commit("addTodo", "hello");
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("deep and shallow connecting a component", async () => {
|
||||
const state = { todos: [{ title: "Kasteel" }] };
|
||||
const mutations = {
|
||||
edit({ state }, title) {
|
||||
state.todos[0].title = title;
|
||||
}
|
||||
};
|
||||
function mapStateToProps(s) {
|
||||
return { todos: s.todos };
|
||||
}
|
||||
const store = new Store({ state, mutations });
|
||||
|
||||
env.qweb.addTemplate(
|
||||
"App",
|
||||
`
|
||||
<div>
|
||||
<span t-foreach="props.todos" t-as="todo" t-key="todo">
|
||||
<t t-esc="todo.title"/>
|
||||
</span>
|
||||
</div>`
|
||||
);
|
||||
class App extends Component<any, any, any> {}
|
||||
|
||||
const DeepTodoApp = connect(
|
||||
mapStateToProps,
|
||||
{ deep: true }
|
||||
)(App);
|
||||
const ShallowTodoApp = connect(
|
||||
mapStateToProps,
|
||||
{ deep: false }
|
||||
)(App);
|
||||
(<any>env).store = store;
|
||||
const deepTodoApp = new DeepTodoApp(env);
|
||||
const shallowTodoApp = new ShallowTodoApp(env);
|
||||
|
||||
await deepTodoApp.mount(fixture);
|
||||
|
||||
const shallowFix = makeTestFixture();
|
||||
await shallowTodoApp.mount(shallowFix);
|
||||
|
||||
expect(fixture.innerHTML).toMatchSnapshot();
|
||||
expect(shallowFix.innerHTML).toMatchSnapshot();
|
||||
|
||||
store.commit("edit", "Bertinchamps");
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toMatchSnapshot();
|
||||
expect(shallowFix.innerHTML).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("connected child components with custom hooks", async () => {
|
||||
let steps: any = [];
|
||||
env.qweb.addTemplate("Child", `<div/>`);
|
||||
class Child extends Component<any, any, any> {
|
||||
mounted() {
|
||||
steps.push("child:mounted");
|
||||
}
|
||||
willUnmount() {
|
||||
steps.push("child:willUnmount");
|
||||
}
|
||||
}
|
||||
|
||||
const ConnectedChild = connect(s => s)(Child);
|
||||
|
||||
env.qweb.addTemplate(
|
||||
"Parent",
|
||||
`
|
||||
<div>
|
||||
<t t-if="state.child" t-widget="ConnectedChild"/>
|
||||
</div>`
|
||||
);
|
||||
class Parent extends Component<any, any, any> {
|
||||
widgets = { ConnectedChild };
|
||||
|
||||
constructor(env: Env) {
|
||||
super(env);
|
||||
this.state = { child: true };
|
||||
}
|
||||
}
|
||||
|
||||
const store = new Store({ state: {} });
|
||||
(<any>env).store = store;
|
||||
const parent = new Parent(env);
|
||||
|
||||
await parent.mount(fixture);
|
||||
expect(steps).toEqual(["child:mounted"]);
|
||||
|
||||
parent.state.child = false;
|
||||
await nextTick();
|
||||
expect(steps).toEqual(["child:mounted", "child:willUnmount"]);
|
||||
});
|
||||
|
||||
test("connect receives ownprops as second argument", async () => {
|
||||
const state = { todos: [{ id: 1, text: "jupiler" }] };
|
||||
let nextId = 2;
|
||||
const mutations = {
|
||||
addTodo({ state }, text) {
|
||||
state.todos.push({ text, id: nextId++ });
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, mutations });
|
||||
|
||||
env.qweb.addTemplate("TodoItem", `<span><t t-esc="props.text"/></span>`);
|
||||
class TodoItem extends Component<any, any, any> {}
|
||||
const ConnectedTodo = connect((state, props) => {
|
||||
const todo = state.todos.find(t => t.id === props.id);
|
||||
return todo;
|
||||
})(TodoItem);
|
||||
|
||||
env.qweb.addTemplate(
|
||||
"TodoList",
|
||||
`<div>
|
||||
<t t-foreach="props.todos" t-as="todo">
|
||||
<t t-widget="ConnectedTodo" t-props="todo"/>
|
||||
</t>
|
||||
</div>`
|
||||
);
|
||||
class TodoList extends Component<any, any, any> {
|
||||
widgets = { ConnectedTodo };
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return { todos: state.todos };
|
||||
}
|
||||
const ConnectedTodoList = connect(mapStateToProps)(TodoList);
|
||||
|
||||
(<any>env).store = store;
|
||||
const app = new ConnectedTodoList(env);
|
||||
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
|
||||
|
||||
store.commit("addTodo", "hoegaarden");
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
"<div><span>jupiler</span><span>hoegaarden</span></div>"
|
||||
);
|
||||
});
|
||||
|
||||
test("connect receives store getters as third argument", async () => {
|
||||
const state = {
|
||||
importantID: 1,
|
||||
todos: [{ id: 1, text: "jupiler" }, { id: 2, text: "bertinchamps" }]
|
||||
};
|
||||
const getters = {
|
||||
importantTodoText({ state }) {
|
||||
return state.todos.find(todo => todo.id === state.importantID).text;
|
||||
},
|
||||
text({ state }, id) {
|
||||
return state.todos.find(todo => todo.id === id).text;
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, getters });
|
||||
|
||||
env.qweb.addTemplate(
|
||||
"TodoItem",
|
||||
`<div>
|
||||
<span><t t-esc="props.activeTodoText"/></span>
|
||||
<span><t t-esc="props.importantTodoText"/></span>
|
||||
</div>`
|
||||
);
|
||||
class TodoItem extends Component<any, any, any> {}
|
||||
const ConnectedTodo = connect((state, props, getters) => {
|
||||
const todo = state.todos.find(t => t.id === props.id);
|
||||
return {
|
||||
activeTodoText: getters.text(todo.id),
|
||||
importantTodoText: getters.importantTodoText()
|
||||
};
|
||||
})(TodoItem);
|
||||
|
||||
env.qweb.addTemplate(
|
||||
"TodoList",
|
||||
`<div>
|
||||
<t t-foreach="props.todos" t-as="todo">
|
||||
<t t-widget="ConnectedTodo" t-props="todo"/>
|
||||
</t>
|
||||
</div>`
|
||||
);
|
||||
class TodoList extends Component<any, any, any> {
|
||||
widgets = { ConnectedTodo };
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return { todos: state.todos };
|
||||
}
|
||||
const ConnectedTodoList = connect(mapStateToProps)(TodoList);
|
||||
|
||||
(<any>env).store = store;
|
||||
const app = new ConnectedTodoList(env);
|
||||
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe(
|
||||
"<div><div><span>jupiler</span><span>jupiler</span></div><div><span>bertinchamps</span><span>jupiler</span></div></div>"
|
||||
);
|
||||
});
|
||||
|
||||
test("connected component is updated when props are updated", async () => {
|
||||
env.qweb.addTemplate("Beer", `<span><t t-esc="props.name"/></span>`);
|
||||
class Beer extends Component<any, any, any> {}
|
||||
const ConnectedBeer = connect((state, props) => {
|
||||
return state.beers[props.id];
|
||||
})(Beer);
|
||||
|
||||
env.qweb.addTemplate(
|
||||
"App",
|
||||
`<div>
|
||||
<t t-widget="ConnectedBeer" t-props="{id: state.beerId}"/>
|
||||
</div>`
|
||||
);
|
||||
class App extends Component<any, any, any> {
|
||||
widgets = { ConnectedBeer };
|
||||
state = { beerId: 1 };
|
||||
}
|
||||
|
||||
const state = { beers: { 1: { name: "jupiler" }, 2: { name: "kwak" } } };
|
||||
const store = new Store({ state });
|
||||
(<any>env).store = store;
|
||||
const app = new App(env);
|
||||
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
|
||||
|
||||
app.state.beerId = 2;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><span>kwak</span></div>");
|
||||
});
|
||||
|
||||
test("connected component is updated when store is changed", async () => {
|
||||
env.qweb.addTemplate(
|
||||
"App",
|
||||
`
|
||||
<div>
|
||||
<span t-foreach="props.beers" t-as="beer" t-key="beer.name"><t t-esc="beer.name"/></span>
|
||||
</div>`
|
||||
);
|
||||
class App extends Component<any, any, any> {}
|
||||
|
||||
const mutations = {
|
||||
addBeer({ state }, name) {
|
||||
state.beers.push({ name });
|
||||
}
|
||||
};
|
||||
|
||||
const state = { beers: [{ name: "jupiler" }] };
|
||||
const store = new Store({ state, mutations });
|
||||
(<any>env).store = store;
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return { beers: state.beers, otherKey: 1 };
|
||||
}
|
||||
const ConnectedApp = connect(mapStateToProps)(App);
|
||||
const app = new ConnectedApp(env);
|
||||
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
|
||||
|
||||
store.commit("addBeer", "kwak");
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
"<div><span>jupiler</span><span>kwak</span></div>"
|
||||
);
|
||||
});
|
||||
|
||||
test("connected component with undefined, null and string props", async () => {
|
||||
env.qweb.addTemplate(
|
||||
"Beer",
|
||||
`<div>
|
||||
<span>taster:<t t-esc="props.taster"/></span>
|
||||
<span t-if="props.selected">selected:<t t-esc="props.selected.name"/></span>
|
||||
<span t-if="props.consumed">consumed:<t t-esc="props.consumed.name"/></span>
|
||||
</div>`
|
||||
);
|
||||
class Beer extends Component<any, any, any> {}
|
||||
const ConnectedBeer = connect((state, props) => {
|
||||
return {
|
||||
selected: state.beers[props.id],
|
||||
consumed: state.beers[state.consumedID] || null,
|
||||
taster: state.taster
|
||||
};
|
||||
})(Beer);
|
||||
|
||||
env.qweb.addTemplate(
|
||||
"App",
|
||||
`<div>
|
||||
<t t-widget="ConnectedBeer" t-props="{id: state.beerId}"/>
|
||||
</div>`
|
||||
);
|
||||
class App extends Component<any, any, any> {
|
||||
widgets = { ConnectedBeer };
|
||||
state = { beerId: 0 };
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
consume({ state }, beerId) {
|
||||
state.consumedID = beerId;
|
||||
}
|
||||
};
|
||||
const state = {
|
||||
beers: {
|
||||
1: { name: "jupiler" }
|
||||
},
|
||||
consumedID: null,
|
||||
taster: "aaron"
|
||||
};
|
||||
const store = new Store({ state, mutations });
|
||||
(<any>env).store = store;
|
||||
const app = new App(env);
|
||||
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe(
|
||||
"<div><div><span>taster:aaron</span></div></div>"
|
||||
);
|
||||
|
||||
app.state.beerId = 1;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
"<div><div><span>taster:aaron</span><span>selected:jupiler</span></div></div>"
|
||||
);
|
||||
|
||||
store.commit("consume", 1);
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
"<div><div><span>taster:aaron</span><span>selected:jupiler</span><span>consumed:jupiler</span></div></div>"
|
||||
);
|
||||
|
||||
app.state.beerId = 0;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
"<div><div><span>taster:aaron</span><span>consumed:jupiler</span></div></div>"
|
||||
);
|
||||
});
|
||||
|
||||
test("connected component deeply reactive with undefined, null and string props", async () => {
|
||||
env.qweb.addTemplate(
|
||||
"Beer",
|
||||
`<div>
|
||||
<span>taster:<t t-esc="props.taster"/></span>
|
||||
<span t-if="props.selected">selected:<t t-esc="props.selected.name"/></span>
|
||||
<span t-if="props.consumed">consumed:<t t-esc="props.consumed.name"/></span>
|
||||
</div>`
|
||||
);
|
||||
class Beer extends Component<any, any, any> {}
|
||||
const ConnectedBeer = connect((state, props) => {
|
||||
return {
|
||||
selected: state.beers[props.id],
|
||||
consumed: state.beers[state.consumedID] || null,
|
||||
taster: state.taster
|
||||
};
|
||||
})(Beer);
|
||||
|
||||
env.qweb.addTemplate(
|
||||
"App",
|
||||
`<div>
|
||||
<t t-widget="ConnectedBeer" t-props="{id: state.beerId}"/>
|
||||
</div>`
|
||||
);
|
||||
class App extends Component<any, any, any> {
|
||||
widgets = { ConnectedBeer };
|
||||
state = { beerId: 0 };
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
changeTaster({ state }, newTaster) {
|
||||
state.taster = newTaster;
|
||||
},
|
||||
consume({ state }, beerId) {
|
||||
state.consumedID = beerId;
|
||||
},
|
||||
renameBeer({ state }, { beerId, name }) {
|
||||
state.beers[beerId].name = name;
|
||||
}
|
||||
};
|
||||
const state = {
|
||||
beers: {
|
||||
1: { name: "jupiler" }
|
||||
},
|
||||
consumedID: null,
|
||||
taster: "aaron"
|
||||
};
|
||||
const store = new Store({ state, mutations });
|
||||
(<any>env).store = store;
|
||||
const app = new App(env);
|
||||
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe(
|
||||
"<div><div><span>taster:aaron</span></div></div>"
|
||||
);
|
||||
|
||||
app.state.beerId = 1;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
"<div><div><span>taster:aaron</span><span>selected:jupiler</span></div></div>"
|
||||
);
|
||||
|
||||
store.commit("renameBeer", { beerId: 1, name: "kwak" });
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
"<div><div><span>taster:aaron</span><span>selected:kwak</span></div></div>"
|
||||
);
|
||||
|
||||
store.commit("consume", 1);
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
"<div><div><span>taster:aaron</span><span>selected:kwak</span><span>consumed:kwak</span></div></div>"
|
||||
);
|
||||
|
||||
app.state.beerId = 0;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
"<div><div><span>taster:aaron</span><span>consumed:kwak</span></div></div>"
|
||||
);
|
||||
|
||||
store.commit("renameBeer", { beerId: 1, name: "jupiler" });
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
"<div><div><span>taster:aaron</span><span>consumed:jupiler</span></div></div>"
|
||||
);
|
||||
|
||||
store.commit("changeTaster", "matthieu");
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
"<div><div><span>taster:matthieu</span><span>consumed:jupiler</span></div></div>"
|
||||
);
|
||||
});
|
||||
|
||||
test("correct update order when parent/children are connected", async () => {
|
||||
const steps: string[] = [];
|
||||
|
||||
env.qweb.addTemplate(
|
||||
"Parent",
|
||||
`
|
||||
<div>
|
||||
<t t-widget="Child" t-props="{key: props.current}"/>
|
||||
</div>
|
||||
`
|
||||
);
|
||||
class Parent extends Component<any, any, any> {
|
||||
widgets = { Child: ConnectedChild };
|
||||
}
|
||||
const ConnectedParent = connect(function(s) {
|
||||
steps.push("parent");
|
||||
return { current: s.current, isvisible: s.isvisible };
|
||||
})(Parent);
|
||||
|
||||
env.qweb.addTemplate("Child", `<span><t t-esc="props.msg"/></span>`);
|
||||
class Child extends Component<any, any, any> {}
|
||||
|
||||
const ConnectedChild = connect(function(s, props) {
|
||||
steps.push("child");
|
||||
return { msg: s.msg[props.key] };
|
||||
})(Child);
|
||||
|
||||
const state = { current: "a", msg: { a: "a", b: "b" } };
|
||||
const mutations = {
|
||||
setCurrent({ state }, c) {
|
||||
state.current = c;
|
||||
}
|
||||
};
|
||||
|
||||
const store = new Store({ state, mutations });
|
||||
(<any>env).store = store;
|
||||
const app = new ConnectedParent(env);
|
||||
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><span>a</span></div>");
|
||||
expect(steps).toEqual(["parent", "child"]);
|
||||
|
||||
store.commit("setCurrent", "b");
|
||||
await nextTick();
|
||||
expect(steps).toEqual(["parent", "child", "parent", "child", "child"]);
|
||||
});
|
||||
|
||||
test("connected component willpatch/patch hooks are called on store updates", async () => {
|
||||
const steps: string[] = [];
|
||||
env.qweb.addTemplate("App", `<div><t t-esc="props.msg"/></div>`);
|
||||
class App extends Component<any, any, any> {
|
||||
willPatch() {
|
||||
steps.push("willpatch");
|
||||
}
|
||||
patched() {
|
||||
steps.push("patched");
|
||||
}
|
||||
}
|
||||
const ConnectedApp = connect(function(s) {
|
||||
return { msg: s.msg };
|
||||
})(App);
|
||||
|
||||
const state = { msg: "a" };
|
||||
const mutations = {
|
||||
setMsg({ state }, c) {
|
||||
state.msg = c;
|
||||
}
|
||||
};
|
||||
|
||||
const store = new Store({ state, mutations });
|
||||
(<any>env).store = store;
|
||||
const app = new ConnectedApp(env);
|
||||
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div>a</div>");
|
||||
|
||||
store.commit("setMsg", "b");
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div>b</div>");
|
||||
expect(steps).toEqual(["willpatch", "patched"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,990 @@
|
||||
import { Component, Env } from "../src/component/component";
|
||||
import { Store, useStore, useDispatch, useGetters } from "../src/store";
|
||||
import { useState } from "../src/hooks";
|
||||
import { xml } from "../src/tags";
|
||||
import { shallowEqual } from "../src/utils";
|
||||
import { makeTestEnv, makeTestFixture, nextTick, makeDeferred } from "./helpers";
|
||||
|
||||
describe("connecting a component to store", () => {
|
||||
let fixture: HTMLElement;
|
||||
let env: Env;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = makeTestFixture();
|
||||
env = makeTestEnv();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.remove();
|
||||
});
|
||||
|
||||
test("connecting a component works, with useStore", async () => {
|
||||
let nextId = 1;
|
||||
const state = { todos: [] };
|
||||
const actions = {
|
||||
addTodo({ state }, msg) {
|
||||
state.todos.push({ msg, id: nextId++ });
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, actions });
|
||||
|
||||
class App extends Component<any, any> {
|
||||
static template = xml`
|
||||
<div>
|
||||
<span t-foreach="todos" t-key="todo.id" t-as="todo"><t t-esc="todo.msg"/></span>
|
||||
</div>`;
|
||||
todos = useStore(state => state.todos);
|
||||
}
|
||||
|
||||
(<any>env).store = store;
|
||||
const app = new App(env);
|
||||
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div></div>");
|
||||
|
||||
await store.dispatch("addTodo", "hello");
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><span>hello</span></div>");
|
||||
});
|
||||
|
||||
test("can use useStore twice in a component", async () => {
|
||||
const state = { a: 1, b: 2 };
|
||||
const actions = {
|
||||
doSomething({ state }) {
|
||||
state.a = 2;
|
||||
state.b = 3;
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, actions });
|
||||
|
||||
class App extends Component<any, any> {
|
||||
static template = xml`
|
||||
<div>
|
||||
<span t-esc="a.value"/>
|
||||
<span t-esc="b.value"/>
|
||||
</div>`;
|
||||
a = useStore(state => ({value: state.a}));
|
||||
b = useStore(state => ({value: state.b}));
|
||||
}
|
||||
App.prototype.__render = jest.fn(App.prototype.__render);
|
||||
|
||||
(<any>env).store = store;
|
||||
const app = new App(env);
|
||||
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><span>1</span><span>2</span></div>");
|
||||
expect(App.prototype.__render).toBeCalledTimes(1);
|
||||
|
||||
await store.dispatch("doSomething");
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><span>2</span><span>3</span></div>");
|
||||
expect(App.prototype.__render).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
test("useStore: do not re-render if not changed", async () => {
|
||||
let nextId = 1;
|
||||
const state = { todos: [], a: 1 };
|
||||
const actions = {
|
||||
addTodo({ state }, msg) {
|
||||
state.todos.push({ msg, id: nextId++ });
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, actions });
|
||||
|
||||
class App extends Component<any, any> {
|
||||
static template = xml`
|
||||
<div>
|
||||
<span t-foreach="todos" t-key="todo.id" t-as="todo"><t t-esc="todo.msg"/></span>
|
||||
</div>`;
|
||||
todos = useStore(state => state.todos);
|
||||
}
|
||||
App.prototype.__render = jest.fn(App.prototype.__render);
|
||||
|
||||
(<any>env).store = store;
|
||||
const app = new App(env);
|
||||
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div></div>");
|
||||
expect(App.prototype.__render).toBeCalledTimes(1);
|
||||
|
||||
store.state.todos.push({ id: 3, msg: "hello" });
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><span>hello</span></div>");
|
||||
expect(App.prototype.__render).toBeCalledTimes(2);
|
||||
|
||||
store.state.a = 2;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><span>hello</span></div>");
|
||||
expect(App.prototype.__render).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
test("connecting a component with useStore returning number", async () => {
|
||||
let nextId = 1;
|
||||
const state = { todos: [] };
|
||||
const actions = {
|
||||
addTodo({ state }, msg) {
|
||||
state.todos.push({ msg, id: nextId++ });
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, actions });
|
||||
|
||||
class App extends Component<any, any> {
|
||||
static template = xml`<div><t t-esc="nbrTodos.value"/></div>`;
|
||||
nbrTodos = useStore(state => ({ value: state.todos.length }));
|
||||
}
|
||||
|
||||
(<any>env).store = store;
|
||||
const app = new App(env);
|
||||
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div>0</div>");
|
||||
|
||||
await store.dispatch("addTodo", "hello");
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div>1</div>");
|
||||
});
|
||||
|
||||
test("connecting a component with useStore returning number", async () => {
|
||||
let nextId = 1;
|
||||
const state = { todos: [], a: 1 };
|
||||
const actions = {
|
||||
addTodo({ state }, msg) {
|
||||
state.todos.push({ msg, id: nextId++ });
|
||||
},
|
||||
incrementA({ state }) {
|
||||
state.a++;
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, actions });
|
||||
class App extends Component<any, any> {
|
||||
static template = xml`<div><t t-esc="nbrTodos.value"/></div>`;
|
||||
nbrTodos = useStore(state => ({ value: state.todos.length }), { isEqual: shallowEqual });
|
||||
}
|
||||
App.prototype.__render = jest.fn(App.prototype.__render);
|
||||
|
||||
(<any>env).store = store;
|
||||
const app = new App(env);
|
||||
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div>0</div>");
|
||||
expect(App.prototype.__render).toBeCalledTimes(1);
|
||||
|
||||
await store.dispatch("addTodo", "hello");
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div>1</div>");
|
||||
expect(App.prototype.__render).toBeCalledTimes(2);
|
||||
|
||||
await store.dispatch("incrementA");
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div>1</div>");
|
||||
expect(App.prototype.__render).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
test("connecting a component to a local store", async () => {
|
||||
let nextId = 1;
|
||||
const state = { todos: [] };
|
||||
const actions = {
|
||||
addTodo({ state }, msg) {
|
||||
state.todos.push({ msg, id: nextId++ });
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, actions });
|
||||
|
||||
class App extends Component<any, any> {
|
||||
static template = xml`
|
||||
<div>
|
||||
<span t-foreach="todos" t-key="todo.id" t-as="todo"><t t-esc="todo.msg"/></span>
|
||||
</div>`;
|
||||
todos = useStore(state => state.todos, { store });
|
||||
}
|
||||
|
||||
const app = new App(env);
|
||||
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div></div>");
|
||||
|
||||
await store.dispatch("addTodo", "hello");
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><span>hello</span></div>");
|
||||
});
|
||||
|
||||
test("can dispatch actions from a connected component", async () => {
|
||||
const store = new Store({
|
||||
state: { value: 1 },
|
||||
actions: {
|
||||
inc({ state }) {
|
||||
state.value++;
|
||||
}
|
||||
}
|
||||
});
|
||||
(<any>env).store = store;
|
||||
|
||||
class App extends Component<any, any> {
|
||||
static template = xml`
|
||||
<div>
|
||||
<button t-on-click="dispatch('inc')">Inc</button>
|
||||
<span><t t-esc="storeState.value"/></span>
|
||||
</div>`;
|
||||
storeState = useStore(state => state);
|
||||
dispatch = useDispatch();
|
||||
}
|
||||
const app = new App(env);
|
||||
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><button>Inc</button><span>1</span></div>");
|
||||
|
||||
fixture.querySelector("button")!.click();
|
||||
await nextTick();
|
||||
|
||||
expect(fixture.innerHTML).toBe("<div><button>Inc</button><span>2</span></div>");
|
||||
});
|
||||
|
||||
test("useStore can use props", async () => {
|
||||
const state = { todos: [{ id: 1, text: "jupiler" }, { id: 2, text: "chimay" }] };
|
||||
const store = new Store({ state, actions: {} });
|
||||
|
||||
class TodoItem extends Component<any, any> {
|
||||
static template = xml`<span><t t-esc="todo.text"/></span>`;
|
||||
todo = useStore((state, props) => {
|
||||
return state.todos.find(t => t.id === props.todoId);
|
||||
});
|
||||
}
|
||||
|
||||
class App extends Component<any, any> {
|
||||
static template = xml`<div><TodoItem todoId="state.currentId"/></div>`;
|
||||
static components = { TodoItem };
|
||||
state = useState({ currentId: 1 });
|
||||
}
|
||||
|
||||
(<any>env).store = store;
|
||||
const app = new App(env);
|
||||
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
|
||||
|
||||
app.state.currentId = 2;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><span>chimay</span></div>");
|
||||
});
|
||||
|
||||
test("useStore receives props as second argument", async () => {
|
||||
const state = { todos: [{ id: 1, text: "jupiler" }] };
|
||||
let nextId = 2;
|
||||
const actions = {
|
||||
addTodo({ state }, text) {
|
||||
state.todos.push({ text, id: nextId++ });
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, actions });
|
||||
|
||||
class TodoItem extends Component<any, any> {
|
||||
static template = xml`<span><t t-esc="todo.text"/></span>`;
|
||||
todo = useStore((state, props) => {
|
||||
return state.todos.find(t => t.id === props.id);
|
||||
});
|
||||
}
|
||||
|
||||
class TodoList extends Component<any, any> {
|
||||
static template = xml`
|
||||
<div>
|
||||
<TodoItem t-foreach="todos" t-as="todo" id="todo.id" t-key="todo.id"/>
|
||||
</div>`;
|
||||
static components = { TodoItem };
|
||||
todos = useStore(state => state.todos);
|
||||
}
|
||||
|
||||
(<any>env).store = store;
|
||||
const app = new TodoList(env);
|
||||
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
|
||||
|
||||
store.dispatch("addTodo", "hoegaarden");
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><span>jupiler</span><span>hoegaarden</span></div>");
|
||||
});
|
||||
|
||||
test("can call useGetters to receive store getters", async () => {
|
||||
const state = {
|
||||
importantID: 1,
|
||||
todos: [{ id: 1, text: "jupiler" }, { id: 2, text: "bertinchamps" }]
|
||||
};
|
||||
const getters = {
|
||||
importantTodoText({ state }) {
|
||||
return state.todos.find(todo => todo.id === state.importantID).text;
|
||||
},
|
||||
text({ state }, id) {
|
||||
return state.todos.find(todo => todo.id === id).text;
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, getters });
|
||||
|
||||
class TodoItem extends Component<any, any> {
|
||||
static template = xml`
|
||||
<div>
|
||||
<span><t t-esc="storeProps.activeTodoText"/></span>
|
||||
<span><t t-esc="storeProps.importantTodoText"/></span>
|
||||
</div>`;
|
||||
getters = useGetters();
|
||||
storeProps = useStore((state, props) => {
|
||||
const todo = state.todos.find(t => t.id === props.id);
|
||||
return {
|
||||
activeTodoText: this.getters.text(todo.id),
|
||||
importantTodoText: this.getters.importantTodoText()
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
class TodoList extends Component<any, any> {
|
||||
static components = { TodoItem };
|
||||
static template = xml`
|
||||
<div>
|
||||
<t t-foreach="todos" t-as="todo">
|
||||
<TodoItem id="todo.id" t-key="todo.id"/>
|
||||
</t>
|
||||
</div>`;
|
||||
todos = useStore(state => state.todos);
|
||||
}
|
||||
|
||||
(<any>env).store = store;
|
||||
const app = new TodoList(env);
|
||||
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe(
|
||||
"<div><div><span>jupiler</span><span>jupiler</span></div><div><span>bertinchamps</span><span>jupiler</span></div></div>"
|
||||
);
|
||||
});
|
||||
|
||||
test("connected component is updated when props are updated", async () => {
|
||||
class Beer extends Component<any, any> {
|
||||
static template = xml`<span><t t-esc="beer.name"/></span>`;
|
||||
beer = useStore((state, props) => state.beers[props.id]);
|
||||
}
|
||||
|
||||
class App extends Component<any, any> {
|
||||
static template = xml`<div><Beer id="state.beerId"/></div>`;
|
||||
static components = { Beer };
|
||||
state = useState({ beerId: 1 });
|
||||
}
|
||||
|
||||
const state = { beers: { 1: { name: "jupiler" }, 2: { name: "kwak" } } };
|
||||
const store = new Store({ state });
|
||||
(<any>env).store = store;
|
||||
const app = new App(env);
|
||||
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
|
||||
|
||||
app.state.beerId = 2;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><span>kwak</span></div>");
|
||||
});
|
||||
|
||||
test("connected component is updated when store is changed", async () => {
|
||||
class App extends Component<any, any> {
|
||||
static template = xml`
|
||||
<div>
|
||||
<span t-foreach="data.beers" t-as="beer" t-key="beer.name"><t t-esc="beer.name"/></span>
|
||||
</div>`;
|
||||
|
||||
// we have here a new object
|
||||
data = useStore(state => ({ beers: state.beers, otherKey: 1 }));
|
||||
}
|
||||
|
||||
const actions = {
|
||||
addBeer({ state }, name) {
|
||||
state.beers.push({ name });
|
||||
}
|
||||
};
|
||||
|
||||
const state = { beers: [{ name: "jupiler" }] };
|
||||
const store = new Store({ state, actions });
|
||||
(<any>env).store = store;
|
||||
|
||||
const app = new App(env);
|
||||
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
|
||||
|
||||
store.dispatch("addBeer", "kwak");
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><span>jupiler</span><span>kwak</span></div>");
|
||||
});
|
||||
|
||||
test("connected component with undefined, null and string props", async () => {
|
||||
class Beer extends Component<any, any> {
|
||||
static template = xml`
|
||||
<div t-name="Beer">
|
||||
<span>taster:<t t-esc="data.taster"/></span>
|
||||
<span t-if="data.selected">selected:<t t-esc="data.selected.name"/></span>
|
||||
<span t-if="data.consumed">consumed:<t t-esc="data.consumed.name"/></span>
|
||||
</div>`;
|
||||
data = useStore((state, props) => ({
|
||||
selected: state.beers[props.id],
|
||||
consumed: state.beers[state.consumedID] || null,
|
||||
taster: state.taster
|
||||
}));
|
||||
}
|
||||
|
||||
class App extends Component<any, any> {
|
||||
static template = xml`<div><Beer id="state.beerId"/></div>`;
|
||||
static components = { Beer };
|
||||
state = useState({ beerId: 0 });
|
||||
}
|
||||
|
||||
const actions = {
|
||||
consume({ state }, beerId) {
|
||||
state.consumedID = beerId;
|
||||
}
|
||||
};
|
||||
const state = {
|
||||
beers: {
|
||||
1: { name: "jupiler" }
|
||||
},
|
||||
consumedID: null,
|
||||
taster: "aaron"
|
||||
};
|
||||
const store = new Store({ state, actions });
|
||||
(<any>env).store = store;
|
||||
const app = new App(env);
|
||||
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><div><span>taster:aaron</span></div></div>");
|
||||
|
||||
app.state.beerId = 1;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
"<div><div><span>taster:aaron</span><span>selected:jupiler</span></div></div>"
|
||||
);
|
||||
|
||||
store.dispatch("consume", 1);
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
"<div><div><span>taster:aaron</span><span>selected:jupiler</span><span>consumed:jupiler</span></div></div>"
|
||||
);
|
||||
|
||||
app.state.beerId = 0;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
"<div><div><span>taster:aaron</span><span>consumed:jupiler</span></div></div>"
|
||||
);
|
||||
});
|
||||
|
||||
test("connected component deeply reactive with undefined, null and string props", async () => {
|
||||
class Beer extends Component<any, any> {
|
||||
static template = xml`
|
||||
<div>
|
||||
<span>taster:<t t-esc="info.taster"/></span>
|
||||
<span t-if="info.selected">selected:<t t-esc="info.selected.name"/></span>
|
||||
<span t-if="info.consumed">consumed:<t t-esc="info.consumed.name"/></span>
|
||||
</div>`;
|
||||
info = useStore(function(state, props) {
|
||||
return {
|
||||
selected: state.beers[props.id],
|
||||
consumed: state.beers[state.consumedID] || null,
|
||||
taster: state.taster
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
class App extends Component<any, any> {
|
||||
static template = xml`<div><Beer id="state.beerId"/></div>`;
|
||||
static components = { Beer };
|
||||
state = useState({ beerId: 0 });
|
||||
}
|
||||
|
||||
const actions = {
|
||||
changeTaster({ state }, newTaster) {
|
||||
state.taster = newTaster;
|
||||
},
|
||||
consume({ state }, beerId) {
|
||||
state.consumedID = beerId;
|
||||
},
|
||||
renameBeer({ state }, { beerId, name }) {
|
||||
state.beers[beerId].name = name;
|
||||
}
|
||||
};
|
||||
const state = {
|
||||
beers: {
|
||||
1: { name: "jupiler" }
|
||||
},
|
||||
consumedID: null,
|
||||
taster: "aaron"
|
||||
};
|
||||
const store = new Store({ state, actions });
|
||||
(<any>env).store = store;
|
||||
const app = new App(env);
|
||||
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><div><span>taster:aaron</span></div></div>");
|
||||
|
||||
app.state.beerId = 1;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
"<div><div><span>taster:aaron</span><span>selected:jupiler</span></div></div>"
|
||||
);
|
||||
|
||||
store.dispatch("renameBeer", { beerId: 1, name: "kwak" });
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
"<div><div><span>taster:aaron</span><span>selected:kwak</span></div></div>"
|
||||
);
|
||||
|
||||
store.dispatch("consume", 1);
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
"<div><div><span>taster:aaron</span><span>selected:kwak</span><span>consumed:kwak</span></div></div>"
|
||||
);
|
||||
|
||||
app.state.beerId = 0;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
"<div><div><span>taster:aaron</span><span>consumed:kwak</span></div></div>"
|
||||
);
|
||||
|
||||
store.dispatch("renameBeer", { beerId: 1, name: "jupiler" });
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
"<div><div><span>taster:aaron</span><span>consumed:jupiler</span></div></div>"
|
||||
);
|
||||
|
||||
store.dispatch("changeTaster", "matthieu");
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
"<div><div><span>taster:matthieu</span><span>consumed:jupiler</span></div></div>"
|
||||
);
|
||||
});
|
||||
|
||||
test("correct update order when parent/children are connected", async () => {
|
||||
const steps: string[] = [];
|
||||
|
||||
class Child extends Component<any, any> {
|
||||
static template = xml`<span><t t-esc="state.msg"/></span>`;
|
||||
|
||||
state = useStore((state, props) => {
|
||||
steps.push("child");
|
||||
return { msg: state.msg[props.key] };
|
||||
});
|
||||
}
|
||||
class Parent extends Component<any, any> {
|
||||
static template = xml`<div><Child key="state.current"/></div>`;
|
||||
static components = { Child };
|
||||
|
||||
state = useStore(state => {
|
||||
steps.push("parent");
|
||||
return {
|
||||
current: state.current,
|
||||
isvisible: state.isvisible
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const state = { current: "a", msg: { a: "a", b: "b" } };
|
||||
const actions = {
|
||||
setCurrent({ state }, c) {
|
||||
state.current = c;
|
||||
}
|
||||
};
|
||||
|
||||
const store = new Store({ state, actions });
|
||||
(<any>env).store = store;
|
||||
const app = new Parent(env);
|
||||
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><span>a</span></div>");
|
||||
expect(steps).toEqual(["parent", "child"]);
|
||||
|
||||
store.dispatch("setCurrent", "b");
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><span>b</span></div>");
|
||||
expect(steps).toEqual(["parent", "child", "parent", "child"]);
|
||||
});
|
||||
|
||||
test("correct update order when parent/children are connected, part 2", async () => {
|
||||
const steps: string[] = [];
|
||||
let def = makeDeferred();
|
||||
def.resolve();
|
||||
|
||||
class Child extends Component<any, any> {
|
||||
static template = xml`<span><t t-esc="state.msg"/></span>`;
|
||||
state = useStore((s, props) => {
|
||||
steps.push("child");
|
||||
return { msg: s.messages[props.someId] };
|
||||
});
|
||||
}
|
||||
|
||||
class Parent extends Component<any, any> {
|
||||
static template = xml`
|
||||
<div>
|
||||
<Child t-if="state.flag" someId="state.someId"/>
|
||||
</div>`;
|
||||
static components = { Child };
|
||||
state = useStore(s => {
|
||||
steps.push("parent");
|
||||
return { flag: s.flag, someId: s.someId };
|
||||
});
|
||||
|
||||
async render(force) {
|
||||
await def;
|
||||
return super.render(force);
|
||||
}
|
||||
}
|
||||
|
||||
const state = { someId: 1, flag: true, messages: { 1: "abc" } };
|
||||
const actions = {
|
||||
setFlagToFalse({ state }) {
|
||||
state.flag = false;
|
||||
}
|
||||
};
|
||||
|
||||
const store = new Store({ state, actions });
|
||||
(<any>env).store = store;
|
||||
const app = new Parent(env);
|
||||
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><span>abc</span></div>");
|
||||
expect(steps).toEqual(["parent", "child"]);
|
||||
|
||||
def = makeDeferred();
|
||||
store.dispatch("setFlagToFalse");
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><span>abc</span></div>");
|
||||
expect(steps).toEqual(["parent", "child", "parent"]);
|
||||
|
||||
def.resolve();
|
||||
await nextTick();
|
||||
expect(steps).toEqual(["parent", "child", "parent"]);
|
||||
expect(fixture.innerHTML).toBe("<div></div>");
|
||||
});
|
||||
|
||||
test("connected parent/children: no double rendering", async () => {
|
||||
let steps: string[] = [];
|
||||
const actions = {
|
||||
editTodo({ state }) {
|
||||
state.todos[1].title = "abc";
|
||||
}
|
||||
};
|
||||
const todos = { 1: { id: 1, title: "kikoou" } };
|
||||
const state = {
|
||||
todos
|
||||
};
|
||||
const store = new Store({
|
||||
state,
|
||||
actions
|
||||
});
|
||||
|
||||
class TodoItem extends Component<any, any> {
|
||||
static template = xml`
|
||||
<div class="todo">
|
||||
<t t-esc="state.todo.title"/>
|
||||
<button class="destroy" t-on-click="editTodo">x</button>
|
||||
</div>`;
|
||||
state = useStore((state, props) => {
|
||||
steps.push("item:usestore");
|
||||
return {
|
||||
todo: state.todos[props.id]
|
||||
};
|
||||
});
|
||||
|
||||
editTodo() {
|
||||
this.env.store.dispatch("editTodo");
|
||||
}
|
||||
__render(f) {
|
||||
steps.push("item:render");
|
||||
return super.__render(f);
|
||||
}
|
||||
}
|
||||
class TodoApp extends Component<any, any> {
|
||||
static template = xml`
|
||||
<div class="todoapp">
|
||||
<t t-foreach="Object.values(state.todos)" t-as="todo">
|
||||
<TodoItem t-key="todo.id" id="todo.id"/>
|
||||
</t>
|
||||
</div>`;
|
||||
static components = { TodoItem };
|
||||
state = useStore(state => {
|
||||
steps.push("app:usestore");
|
||||
return { todos: state.todos };
|
||||
});
|
||||
__render(f) {
|
||||
steps.push("app:render");
|
||||
return super.__render(f);
|
||||
}
|
||||
}
|
||||
|
||||
(<any>env).store = store;
|
||||
const app = new TodoApp(env);
|
||||
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div class="todoapp"><div class="todo">kikoou<button class="destroy">x</button></div></div>'
|
||||
);
|
||||
|
||||
expect(steps).toEqual(["app:usestore", "app:render", "item:usestore", "item:render"]);
|
||||
steps = [];
|
||||
fixture.querySelector("button")!.click();
|
||||
await nextTick();
|
||||
expect(steps).toEqual(["app:usestore", "app:render", "item:usestore", "item:render"]);
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div class="todoapp"><div class="todo">abc<button class="destroy">x</button></div></div>'
|
||||
);
|
||||
});
|
||||
|
||||
test("connected parent/children: no rendering if child is destroyed", async () => {
|
||||
let steps: string[] = [];
|
||||
const actions = {
|
||||
removeTodo({ state }) {
|
||||
delete state.todos[1];
|
||||
}
|
||||
};
|
||||
const todos = { 1: { id: 1, title: "kikoou" } };
|
||||
const state = {
|
||||
todos
|
||||
};
|
||||
const store = new Store({
|
||||
state,
|
||||
actions
|
||||
});
|
||||
|
||||
class TodoItem extends Component<any, any> {
|
||||
static template = xml`
|
||||
<div class="todo">
|
||||
<t t-esc="state.todo.title"/>
|
||||
<button class="destroy" t-on-click="removeTodo">x</button>
|
||||
</div>`;
|
||||
state = useStore((state, props) => {
|
||||
steps.push("item:usestore");
|
||||
return {
|
||||
todo: state.todos[props.id]
|
||||
};
|
||||
});
|
||||
|
||||
removeTodo() {
|
||||
this.env.store.dispatch("removeTodo");
|
||||
}
|
||||
__render(f) {
|
||||
steps.push("item:render");
|
||||
return super.__render(f);
|
||||
}
|
||||
}
|
||||
|
||||
class TodoApp extends Component<any, any> {
|
||||
static template = xml`
|
||||
<div class="todoapp">
|
||||
<t t-foreach="Object.values(state.todos)" t-as="todo">
|
||||
<TodoItem t-key="todo.id" id="todo.id"/>
|
||||
</t>
|
||||
</div>`;
|
||||
static components = { TodoItem };
|
||||
state = useStore(state => {
|
||||
steps.push("app:usestore");
|
||||
return { todos: state.todos };
|
||||
});
|
||||
__render(f) {
|
||||
steps.push("app:render");
|
||||
return super.__render(f);
|
||||
}
|
||||
}
|
||||
|
||||
(<any>env).store = store;
|
||||
const app = new TodoApp(env);
|
||||
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div class="todoapp"><div class="todo">kikoou<button class="destroy">x</button></div></div>'
|
||||
);
|
||||
|
||||
expect(steps).toEqual(["app:usestore", "app:render", "item:usestore", "item:render"]);
|
||||
|
||||
fixture.querySelector("button")!.click();
|
||||
await nextTick();
|
||||
expect(steps).toEqual([
|
||||
"app:usestore",
|
||||
"app:render",
|
||||
"item:usestore",
|
||||
"item:render",
|
||||
"app:usestore",
|
||||
"app:render"
|
||||
]);
|
||||
expect(fixture.innerHTML).toBe('<div class="todoapp"></div>');
|
||||
});
|
||||
|
||||
test("connected component willpatch/patch hooks are called on store updates", async () => {
|
||||
const steps: string[] = [];
|
||||
|
||||
class App extends Component<any, any> {
|
||||
static template = xml`<div><t t-esc="store.msg"/></div>`;
|
||||
store = useStore(s => ({ msg: s.msg }));
|
||||
|
||||
willPatch() {
|
||||
steps.push("willpatch");
|
||||
}
|
||||
patched() {
|
||||
steps.push("patched");
|
||||
}
|
||||
}
|
||||
|
||||
const state = { msg: "a" };
|
||||
const actions = {
|
||||
setMsg({ state }, c) {
|
||||
state.msg = c;
|
||||
}
|
||||
};
|
||||
|
||||
const store = new Store({ state, actions });
|
||||
(<any>env).store = store;
|
||||
const app = new App(env);
|
||||
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div>a</div>");
|
||||
|
||||
store.dispatch("setMsg", "b");
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div>b</div>");
|
||||
expect(steps).toEqual(["willpatch", "patched"]);
|
||||
});
|
||||
|
||||
test("connected child components stop listening to store when destroyed", async () => {
|
||||
let steps: any = [];
|
||||
|
||||
class Child extends Component<any, any> {
|
||||
static template = xml`<div><t t-esc="store.val"/></div>`;
|
||||
store = useStore(s => s);
|
||||
}
|
||||
|
||||
class Parent extends Component<any, any> {
|
||||
static template = xml`<div><Child t-if="state.child" /></div>`;
|
||||
static components = { Child };
|
||||
state = useState({ child: true });
|
||||
}
|
||||
|
||||
class TestStore extends Store {
|
||||
on(eventType, owner, callback) {
|
||||
steps.push(`on:${eventType}`);
|
||||
super.on(eventType, owner, callback);
|
||||
}
|
||||
off(eventType, owner) {
|
||||
steps.push(`off:${eventType}`);
|
||||
super.off(eventType, owner);
|
||||
}
|
||||
}
|
||||
const store = new TestStore({ state: { val: 1 } });
|
||||
(<any>env).store = store;
|
||||
const parent = new Parent(env);
|
||||
|
||||
await parent.mount(fixture);
|
||||
expect(steps).toEqual(["on:update"]);
|
||||
expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
|
||||
|
||||
parent.state.child = false;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div></div>");
|
||||
expect(steps).toEqual(["on:update", "off:update"]);
|
||||
});
|
||||
|
||||
test("dispatch an action", async () => {
|
||||
class App extends Component<any, any> {
|
||||
static template = xml`<div><t t-esc="store.counter"/></div>`;
|
||||
store = useStore(state => state);
|
||||
dispatch = useDispatch();
|
||||
}
|
||||
|
||||
const state = {
|
||||
counter: 0
|
||||
};
|
||||
|
||||
const actions = {
|
||||
inc({ state }) {
|
||||
return ++state.counter;
|
||||
}
|
||||
};
|
||||
|
||||
const store = new Store({ state, actions });
|
||||
(<any>env).store = store;
|
||||
const app = new App(env);
|
||||
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div>0</div>");
|
||||
|
||||
const res = app.dispatch("inc");
|
||||
expect(res).toBe(1);
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div>1</div>");
|
||||
});
|
||||
});
|
||||
|
||||
describe("various scenarios", () => {
|
||||
let fixture: HTMLElement;
|
||||
let env: Env;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = makeTestFixture();
|
||||
env = makeTestEnv();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.remove();
|
||||
});
|
||||
|
||||
test("scenarios with async store updates and some components events", async () => {
|
||||
const actions = {
|
||||
async deleteAttachment({ state }) {
|
||||
await Promise.resolve();
|
||||
delete state.attachments[100];
|
||||
state.messages[10].attachmentIds = [];
|
||||
}
|
||||
};
|
||||
const state = {
|
||||
attachments: {
|
||||
100: {
|
||||
id: 100,
|
||||
name: "text.txt"
|
||||
}
|
||||
},
|
||||
messages: {
|
||||
10: {
|
||||
attachmentIds: [100],
|
||||
id: 10
|
||||
}
|
||||
}
|
||||
};
|
||||
const store = new Store({ actions, state });
|
||||
|
||||
class Attachment extends Component<any, any> {
|
||||
static template = xml`
|
||||
<div>
|
||||
<span>Attachment <t t-esc="props.id"/></span>
|
||||
<span>Name: <t t-esc="attachment.name"/></span>
|
||||
</div>`;
|
||||
|
||||
attachment = useStore((state, props) => ({ name: state.attachments[props.id].name }));
|
||||
}
|
||||
|
||||
class Message extends Component<any, any> {
|
||||
static template = xml`
|
||||
<div>
|
||||
<button t-on-click="doStuff">Do stuff</button>
|
||||
<Attachment t-foreach="store.attachmentIds" t-key="attachmentId" t-as="attachmentId" id="attachmentId"/>
|
||||
</div>`;
|
||||
|
||||
store = useStore(state => ({ attachmentIds: state.messages[10].attachmentIds }));
|
||||
static components = { Attachment };
|
||||
state = { isAttachmentDeleted: false };
|
||||
dispatch = useDispatch();
|
||||
doStuff() {
|
||||
this.dispatch("deleteAttachment", 100);
|
||||
this.state.isAttachmentDeleted = true;
|
||||
}
|
||||
}
|
||||
|
||||
(<any>env).store = store;
|
||||
const message = new Message(env);
|
||||
await message.mount(fixture);
|
||||
|
||||
expect(fixture.innerHTML).toMatchSnapshot();
|
||||
|
||||
fixture.querySelector("button")!.click();
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
+84
-152
@@ -1,4 +1,6 @@
|
||||
import { h, patch, init } from "../src/vdom";
|
||||
import { h, patch } from "../src/vdom";
|
||||
import { htmlToVDOM } from "../src/vdom/html_to_vdom";
|
||||
import { init, addNS } from "../src/vdom/vdom";
|
||||
|
||||
function map(list, fn) {
|
||||
var ret: any[] = [];
|
||||
@@ -59,9 +61,7 @@ describe("attributes", function() {
|
||||
test("are set correctly when namespaced", function() {
|
||||
const vnode1 = h("div", { attrs: { "xlink:href": "#foo" } });
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
expect(elm.getAttributeNS("http://www.w3.org/1999/xlink", "href")).toBe(
|
||||
"#foo"
|
||||
);
|
||||
expect(elm.getAttributeNS("http://www.w3.org/1999/xlink", "href")).toBe("#foo");
|
||||
});
|
||||
|
||||
test("should not touch class nor id fields", function() {
|
||||
@@ -207,23 +207,15 @@ describe("snabbdom", function() {
|
||||
expect(elm.firstChild.namespaceURI).toBe(SVGNamespace);
|
||||
|
||||
// verify that svg tag automatically gets svg namespace
|
||||
elm = patch(
|
||||
vnode0,
|
||||
h("svg", [
|
||||
h("foreignObject", [h("div", ["I am HTML embedded in SVG"])])
|
||||
])
|
||||
).elm;
|
||||
const vnode = h("svg", [h("foreignObject", [h("div", ["I am HTML embedded in SVG"])])]);
|
||||
// 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.firstChild.namespaceURI).toBe(SVGNamespace);
|
||||
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() {
|
||||
@@ -352,13 +344,7 @@ describe("snabbdom", function() {
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
expect(elm.children.length).toBe(2);
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual([
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5"
|
||||
]);
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual(["1", "2", "3", "4", "5"]);
|
||||
});
|
||||
|
||||
test("add elements in the middle", function() {
|
||||
@@ -368,13 +354,7 @@ describe("snabbdom", function() {
|
||||
expect(elm.children.length).toBe(4);
|
||||
expect(elm.children.length).toBe(4);
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual([
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5"
|
||||
]);
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual(["1", "2", "3", "4", "5"]);
|
||||
});
|
||||
|
||||
test("add elements at beginning and end", function() {
|
||||
@@ -383,13 +363,7 @@ describe("snabbdom", function() {
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
expect(elm.children.length).toBe(3);
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual([
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5"
|
||||
]);
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual(["1", "2", "3", "4", "5"]);
|
||||
});
|
||||
|
||||
test("adds children to parent with no children", function() {
|
||||
@@ -588,35 +562,19 @@ describe("snabbdom", function() {
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
expect(elm.children.length).toBe(6);
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual([
|
||||
"4",
|
||||
"3",
|
||||
"2",
|
||||
"1",
|
||||
"5",
|
||||
"0"
|
||||
]);
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual(["4", "3", "2", "1", "5", "0"]);
|
||||
});
|
||||
|
||||
test("supports null/undefined children", function() {
|
||||
const vnode1 = h("i", [0, 1, 2, 3, 4, 5].map(spanNum));
|
||||
const vnode2 = h(
|
||||
"i",
|
||||
[null, 2, undefined, null, 1, 0, null, 5, 4, null, 3, undefined].map(
|
||||
spanNum
|
||||
)
|
||||
[null, 2, undefined, null, 1, 0, null, 5, 4, null, 3, undefined].map(spanNum)
|
||||
);
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
expect(elm.children.length).toBe(6);
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual([
|
||||
"2",
|
||||
"1",
|
||||
"0",
|
||||
"5",
|
||||
"4",
|
||||
"3"
|
||||
]);
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual(["2", "1", "0", "5", "4", "3"]);
|
||||
});
|
||||
|
||||
test("supports all null/undefined children", function() {
|
||||
@@ -627,14 +585,7 @@ describe("snabbdom", function() {
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
expect(elm.children.length).toBe(0);
|
||||
elm = patch(vnode2, vnode3).elm;
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual([
|
||||
"5",
|
||||
"4",
|
||||
"3",
|
||||
"2",
|
||||
"1",
|
||||
"0"
|
||||
]);
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual(["5", "4", "3", "2", "1", "0"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -714,18 +665,10 @@ describe("snabbdom", function() {
|
||||
});
|
||||
|
||||
test("removes elements", function() {
|
||||
const vnode1 = h("div", [
|
||||
h("span", "One"),
|
||||
h("span", "Two"),
|
||||
h("span", "Three")
|
||||
]);
|
||||
const vnode1 = h("div", [h("span", "One"), h("span", "Two"), h("span", "Three")]);
|
||||
const vnode2 = h("div", [h("span", "One"), h("span", "Three")]);
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual([
|
||||
"One",
|
||||
"Two",
|
||||
"Three"
|
||||
]);
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual(["One", "Two", "Three"]);
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual(["One", "Three"]);
|
||||
});
|
||||
@@ -760,49 +703,19 @@ describe("snabbdom", function() {
|
||||
});
|
||||
|
||||
test("reorders elements", function() {
|
||||
const vnode1 = h("div", [
|
||||
h("span", "One"),
|
||||
h("div", "Two"),
|
||||
h("b", "Three")
|
||||
]);
|
||||
const vnode2 = h("div", [
|
||||
h("b", "Three"),
|
||||
h("span", "One"),
|
||||
h("div", "Two")
|
||||
]);
|
||||
const vnode1 = h("div", [h("span", "One"), h("div", "Two"), h("b", "Three")]);
|
||||
const vnode2 = h("div", [h("b", "Three"), h("span", "One"), h("div", "Two")]);
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual([
|
||||
"One",
|
||||
"Two",
|
||||
"Three"
|
||||
]);
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual(["One", "Two", "Three"]);
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
expect(map(elm.children, c => c.tagName)).toEqual(["B", "SPAN", "DIV"]);
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual([
|
||||
"Three",
|
||||
"One",
|
||||
"Two"
|
||||
]);
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual(["Three", "One", "Two"]);
|
||||
});
|
||||
|
||||
test("supports null/undefined children", function() {
|
||||
const vnode1 = h("i", [null, h("i", "1"), h("i", "2"), null]);
|
||||
const vnode2 = h("i", [
|
||||
h("i", "2"),
|
||||
undefined,
|
||||
undefined,
|
||||
h("i", "1"),
|
||||
undefined
|
||||
]);
|
||||
const vnode3 = h("i", [
|
||||
null,
|
||||
h("i", "1"),
|
||||
undefined,
|
||||
null,
|
||||
h("i", "2"),
|
||||
undefined,
|
||||
null
|
||||
]);
|
||||
const vnode2 = h("i", [h("i", "2"), undefined, undefined, h("i", "1"), undefined]);
|
||||
const vnode3 = h("i", [null, h("i", "1"), undefined, null, h("i", "2"), undefined, null]);
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual(["1", "2"]);
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
@@ -835,10 +748,7 @@ describe("snabbdom", function() {
|
||||
}
|
||||
const vnode1 = h("div", [
|
||||
h("span", "First sibling"),
|
||||
h("div", { hook: { create: cb } }, [
|
||||
h("span", "Child 1"),
|
||||
h("span", "Child 2")
|
||||
]),
|
||||
h("div", { hook: { create: cb } }, [h("span", "Child 1"), h("span", "Child 2")]),
|
||||
h("span", "Can't touch me")
|
||||
]);
|
||||
patch(vnode0, vnode1);
|
||||
@@ -855,10 +765,7 @@ describe("snabbdom", function() {
|
||||
}
|
||||
const vnode1 = h("div", [
|
||||
h("span", "First sibling"),
|
||||
h("div", { hook: { insert: cb } }, [
|
||||
h("span", "Child 1"),
|
||||
h("span", "Child 2")
|
||||
]),
|
||||
h("div", { hook: { insert: cb } }, [h("span", "Child 1"), h("span", "Child 2")]),
|
||||
h("span", "Can touch me")
|
||||
]);
|
||||
patch(vnode0, vnode1);
|
||||
@@ -874,17 +781,11 @@ describe("snabbdom", function() {
|
||||
}
|
||||
const vnode1 = h("div", [
|
||||
h("span", "First sibling"),
|
||||
h("div", { hook: { prepatch: cb } }, [
|
||||
h("span", "Child 1"),
|
||||
h("span", "Child 2")
|
||||
])
|
||||
h("div", { hook: { prepatch: cb } }, [h("span", "Child 1"), h("span", "Child 2")])
|
||||
]);
|
||||
const vnode2 = h("div", [
|
||||
h("span", "First sibling"),
|
||||
h("div", { hook: { prepatch: cb } }, [
|
||||
h("span", "Child 1"),
|
||||
h("span", "Child 2")
|
||||
])
|
||||
h("div", { hook: { prepatch: cb } }, [h("span", "Child 1"), h("span", "Child 2")])
|
||||
]);
|
||||
patch(vnode0, vnode1);
|
||||
patch(vnode1, vnode2);
|
||||
@@ -965,10 +866,7 @@ describe("snabbdom", function() {
|
||||
}
|
||||
const vnode1 = h("div", [
|
||||
h("span", "First sibling"),
|
||||
h("div", { hook: { remove: cb } }, [
|
||||
h("span", "Child 1"),
|
||||
h("span", "Child 2")
|
||||
])
|
||||
h("div", { hook: { remove: cb } }, [h("span", "Child 1"), h("span", "Child 2")])
|
||||
]);
|
||||
const vnode2 = h("div", [h("span", "First sibling")]);
|
||||
patch(vnode0, vnode1);
|
||||
@@ -981,9 +879,7 @@ describe("snabbdom", function() {
|
||||
function cb(vnode) {
|
||||
calls++;
|
||||
}
|
||||
const vnode1 = h("div", [
|
||||
h("div", { hook: { destroy: cb } }, [h("span", "Child 1")])
|
||||
]);
|
||||
const vnode1 = h("div", [h("div", { hook: { destroy: cb } }, [h("span", "Child 1")])]);
|
||||
const vnode2 = h("div", "Text node");
|
||||
patch(vnode0, vnode1);
|
||||
patch(vnode1, vnode2);
|
||||
@@ -1053,10 +949,7 @@ describe("snabbdom", function() {
|
||||
result.push(vnode);
|
||||
rm();
|
||||
}
|
||||
const vnode1 = h("div", { hook: { remove: cb } }, [
|
||||
h("b", "Child 1"),
|
||||
h("i", "Child 2")
|
||||
]);
|
||||
const vnode1 = h("div", { hook: { remove: cb } }, [h("b", "Child 1"), h("i", "Child 2")]);
|
||||
const vnode2 = h("span", [h("b", "Child 1"), h("i", "Child 2")]);
|
||||
patch(vnode0, vnode1);
|
||||
patch(vnode1, vnode2);
|
||||
@@ -1091,10 +984,7 @@ describe("snabbdom", function() {
|
||||
}
|
||||
const vnode1 = h("div", [
|
||||
h("span", "First sibling"),
|
||||
h("div", [
|
||||
h("span", { hook: { destroy: cb } }, "Child 1"),
|
||||
h("span", "Child 2")
|
||||
])
|
||||
h("div", [h("span", { hook: { destroy: cb } }, "Child 1"), h("span", "Child 2")])
|
||||
]);
|
||||
const vnode2 = h("div");
|
||||
patch(vnode0, vnode1);
|
||||
@@ -1150,11 +1040,7 @@ describe("snabbdom", function() {
|
||||
}
|
||||
}
|
||||
]);
|
||||
const vnode1 = h("div", [
|
||||
h("span", "First child"),
|
||||
"",
|
||||
h("span", "Third child")
|
||||
]);
|
||||
const vnode1 = h("div", [h("span", "First child"), "", h("span", "Third child")]);
|
||||
const vnode2 = h("div");
|
||||
patch(vnode0, vnode1);
|
||||
patch(vnode1, vnode2);
|
||||
@@ -1196,10 +1082,7 @@ describe("snabbdom", function() {
|
||||
function cb(vnode) {
|
||||
result.push(vnode);
|
||||
}
|
||||
const vnode1 = h("div", [
|
||||
h("span", { hook: { update: cb } }, "Hello"),
|
||||
h("span", "there")
|
||||
]);
|
||||
const vnode1 = h("div", [h("span", { hook: { update: cb } }, "Hello"), h("span", "there")]);
|
||||
patch(vnode0, vnode1);
|
||||
patch(vnode1, vnode1);
|
||||
expect(result).toHaveLength(0);
|
||||
@@ -1222,3 +1105,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>`);
|
||||
});
|
||||
});
|
||||
|
||||
+118
-35
@@ -1,4 +1,9 @@
|
||||
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
|
||||
import {
|
||||
buildData,
|
||||
startMeasure,
|
||||
stopMeasure,
|
||||
formatNumber
|
||||
} from "../shared/utils.js";
|
||||
|
||||
odoo.define("app", function(require) {
|
||||
const Widget = require("web.Widget");
|
||||
@@ -97,14 +102,23 @@ odoo.define("app", function(require) {
|
||||
"click .o_btn_msg.10000": function() {
|
||||
this.addMessages(10000);
|
||||
},
|
||||
"click .o_btn_msg.50000": function() {
|
||||
this.addMessages(50000);
|
||||
"click .o_btn_msg.30000": function() {
|
||||
this.addMessages(30000);
|
||||
},
|
||||
"click .updateSomeMessages": function() {
|
||||
this.updateSomeMessages();
|
||||
},
|
||||
"click .clear": function() {
|
||||
this.clear();
|
||||
},
|
||||
"click .o_multiple": function() {
|
||||
this.multipleFlag = !this.multipleFlag;
|
||||
},
|
||||
"click .o_clear": function() {
|
||||
this.clearFlag = !this.clearFlag;
|
||||
},
|
||||
"click .clear-log": function() {
|
||||
this.$log[0].innerHTML = "";
|
||||
}
|
||||
},
|
||||
custom_events: {
|
||||
@@ -116,11 +130,15 @@ odoo.define("app", function(require) {
|
||||
this.widgets = {};
|
||||
this.isInDom = false;
|
||||
this.messageCount = 0;
|
||||
this.multipleFlag = false;
|
||||
this.clearFlag = false;
|
||||
},
|
||||
|
||||
start: function() {
|
||||
this.$content = this.$(".content");
|
||||
this.$msgCount = this.$(".message_count");
|
||||
this.$log = this.$(".log-content");
|
||||
this.log("Benchmarking odoo widgets, 12.0");
|
||||
},
|
||||
on_attach_callback: function() {
|
||||
this.isInDom = true;
|
||||
@@ -144,34 +162,41 @@ odoo.define("app", function(require) {
|
||||
},
|
||||
|
||||
addMessages: function(n) {
|
||||
var self = this;
|
||||
startMeasure("add " + n);
|
||||
const defs = [];
|
||||
const messages = buildData(n);
|
||||
for (let message of messages) {
|
||||
const widget = new Message(this, message);
|
||||
this.widgets[message.id] = widget;
|
||||
defs.push(widget.appendTo("<div>"));
|
||||
}
|
||||
$.when
|
||||
.apply($, defs)
|
||||
.then(function() {
|
||||
for (let message of messages) {
|
||||
let widget = self.widgets[message.id];
|
||||
dom.append(self.$content, widget.$el, {
|
||||
in_DOM: this.isInDom,
|
||||
callbacks: [{ widget: widget }]
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(function() {
|
||||
self.messageCount += n;
|
||||
self.updateMessageCount();
|
||||
stopMeasure();
|
||||
});
|
||||
const self = this;
|
||||
this.benchmark("add " + n, () => {
|
||||
const defs = [];
|
||||
const messages = buildData(n);
|
||||
for (let message of messages) {
|
||||
const widget = new Message(this, message);
|
||||
this.widgets[message.id] = widget;
|
||||
defs.push(widget.appendTo("<div>"));
|
||||
}
|
||||
return $.when
|
||||
.apply($, defs)
|
||||
.then(function() {
|
||||
for (let message of messages) {
|
||||
let widget = self.widgets[message.id];
|
||||
dom.append(self.$content, widget.$el, {
|
||||
in_DOM: this.isInDom,
|
||||
callbacks: [{ widget: widget }]
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(function() {
|
||||
self.messageCount += n;
|
||||
self.updateMessageCount();
|
||||
});
|
||||
});
|
||||
},
|
||||
clear: function() {
|
||||
startMeasure("clear");
|
||||
this._clear();
|
||||
stopMeasure(info => {
|
||||
this.log(info.msg);
|
||||
});
|
||||
},
|
||||
|
||||
_clear: function() {
|
||||
this.$content.empty();
|
||||
for (let key in this.widgets) {
|
||||
this.widgets[key].destroy();
|
||||
@@ -179,15 +204,14 @@ odoo.define("app", function(require) {
|
||||
}
|
||||
this.messageCount = 0;
|
||||
this.updateMessageCount();
|
||||
stopMeasure();
|
||||
},
|
||||
updateSomeMessages: function() {
|
||||
startMeasure("update every 10th");
|
||||
const widgets = Object.values(this.widgets);
|
||||
for (let i = 0; i < widgets.length; i += 10) {
|
||||
widgets[i].update();
|
||||
}
|
||||
stopMeasure();
|
||||
this.benchmark("update every 10th", () => {
|
||||
const widgets = Object.values(this.widgets);
|
||||
for (let i = 0; i < widgets.length; i += 10) {
|
||||
widgets[i].update();
|
||||
}
|
||||
});
|
||||
},
|
||||
_onRemoveMessage: function(ev) {
|
||||
startMeasure("remove message");
|
||||
@@ -196,6 +220,65 @@ odoo.define("app", function(require) {
|
||||
this.messageCount--;
|
||||
this.updateMessageCount();
|
||||
stopMeasure();
|
||||
},
|
||||
|
||||
benchmark: function(message, fn, callback) {
|
||||
if (this.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.clearFlag) {
|
||||
this._benchmark("clear", this._clear.bind(this), finalize, false);
|
||||
} else {
|
||||
finalize();
|
||||
}
|
||||
};
|
||||
this._benchmark(message, fn, cb);
|
||||
} else {
|
||||
this._benchmark(message, fn, callback);
|
||||
}
|
||||
},
|
||||
|
||||
_benchmark: function(message, fn, cb, log = true) {
|
||||
setTimeout(() => {
|
||||
startMeasure(message);
|
||||
const benchmark = fn();
|
||||
(benchmark && benchmark.then ? benchmark : $.when()).then(() => {
|
||||
stopMeasure(info => {
|
||||
if (log) {
|
||||
this.log(info.msg);
|
||||
}
|
||||
if (cb) {
|
||||
cb(info);
|
||||
}
|
||||
});
|
||||
}, 10);
|
||||
});
|
||||
},
|
||||
|
||||
log: function(str, isBold) {
|
||||
const div = document.createElement("div");
|
||||
if (isBold) {
|
||||
div.classList.add("bold");
|
||||
}
|
||||
div.textContent = `> ${str}`;
|
||||
this.$log[0].appendChild(div);
|
||||
this.$log[0].scrollTop = this.$log[0].scrollHeight;
|
||||
}
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user