mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 46095b7a96 | |||
| c3453d35b9 | |||
| b0dd0252a4 | |||
| e2b2e54e29 | |||
| 766c378ac9 | |||
| 022b29b6a0 | |||
| 367725d194 | |||
| e82ed7dc8c | |||
| 63c3d4f6b0 | |||
| b859fcb8f0 | |||
| 3005756d89 | |||
| f6820bb8ac | |||
| 090880f478 | |||
| 5cdaa7b473 | |||
| 91b9e78182 | |||
| 5335fb8fba | |||
| 4513e3f31c | |||
| ecf12f6eee | |||
| 82a6961b3a | |||
| 78a4550ebb | |||
| cc82b3ffcd | |||
| bbbaf95c8e | |||
| caa63628dc | |||
| 7a49b9d94e | |||
| 01eb338e69 | |||
| 3048c6f961 | |||
| 8edc637033 | |||
| c9910077a4 | |||
| 6bf70f016c | |||
| 905619f80f |
@@ -0,0 +1,14 @@
|
|||||||
|
|
||||||
|
Most of the files are
|
||||||
|
|
||||||
|
Copyright (c) 2004-2015 Odoo S.A.
|
||||||
|
|
||||||
|
Many files also contain contributions from third
|
||||||
|
parties. In this case the original copyright of
|
||||||
|
the contributions can be traced through the
|
||||||
|
history of the source version control system.
|
||||||
|
|
||||||
|
When that is not the case, the files contain a prominent
|
||||||
|
notice stating the original copyright and applicable
|
||||||
|
license, or come with their own dedicated COPYRIGHT
|
||||||
|
and/or LICENSE file.
|
||||||
@@ -1,26 +1,32 @@
|
|||||||
<h1 align="center">🦉 <a href="https://odoo.github.io/owl/">Odoo Web Library</a> 🦉</h1>
|
<h1 align="center">🦉 <a href="https://odoo.github.io/owl/">Odoo Web Library</a> 🦉</h1>
|
||||||
|
|
||||||
_A web framework for structured, dynamic and maintainable applications_
|
_A no nonsense web framework for structured, dynamic and maintainable applications_
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
The Odoo Web Library (OWL) is a small (~16kb gzipped) UI framework intended to be the basis for
|
The Odoo Web Library (OWL) is a smallish (~17kb gzipped) UI framework intended to
|
||||||
the [Odoo](https://www.odoo.com/) Web Client, and hopefully many other Odoo
|
be the basis for the [Odoo](https://www.odoo.com/) Web Client. Owl is a modern
|
||||||
related projects. OWL's main features are:
|
framework, written in Typescript, taking the best ideas from React and Vue in a
|
||||||
|
simple and consistent way. Owl's main features are:
|
||||||
|
|
||||||
- a _declarative component system_, (template based, with asynchronous rendering and a virtual dom)
|
- a declarative component system,
|
||||||
- a store implementation (for state management)
|
- a reactivity system based on hooks,
|
||||||
|
- a store implementation (for state management),
|
||||||
- a small frontend router
|
- a small frontend router
|
||||||
|
|
||||||
**Try it online!** An online playground is available at [https://odoo.github.io/owl/playground](https://odoo.github.io/owl/playground) to let you experiment with the OWL framework.
|
Owl components are defined with ES6 classes, they use QWeb templates, an underlying
|
||||||
|
virtual dom, integrates beautifully with hooks, and the rendering is asynchronous.
|
||||||
|
|
||||||
|
**Try it online!** An online playground is available at [https://odoo.github.io/owl/playground](https://odoo.github.io/owl/playground) to let you experiment with the Owl framework. There
|
||||||
|
are some code examples to showcase some interesting features.
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
Here is a short example to illustrate interactive components:
|
Here is a short example to illustrate interactive components:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
import { Component, QWeb } from 'owl'
|
import { Component, QWeb, useState } from "owl";
|
||||||
import { xml } from 'owl/tags'
|
import { xml } from "owl/tags";
|
||||||
|
|
||||||
class Counter extends Component {
|
class Counter extends Component {
|
||||||
static template = xml`
|
static template = xml`
|
||||||
@@ -28,7 +34,7 @@ class Counter extends Component {
|
|||||||
Click Me! [<t t-esc="state.value"/>]
|
Click Me! [<t t-esc="state.value"/>]
|
||||||
</button>`;
|
</button>`;
|
||||||
|
|
||||||
state = { value: 0 };
|
state = useState({ value: 0 });
|
||||||
|
|
||||||
increment() {
|
increment() {
|
||||||
this.state.value++;
|
this.state.value++;
|
||||||
@@ -43,17 +49,20 @@ class App extends Component {
|
|||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
static components = { Counter };
|
static components = { Counter };
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const app = new App({ qweb: new QWeb() });
|
const app = new App({ qweb: new QWeb() });
|
||||||
app.mount(document.body);
|
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
|
More interesting examples can be found on the
|
||||||
[playground](https://odoo.github.io/owl/playground) application.
|
[playground](https://odoo.github.io/owl/playground) application.
|
||||||
|
|
||||||
## OWL's Design Principles
|
## Design Principles
|
||||||
|
|
||||||
OWL is designed to be used in highly dynamic applications where changing
|
OWL is designed to be used in highly dynamic applications where changing
|
||||||
requirements are common and code needs to be maintained by large teams.
|
requirements are common and code needs to be maintained by large teams.
|
||||||
@@ -82,7 +91,7 @@ The complete documentation can be found [here](doc/readme.md). The most importan
|
|||||||
|
|
||||||
- [Quick Start](doc/quick_start.md)
|
- [Quick Start](doc/quick_start.md)
|
||||||
- [Component](doc/component.md)
|
- [Component](doc/component.md)
|
||||||
- [QWeb](doc/qweb.md)
|
- [Hooks](doc/hooks.md)
|
||||||
|
|
||||||
Found an issue in the documentation? A broken link? Some outdated information?
|
Found an issue in the documentation? A broken link? Some outdated information?
|
||||||
Submit a PR!
|
Submit a PR!
|
||||||
@@ -91,20 +100,159 @@ Submit a PR!
|
|||||||
|
|
||||||
If you want to use a simple `<script>` tag, the last release can be downloaded here:
|
If you want to use a simple `<script>` tag, the last release can be downloaded here:
|
||||||
|
|
||||||
- [owl-0.21.0.js](https://github.com/odoo/owl/releases/download/v0.21.0/owl.js)
|
- [owl-0.22.0.js](https://github.com/odoo/owl/releases/download/v0.22.0/owl.js)
|
||||||
- [owl-0.21.0.min.js](https://github.com/odoo/owl/releases/download/v0.21.0/owl.min.js)
|
- [owl-0.22.0.min.js](https://github.com/odoo/owl/releases/download/v0.22.0/owl.min.js)
|
||||||
|
|
||||||
Some npm scripts are available:
|
Some npm scripts are available:
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
| --------------------- | ---------------------------------------------------------------------------- |
|
| ---------------- | -------------------------------------------------- |
|
||||||
| `npm install` | install every dependency required for this project |
|
| `npm install` | install every dependency required for this project |
|
||||||
| `npm run build` | build a bundle of _owl_ in the _/dist/_ folder |
|
| `npm run build` | build a bundle of _owl_ in the _/dist/_ folder |
|
||||||
| `npm run minify` | minify the prebuilt owl.js file |
|
| `npm run minify` | minify the prebuilt owl.js file |
|
||||||
| `npm run test` | run all tests |
|
| `npm run test` | run all (owl) tests |
|
||||||
| `npm run test:watch` | run all tests, and keep a watcher |
|
|
||||||
| `npm run tools` | build tools applications, start a static server (see [here](doc/tooling.md)) |
|
## Quick Overview
|
||||||
| `npm run tools:watch` | same as `tools`, but with a watcher to rebuild owl |
|
|
||||||
|
Owl components in an application are used to define a (dynamic) tree of components.
|
||||||
|
|
||||||
|
```
|
||||||
|
Root
|
||||||
|
/ \
|
||||||
|
A B
|
||||||
|
/ \
|
||||||
|
C D
|
||||||
|
```
|
||||||
|
|
||||||
|
**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
|
## License
|
||||||
|
|
||||||
|
|||||||
+193
-89
@@ -5,6 +5,7 @@
|
|||||||
- [Overview](#overview)
|
- [Overview](#overview)
|
||||||
- [Example](#example)
|
- [Example](#example)
|
||||||
- [Reference](#reference)
|
- [Reference](#reference)
|
||||||
|
- [Reactive System](#reactive-system)
|
||||||
- [Properties](#properties)
|
- [Properties](#properties)
|
||||||
- [Static Properties](#static-properties)
|
- [Static Properties](#static-properties)
|
||||||
- [Methods](#methods)
|
- [Methods](#methods)
|
||||||
@@ -15,13 +16,14 @@
|
|||||||
- [Event Handling](#event-handling)
|
- [Event Handling](#event-handling)
|
||||||
- [Form Input Bindings](#form-input-bindings)
|
- [Form Input Bindings](#form-input-bindings)
|
||||||
- [`t-key` Directive](#t-key-directive)
|
- [`t-key` Directive](#t-key-directive)
|
||||||
- [`t-mounted` Directive](#t-mounted-directive)
|
|
||||||
- [Semantics](#semantics)
|
- [Semantics](#semantics)
|
||||||
- [Props Validation](#props-validation)
|
- [Props Validation](#props-validation)
|
||||||
- [References](#references)
|
- [References](#references)
|
||||||
- [Slots](#slots)
|
- [Slots](#slots)
|
||||||
- [Asynchronous Rendering](#asynchronous-rendering)
|
- [Asynchronous Rendering](#asynchronous-rendering)
|
||||||
- [Error Handling](#error-handling)
|
- [Error Handling](#error-handling)
|
||||||
|
- [Functional Components](#functional-components)
|
||||||
|
- [SVG components](#svg-components)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -45,16 +47,15 @@ exclusively done by a [QWeb](qweb.md) template (which needs to be preloaded in Q
|
|||||||
Rendering a component generates a virtual dom representation
|
Rendering a component generates a virtual dom representation
|
||||||
of the component, which is then patched to the DOM, in order to apply the changes in an efficient way.
|
of the component, which is then patched to the DOM, in order to apply the changes in an efficient way.
|
||||||
|
|
||||||
OWL components observe their states, and rerender themselves whenever it is
|
|
||||||
changed. This is done by an [observer](observer.md).
|
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
Let us have a look at a simple component:
|
Let us have a look at a simple component:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
|
const { useState } = owl.hooks;
|
||||||
|
|
||||||
class ClickCounter extends owl.Component {
|
class ClickCounter extends owl.Component {
|
||||||
state = { value: 0 };
|
state = useState({ value: 0 });
|
||||||
|
|
||||||
increment() {
|
increment() {
|
||||||
this.state.value++;
|
this.state.value++;
|
||||||
@@ -74,9 +75,8 @@ latest browsers without a transpilation step.
|
|||||||
This example show how a component should be defined: it simply subclasses the
|
This example show how a component should be defined: it simply subclasses the
|
||||||
Component class. If no static `template` key is defined, then
|
Component class. If no static `template` key is defined, then
|
||||||
Owl will use the component's name as template name. Here,
|
Owl will use the component's name as template name. Here,
|
||||||
a state object is defined. It is not mandatory to use the state object, but it
|
a state object is defined, by using the `useState` hook. It is not mandatory to use the state object, but it is certainly encouraged. The result of the `useState` call is
|
||||||
is certainly encouraged. The state object is [observed](observer.md), and any
|
[observed](observer.md), and any change to it will cause a rerendering.
|
||||||
change to it will cause a rerendering.
|
|
||||||
|
|
||||||
## Reference
|
## Reference
|
||||||
|
|
||||||
@@ -89,6 +89,25 @@ Be aware that the name of the component may be significant: if a component does
|
|||||||
not define a `template` key, then Owl will lookup in QWeb to
|
not define a `template` key, then Owl will lookup in QWeb to
|
||||||
find a template with the component name (or one of its ancestor).
|
find a template with the component name (or one of its ancestor).
|
||||||
|
|
||||||
|
### Reactive system
|
||||||
|
|
||||||
|
OWL components can be made reactive by observing some part of their state. See the [hooks](hooks.md) section for more details.
|
||||||
|
|
||||||
|
The main idea is that the `useState` hook generate a proxy version of an object
|
||||||
|
(this is done by an [observer](observer.md)), which allows the component to
|
||||||
|
react to any change.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { useState } = owl.hooks;
|
||||||
|
|
||||||
|
class SomeComponent extends owl.Component {
|
||||||
|
state = useState({ a: 0, b: 1 });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that there is an important limitation: hooks need to be called in the
|
||||||
|
constructor.
|
||||||
|
|
||||||
### Properties
|
### Properties
|
||||||
|
|
||||||
- **`el`** (HTMLElement | null): reference to the DOM root node of the element. It is `null` when the
|
- **`el`** (HTMLElement | null): reference to the DOM root node of the element. It is `null` when the
|
||||||
@@ -96,18 +115,11 @@ find a template with the component name (or one of its ancestor).
|
|||||||
|
|
||||||
- **`env`** (Object): the component environment, which contains a QWeb instance.
|
- **`env`** (Object): the component environment, which contains a QWeb instance.
|
||||||
|
|
||||||
- **`state`** (Object): this is the location of the component's state, if there is
|
|
||||||
any. After the willStart method, the `state` property is observed, and each
|
|
||||||
change will cause the component to rerender itself.
|
|
||||||
|
|
||||||
- **`props`** (Object): this is an object given (in the constructor) by the parent
|
- **`props`** (Object): this is an object given (in the constructor) by the parent
|
||||||
to configure the component. It can be dynamically changed later by the parent,
|
to configure the component. It can be dynamically changed later by the parent,
|
||||||
in some case. Note that `props` are owned by the parent, not by the component.
|
in some case. Note that `props` are owned by the parent, not by the component.
|
||||||
As such, it should not ever be modified by the component!!
|
As such, it should not ever be modified by the component!!
|
||||||
|
|
||||||
- **`refs`** (Object): the `refs` object contains all references to sub DOM nodes
|
|
||||||
or sub components defined by a `t-ref` directive in the component's template.
|
|
||||||
|
|
||||||
### Static Properties
|
### Static Properties
|
||||||
|
|
||||||
- **`template`** (string, optional): if given, this is the name of the QWeb template that will render the component. Note that there is a helper `xml` to
|
- **`template`** (string, optional): if given, this is the name of the QWeb template that will render the component. Note that there is a helper `xml` to
|
||||||
@@ -236,7 +248,7 @@ component.
|
|||||||
```javascript
|
```javascript
|
||||||
constructor(parent, props) {
|
constructor(parent, props) {
|
||||||
super(parent, props);
|
super(parent, props);
|
||||||
this.state = {someValue: true};
|
this.state = useState({someValue: true});
|
||||||
this.template = 'mytemplate';
|
this.template = 'mytemplate';
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -246,7 +258,7 @@ implemented in most cases:
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
class ClickCounter extends owl.Component {
|
class ClickCounter extends owl.Component {
|
||||||
state = { value: 0 };
|
state = useState({ value: 0 });
|
||||||
|
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
@@ -271,9 +283,6 @@ At this point, the component is not yet rendered. Note that a slow `willStart` m
|
|||||||
interface. Therefore, some care should be made to make this method as
|
interface. Therefore, some care should be made to make this method as
|
||||||
fast as possible.
|
fast as possible.
|
||||||
|
|
||||||
After the `willStart` method is completed, the state will be observed with a
|
|
||||||
new `Observer`. Then, the component will be rendered by `QWeb`.
|
|
||||||
|
|
||||||
#### `mounted()`
|
#### `mounted()`
|
||||||
|
|
||||||
`mounted` is called each time a component is attached to the
|
`mounted` is called each time a component is attached to the
|
||||||
@@ -287,10 +296,9 @@ always be unmounted at some point in the future.
|
|||||||
The mounted method will be called recursively on each of its children. First,
|
The mounted method will be called recursively on each of its children. First,
|
||||||
the parent, then all its children.
|
the parent, then all its children.
|
||||||
|
|
||||||
Note that the state is now observed. It is however allowed (but not encouraged)
|
It is allowed (but not encouraged) to modify the state in the `mounted` hook.
|
||||||
to modify the state in the `mounted` hook. Doing so will cause a rerender,
|
Doing so will cause a rerender, which will not be perceptible by the user, but
|
||||||
which will not be perceptible by the user, but will slightly slow down the
|
will slightly slow down the component.
|
||||||
component.
|
|
||||||
|
|
||||||
#### `willUpdateProps(nextProps)`
|
#### `willUpdateProps(nextProps)`
|
||||||
|
|
||||||
@@ -315,14 +323,11 @@ It is not called on the initial render. This is useful to read some
|
|||||||
information from the DOM. For example, the current position of the
|
information from the DOM. For example, the current position of the
|
||||||
scrollbar.
|
scrollbar.
|
||||||
|
|
||||||
Note that modifying the state object is not allowed here. This method is called just
|
Note that modifying the state is not allowed here. This method is called just
|
||||||
before an actual DOM patch, and is only intended to be used to save some local
|
before an actual DOM patch, and is only intended to be used to save some local
|
||||||
DOM state. Also, it will not be called if the component is not in the DOM (this can
|
DOM state. Also, it will not be called if the component is not in the DOM (this can
|
||||||
happen with components with `t-keepalive`).
|
happen with components with `t-keepalive`).
|
||||||
|
|
||||||
The return value of this method will be given as the first argument of the
|
|
||||||
corresponding `patched` call.
|
|
||||||
|
|
||||||
#### `patched(snapshot)`
|
#### `patched(snapshot)`
|
||||||
|
|
||||||
This hook is called whenever a component did actually update its DOM (most
|
This hook is called whenever a component did actually update its DOM (most
|
||||||
@@ -333,8 +338,6 @@ with the DOM (for example, through an external library) whenever the
|
|||||||
component was patched. Note that this hook will not be called if the compoent is
|
component was patched. Note that this hook will not be called if the compoent is
|
||||||
not in the DOM (this can happen with components with `t-keepalive`).
|
not in the DOM (this can happen with components with `t-keepalive`).
|
||||||
|
|
||||||
The `snapshot` parameter is the result of the previous `willPatch` call.
|
|
||||||
|
|
||||||
Updating the compoent state in this hook is possible, but not encouraged.
|
Updating the compoent state in this hook is possible, but not encouraged.
|
||||||
One need to be careful, because updates here will cause rerender, which in
|
One need to be careful, because updates here will cause rerender, which in
|
||||||
turn will cause other calls to patched. So, we need to be particularly
|
turn will cause other calls to patched. So, we need to be particularly
|
||||||
@@ -397,9 +400,9 @@ easy to test a component.
|
|||||||
Updating the environment is not as simple as changing a component's state: its
|
Updating the environment is not as simple as changing a component's state: its
|
||||||
content is not observed, so updates will not be reflected immediately in the
|
content is not observed, so updates will not be reflected immediately in the
|
||||||
user interface. There is however a mechanism to force root widgets to rerender
|
user interface. There is however a mechanism to force root widgets to rerender
|
||||||
themselves whenever the environment is modified: one only needs to trigger the
|
themselves whenever the environment is modified: one only needs to call the
|
||||||
`update` event on the QWeb instance. For example, a responsive environment
|
`forceUpdate` method on the QWeb instance. For example, a responsive environment
|
||||||
could be programmed like this:
|
could be done like this:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
function setupResponsivePlugin(env) {
|
function setupResponsivePlugin(env) {
|
||||||
@@ -408,7 +411,7 @@ function setupResponsivePlugin(env) {
|
|||||||
const updateEnv = owl.utils.debounce(() => {
|
const updateEnv = owl.utils.debounce(() => {
|
||||||
if (env.isMobile !== isMobile()) {
|
if (env.isMobile !== isMobile()) {
|
||||||
env.isMobile = !env.isMobile;
|
env.isMobile = !env.isMobile;
|
||||||
env.qweb.trigger("update");
|
env.qweb.forceUpdate();
|
||||||
}
|
}
|
||||||
}, 15);
|
}, 15);
|
||||||
window.addEventListener("resize", updateEnv);
|
window.addEventListener("resize", updateEnv);
|
||||||
@@ -442,7 +445,8 @@ dynamic. If it is necessary to give a string, this can be done by quoting it:
|
|||||||
`someString="'somevalue'"`.
|
`someString="'somevalue'"`.
|
||||||
|
|
||||||
Note that the rendering context for the template is the component itself. This means
|
Note that the rendering context for the template is the component itself. This means
|
||||||
that the template can access `state`, `props`, `env`, or any methods defined in the component.
|
that the template can access `state` (if it exists), `props`, `env`, or any
|
||||||
|
methods defined in the component.
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<div t-name="ParentComponent">
|
<div t-name="ParentComponent">
|
||||||
@@ -453,7 +457,7 @@ that the template can access `state`, `props`, `env`, or any methods defined in
|
|||||||
```js
|
```js
|
||||||
class ParentComponent {
|
class ParentComponent {
|
||||||
static components = { ChildComponent };
|
static components = { ChildComponent };
|
||||||
state = { val: 4 };
|
state = useState({ val: 4 });
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -502,6 +506,31 @@ class ParentComponent {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
There is an even more dynamic way to use `t-component`: its value can be an
|
||||||
|
expression evaluating to an actual component class. In that case, this is the
|
||||||
|
class that will be used to create the component:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class A extends Component<any, any, any> {
|
||||||
|
static template = xml`<span>child a</span>`;
|
||||||
|
}
|
||||||
|
class B extends Component<any, any, any> {
|
||||||
|
static template = xml`<span>child b</span>`;
|
||||||
|
}
|
||||||
|
class App extends Component<any, any, any> {
|
||||||
|
static template = xml`<t t-component="myComponent" t-key="state.child"/>`;
|
||||||
|
|
||||||
|
state = { child: "a" };
|
||||||
|
|
||||||
|
get myComponent() {
|
||||||
|
return this.state.child === "a" ? A : B;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example, the component `App` selects dynamically the concrete sub
|
||||||
|
component class.
|
||||||
|
|
||||||
**CSS and style:** there is some specific support to allow the parent to declare
|
**CSS and style:** there is some specific support to allow the parent to declare
|
||||||
additional css classes or style for the sub component: css declared in `class`, `style`, `t-att-class` or `t-att-style` will be added to the
|
additional css classes or style for the sub component: css declared in `class`, `style`, `t-att-class` or `t-att-style` will be added to the
|
||||||
root component element.
|
root component element.
|
||||||
@@ -617,7 +646,7 @@ form!). A possible way to do this is to do it by hand:
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
class Form extends owl.Component {
|
class Form extends owl.Component {
|
||||||
state = { text: "" };
|
state = useState({ text: "" });
|
||||||
|
|
||||||
_updateInputValue(event) {
|
_updateInputValue(event) {
|
||||||
this.state.text = event.target.value;
|
this.state.text = event.target.value;
|
||||||
@@ -637,8 +666,9 @@ plumbing code is slightly different if you need to interact with a checkbox,
|
|||||||
or with radio buttons, or with select tags.
|
or with radio buttons, or with select tags.
|
||||||
|
|
||||||
To help with this situation, Owl has a builtin directive `t-model`: its value
|
To help with this situation, Owl has a builtin directive `t-model`: its value
|
||||||
is the (top-level) name in the state object. With the `t-model` directive, we
|
should be an observed value in the component (usually `state.someValue`). With
|
||||||
can write a shorter code, equivalent to the previous example:
|
the `t-model` directive, we can write a shorter code, equivalent to the previous
|
||||||
|
example:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
class Form extends owl.Component {
|
class Form extends owl.Component {
|
||||||
@@ -648,7 +678,7 @@ class Form extends owl.Component {
|
|||||||
|
|
||||||
```xml
|
```xml
|
||||||
<div>
|
<div>
|
||||||
<input t-model="text" />
|
<input t-model="state.text" />
|
||||||
<span t-esc="state.text" />
|
<span t-esc="state.text" />
|
||||||
</div>
|
</div>
|
||||||
```
|
```
|
||||||
@@ -658,11 +688,11 @@ The `t-model` directive works with `<input>`, `<input type="checkbox">`,
|
|||||||
|
|
||||||
```xml
|
```xml
|
||||||
<div>
|
<div>
|
||||||
<div>Text in an input: <input t-model="someVal"/></div>
|
<div>Text in an input: <input t-model="state.someVal"/></div>
|
||||||
<div>Textarea: <textarea t-model="otherVal"/></div>
|
<div>Textarea: <textarea t-model="state.otherVal"/></div>
|
||||||
<div>Boolean value: <input type="checkbox" t-model="someFlag"/></div>
|
<div>Boolean value: <input type="checkbox" t-model="state.someFlag"/></div>
|
||||||
<div>Selection:
|
<div>Selection:
|
||||||
<select t-model="color">
|
<select t-model="state.color">
|
||||||
<option value="">Select a color</option>
|
<option value="">Select a color</option>
|
||||||
<option value="red">Red</option>
|
<option value="red">Red</option>
|
||||||
<option value="blue">Blue</option>
|
<option value="blue">Blue</option>
|
||||||
@@ -671,11 +701,11 @@ The `t-model` directive works with `<input>`, `<input type="checkbox">`,
|
|||||||
<div>
|
<div>
|
||||||
Selection with radio buttons:
|
Selection with radio buttons:
|
||||||
<span>
|
<span>
|
||||||
<input type="radio" name="color" id="red" value="red" t-model="color"/>
|
<input type="radio" name="color" id="red" value="red" t-model="state.color"/>
|
||||||
<label for="red">Red</label>
|
<label for="red">Red</label>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<input type="radio" name="color" id="blue" value="blue" t-model="color" />
|
<input type="radio" name="color" id="blue" value="blue" t-model="state.color" />
|
||||||
<label for="blue">Blue</label>
|
<label for="blue">Blue</label>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -693,7 +723,7 @@ Like event handling, the `t-model` directive accepts some modifiers:
|
|||||||
For example:
|
For example:
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<input t-model.lazy="someVal" />
|
<input t-model.lazy="state.someVal" />
|
||||||
```
|
```
|
||||||
|
|
||||||
These modifiers can be combined. For instance, `t-model.lazy.number` will only
|
These modifiers can be combined. For instance, `t-model.lazy.number` will only
|
||||||
@@ -725,24 +755,6 @@ There are three main use cases:
|
|||||||
- _animations_: give a different identity to a component. Ex: thread id with
|
- _animations_: give a different identity to a component. Ex: thread id with
|
||||||
animations on add/remove message.
|
animations on add/remove message.
|
||||||
|
|
||||||
### `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 MyComponent extends owl.Component {
|
|
||||||
...
|
|
||||||
focusMe() {
|
|
||||||
this.refs.someInput.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Semantics
|
### Semantics
|
||||||
|
|
||||||
We give here an informal description of the way components are created/updated
|
We give here an informal description of the way components are created/updated
|
||||||
@@ -927,44 +939,60 @@ Examples:
|
|||||||
|
|
||||||
### References
|
### References
|
||||||
|
|
||||||
The `t-ref` directive helps a component keep reference to some inside part of it.
|
The `useRef` hook is useful when we need a way to interact with some inside part
|
||||||
Like the `t-on` directive, it can work either on a DOM node, or on a component:
|
of a component, rendered by Owl. It can work either on a DOM node, or on a component,
|
||||||
|
tagged by the `t-ref` directive. See the [hooks section](hooks.md#useref) for
|
||||||
|
more detail.
|
||||||
|
|
||||||
|
As a short example, here is how we could set the focus on a given input:
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<div>
|
<div>
|
||||||
<div t-ref="someDiv"/>
|
<input t-ref="input"/>
|
||||||
<SubComponent t-ref="someComponent"/>
|
<button t-on-click="focusInput">Click</button>
|
||||||
</div>
|
</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
In this example, the component will be able to access the `div` and the component
|
|
||||||
inside the special `refs` variable:
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
this.refs.someDiv;
|
import { useRef } from "owl/hooks";
|
||||||
this.refs.someComponent;
|
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
inputRef = useRef("input");
|
||||||
|
|
||||||
|
focusInput() {
|
||||||
|
this.inputRef.el.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
This is useful for various usecases: for example, integrating with an external
|
The `useRef` hook can also be used to get a reference to an instance of a sub
|
||||||
library that needs to render itself inside an actual DOM node. Or for calling
|
component rendered by Owl. In that case, we need to access it with the `comp`
|
||||||
some method on a sub component.
|
property instead of `el`:
|
||||||
|
|
||||||
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, if we have
|
|
||||||
`id` set to 44 in the rendering context,
|
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<div t-ref="component_{{id}}"/>
|
<div>
|
||||||
|
<SubComponent t-ref="sub"/>
|
||||||
|
<button t-on-click="doSomething">Click</button>
|
||||||
|
</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
```js
|
```js
|
||||||
this.refs.component_44;
|
import { useRef } from "owl/hooks";
|
||||||
|
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static components = { SubComponent };
|
||||||
|
subRef = useRef("sub");
|
||||||
|
|
||||||
|
doSomething() {
|
||||||
|
this.subRef.comp.doSomeThingElse();
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Note that these two examples uses the suffix `ref` to name the reference. This
|
||||||
|
is not mandatory, but it is a useful convention, so we do not forget to access
|
||||||
|
it with the `el` or `comp` suffix.
|
||||||
|
|
||||||
### Slots
|
### Slots
|
||||||
|
|
||||||
To make generic components, it is useful to be able for a parent component to _inject_
|
To make generic components, it is useful to be able for a parent component to _inject_
|
||||||
@@ -1092,8 +1120,8 @@ For example, here is how we could implement an `ErrorBoundary` component:
|
|||||||
```
|
```
|
||||||
|
|
||||||
```js
|
```js
|
||||||
class ErrorBoundary extends Widget {
|
class ErrorBoundary extends Component {
|
||||||
state = { error: false };
|
state = useState({ error: false });
|
||||||
|
|
||||||
catchError() {
|
catchError() {
|
||||||
this.state.error = true;
|
this.state.error = true;
|
||||||
@@ -1120,3 +1148,79 @@ env.qweb.on("error", null, function(error) {
|
|||||||
// react to the error
|
// react to the error
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Functional Components
|
||||||
|
|
||||||
|
Owl does not exactly have functional components. However, there is an extremely
|
||||||
|
close alternative: calling sub templates.
|
||||||
|
|
||||||
|
A stateless functional component in react is usually some kind of function that
|
||||||
|
maps props to a virtual dom (often with `jsx`). So, basically, almost like a
|
||||||
|
template rendered with `props`. In Owl, this can be done by
|
||||||
|
simply defining a template, that will access the `props` object:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const Welcome = xml`<h1>Hello, {props.name}</h1>`;
|
||||||
|
|
||||||
|
class MyComponent extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<t t-call=${Welcome}/>
|
||||||
|
<div>something</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The way this works is that sub templates are inlined, and have access to the
|
||||||
|
ambient context. They can therefore access `props`, and any other part of the
|
||||||
|
caller component.
|
||||||
|
|
||||||
|
### SVG Components
|
||||||
|
|
||||||
|
Owl components can be used to generate dynamic SVG graphs:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class Node extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<g>
|
||||||
|
<circle t-att-cx="props.x" t-att-cy="props.y" r="4" fill="black"/>
|
||||||
|
<text t-att-x="props.x - 5" t-att-y="props.y + 18"><t t-esc="props.node.label"/></text>
|
||||||
|
<t t-set="childx" t-value="props.x + 100"/>
|
||||||
|
<t t-set="height" t-value="props.height/(props.node.children || []).length"/>
|
||||||
|
<t t-foreach="props.node.children || []" t-as="child">
|
||||||
|
<t t-set="childy" t-value="props.y + child_index*height"/>
|
||||||
|
<line t-att-x1="props.x" t-att-y1="props.y" t-att-x2="childx" t-att-y2="childy" stroke="black" />
|
||||||
|
<Node x="childx" y="childy" node="child" height="height"/>
|
||||||
|
</t>
|
||||||
|
</g>
|
||||||
|
`;
|
||||||
|
static components = { Node };
|
||||||
|
}
|
||||||
|
|
||||||
|
class RootNode extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<svg height="180">
|
||||||
|
<Node node="graph" x="10" y="20" height="180"/>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
static components = { Node };
|
||||||
|
graph = {
|
||||||
|
label: "a",
|
||||||
|
children: [
|
||||||
|
{ label: "b" },
|
||||||
|
{ label: "c", children: [{ label: "d" }, { label: "e" }] },
|
||||||
|
{ label: "f", children: [{ label: "g" }] }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This `RootNode` component will then display a live SVG representation of the
|
||||||
|
graph described by the `graph` property. Note that there is a recursive structure
|
||||||
|
here: the `Node` component uses itself as a subcomponent.
|
||||||
|
|
||||||
|
Note that since SVG needs to be handled in a specific way (its namespace needs
|
||||||
|
to be properly set), there is a small constraint for Owl components: if an owl
|
||||||
|
component is supposed to be a part of an svg graph, then its root node needs to
|
||||||
|
be a `g` tag, so Owl can properly set the namespace.
|
||||||
|
|||||||
+275
@@ -0,0 +1,275 @@
|
|||||||
|
# 🦉 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)
|
||||||
|
- [`useRef`](#useref)
|
||||||
|
- [`useSubEnv`](#useSubEnv)
|
||||||
|
|
||||||
|
## 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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `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.
|
||||||
|
|
||||||
|
### `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.
|
||||||
+3
-33
@@ -11,40 +11,10 @@ For example, this code will display `update` in the console:
|
|||||||
```javascript
|
```javascript
|
||||||
const observer = new owl.Observer();
|
const observer = new owl.Observer();
|
||||||
observer.notifyCB = () => console.log("update");
|
observer.notifyCB = () => console.log("update");
|
||||||
observer.observe(obj);
|
const obj = observer.observe({ a: { b: 1 } });
|
||||||
|
|
||||||
const obj = { a: { b: 1 } };
|
|
||||||
obj.a.b = 2;
|
obj.a.b = 2;
|
||||||
```
|
```
|
||||||
|
|
||||||
## Technical Limitations
|
The observer is implemented with the native `Proxy` object. Note that this
|
||||||
|
means that it will not work on older browsers.
|
||||||
Since the observer uses getters and setters, it is actually unable to react to
|
|
||||||
changes in three situations:
|
|
||||||
|
|
||||||
- adding a key to an object
|
|
||||||
- deleting a key from an object
|
|
||||||
- modifying an array by setting a new value at a given index
|
|
||||||
|
|
||||||
In those situations, we need a way to tell the observer that something happened.
|
|
||||||
This can be done by using the `set` and `delete` (only for objects) static
|
|
||||||
methods of the `Observer`.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const observer = new owl.Observer();
|
|
||||||
const obj = { a: 1 };
|
|
||||||
observer.observe(obj);
|
|
||||||
obj.b = 2; // won't notify the change
|
|
||||||
owl.Observer.set(obj, "b", 2); // will notify the change
|
|
||||||
|
|
||||||
delete obj.b; // won't notify the change
|
|
||||||
owl.Observer.delete(obj, "b"); // will notify the change
|
|
||||||
```
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const observer = new owl.Observer();
|
|
||||||
const arr = ["a"];
|
|
||||||
observer.observe(arr);
|
|
||||||
arr[0] = "b"; // won't notify the change
|
|
||||||
owl.Observer.set(arr, 0, "b"); // will notify the change
|
|
||||||
```
|
|
||||||
|
|||||||
+3
-1
@@ -72,11 +72,13 @@ Here are a few steps that we may take to get started:
|
|||||||
Let us now add the javascript to make it work, in `app.js`:
|
Let us now add the javascript to make it work, in `app.js`:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
|
const useState = owl.hooks.useState;
|
||||||
|
|
||||||
class ClickCounter extends owl.Component {
|
class ClickCounter extends owl.Component {
|
||||||
static template = "clickcounter";
|
static template = "clickcounter";
|
||||||
constructor() {
|
constructor() {
|
||||||
super(...arguments);
|
super(...arguments);
|
||||||
this.state = { value: 0 };
|
this.state = useState({ value: 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
increment() {
|
increment() {
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ needs. Here is a list of all Owl specific directives:
|
|||||||
| `t-key` | [Defining a key (to help virtual dom reconciliation)](component.md#t-key-directive) |
|
| `t-key` | [Defining a key (to help virtual dom reconciliation)](component.md#t-key-directive) |
|
||||||
| `t-on-*` | [Event handling](component.md#event-handling) |
|
| `t-on-*` | [Event handling](component.md#event-handling) |
|
||||||
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
|
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
|
||||||
| `t-mounted` | [Callback when a node or component is mounted](component.md#t-mounted-directive) |
|
|
||||||
| `t-slot` | [Rendering a slot](component.md#slots) |
|
| `t-slot` | [Rendering a slot](component.md#slots) |
|
||||||
| `t-model` | [Form input bindings](component.md#form-input-bindings) |
|
| `t-model` | [Form input bindings](component.md#form-input-bindings) |
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,18 @@ build applications. Here is a complete representation of its content:
|
|||||||
owl
|
owl
|
||||||
Component
|
Component
|
||||||
QWeb
|
QWeb
|
||||||
|
useState
|
||||||
core
|
core
|
||||||
EventBus
|
EventBus
|
||||||
Observer
|
Observer
|
||||||
|
hooks
|
||||||
|
onMounted
|
||||||
|
onWillUnmount
|
||||||
|
onWillPatch
|
||||||
|
onPatched
|
||||||
|
useState
|
||||||
|
useRef
|
||||||
|
useSubEnv
|
||||||
router
|
router
|
||||||
Link
|
Link
|
||||||
RouteComponent
|
RouteComponent
|
||||||
@@ -29,11 +38,14 @@ owl
|
|||||||
whenReady
|
whenReady
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Note that for convenience, the `useState` hook is also exported at the root of the `owl` object.
|
||||||
|
|
||||||
## Reference
|
## Reference
|
||||||
|
|
||||||
- [Animations](animations.md)
|
- [Animations](animations.md)
|
||||||
- [Component](component.md)
|
- [Component](component.md)
|
||||||
- [Event Bus](event_bus.md)
|
- [Event Bus](event_bus.md)
|
||||||
|
- [Hooks](hooks.md)
|
||||||
- [Observer](observer.md)
|
- [Observer](observer.md)
|
||||||
- [QWeb](qweb.md)
|
- [QWeb](qweb.md)
|
||||||
- [Router](router.md)
|
- [Router](router.md)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
- [Development Mode](#development-mode)
|
- [Development Mode](#development-mode)
|
||||||
- [Playground](#playground)
|
- [Playground](#playground)
|
||||||
- [Benchmarks](#benchmarks)
|
- [Benchmarks](#benchmarks)
|
||||||
|
- [Single File Component](#single-file-component)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -56,3 +57,31 @@ Note: This is more an internal tool, useful for people working on Owl.
|
|||||||
The benchmarks application is a very small application, implemented in different
|
The benchmarks application is a very small application, implemented in different
|
||||||
frameworks, and in different versions of Owl. This is a simple internal tool,
|
frameworks, and in different versions of Owl. This is a simple internal tool,
|
||||||
useful to compare various performance metrics on some tasks.
|
useful to compare various performance metrics on some tasks.
|
||||||
|
|
||||||
|
## Single File Component
|
||||||
|
|
||||||
|
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...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "owl-framework",
|
"name": "owl-framework",
|
||||||
"version": "0.21.0",
|
"version": "0.22.0",
|
||||||
"description": "Odoo Web Library (OWL)",
|
"description": "Odoo Web Library (OWL)",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+128
-124
@@ -11,7 +11,8 @@ import "./props_validation";
|
|||||||
* contains:
|
* contains:
|
||||||
*
|
*
|
||||||
* - the Env interface (generic type for the environment)
|
* - the Env interface (generic type for the environment)
|
||||||
* - the Meta interface (the owl specific metadata attached to a component)
|
* - the Fiber interface (owl metadata attached to a rendering)
|
||||||
|
* - the Internal interface (the owl specific metadata attached to a component)
|
||||||
* - the Component class
|
* - the Component class
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -33,6 +34,27 @@ export interface Env {
|
|||||||
[key: string]: any;
|
[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
|
* 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
|
* useful to typecheck and describe the internal keys used by Owl to manage the
|
||||||
@@ -48,26 +70,24 @@ interface Internal<T extends Env, Props> {
|
|||||||
|
|
||||||
// parent and children keys are obviously useful to setup the parent-children
|
// parent and children keys are obviously useful to setup the parent-children
|
||||||
// relationship.
|
// relationship.
|
||||||
parent: Component<T, any, any> | null;
|
parent: Component<T, any> | null;
|
||||||
children: { [key: number]: Component<T, any, any> };
|
children: { [key: number]: Component<T, any> };
|
||||||
// children mapping: from templateID to componentID. templateID identifies a
|
// children mapping: from templateID to componentID. templateID identifies a
|
||||||
// place in a template. The t-component directive needs it to be able to get
|
// place in a template. The t-component directive needs it to be able to get
|
||||||
// the component instance back whenever the template is rerendered.
|
// the component instance back whenever the template is rerendered.
|
||||||
cmap: { [key: number]: number };
|
cmap: { [key: number]: number };
|
||||||
|
|
||||||
renderId: number;
|
currentFiber: Fiber<Props> | null;
|
||||||
|
|
||||||
// the renderProps and renderPromise keys are only useful for the "prepare"
|
|
||||||
// step of the lifecycle of a component. Once a component has been rendered
|
|
||||||
// and patched, it is no longer useful.
|
|
||||||
renderProps: Props | null;
|
|
||||||
renderPromise: Promise<VNode> | null;
|
|
||||||
|
|
||||||
boundHandlers: { [key: number]: any };
|
boundHandlers: { [key: number]: any };
|
||||||
observer: Observer | null;
|
observer: Observer | null;
|
||||||
render: CompiledTemplate | null;
|
render: CompiledTemplate | null;
|
||||||
mountedHandlers: { [key: number]: Function };
|
mountedCB: Function | null;
|
||||||
|
willUnmountCB: Function | null;
|
||||||
|
willPatchCB: Function | null;
|
||||||
|
patchedCB: Function | null;
|
||||||
classObj: { [key: string]: boolean } | null;
|
classObj: { [key: string]: boolean } | null;
|
||||||
|
refs: { [key: string]: Component<T, any> | HTMLElement | undefined } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
@@ -75,10 +95,14 @@ interface Internal<T extends Env, Props> {
|
|||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
let nextId = 1;
|
let nextId = 1;
|
||||||
|
|
||||||
export class Component<T extends Env, Props extends {}, State extends {}> {
|
export class Component<T extends Env, Props extends {}> {
|
||||||
readonly __owl__: Internal<Env, Props>;
|
readonly __owl__: Internal<Env, Props>;
|
||||||
static template?: string | null = null;
|
static template?: string | null = null;
|
||||||
static _template?: string | null = null;
|
static _template?: string | null = null;
|
||||||
|
static _current?: 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:
|
* The `el` is the root element of the component. Note that it could be null:
|
||||||
@@ -87,20 +111,10 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
get el(): HTMLElement | null {
|
get el(): HTMLElement | null {
|
||||||
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
|
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
|
||||||
}
|
}
|
||||||
static components = {};
|
|
||||||
|
|
||||||
env: T;
|
env: T;
|
||||||
state?: State;
|
|
||||||
props: Props;
|
props: Props;
|
||||||
|
|
||||||
// type of props is not easily representable in typescript...
|
|
||||||
static props?: any;
|
|
||||||
static defaultProps?: any;
|
|
||||||
|
|
||||||
refs: {
|
|
||||||
[key: string]: Component<T, any, any> | HTMLElement | undefined;
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
@@ -124,8 +138,9 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
* hand. Other components should be created automatically by the framework (with
|
* hand. Other components should be created automatically by the framework (with
|
||||||
* the t-component directive in a template)
|
* the t-component directive in a template)
|
||||||
*/
|
*/
|
||||||
constructor(parent: Component<T, any, any> | T, props?: Props) {
|
constructor(parent: Component<T, any> | T, props?: Props) {
|
||||||
const defaultProps = (<any>this.constructor).defaultProps;
|
const defaultProps = (<any>this.constructor).defaultProps;
|
||||||
|
Component._current = this;
|
||||||
if (defaultProps) {
|
if (defaultProps) {
|
||||||
props = this.__applyDefaultProps(props, defaultProps);
|
props = this.__applyDefaultProps(props, defaultProps);
|
||||||
}
|
}
|
||||||
@@ -135,7 +150,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
// Pro: but creating component (by a template) is always unsafe anyway
|
// Pro: but creating component (by a template) is always unsafe anyway
|
||||||
this.props = <Props>props || <Props>{};
|
this.props = <Props>props || <Props>{};
|
||||||
let id: number = nextId++;
|
let id: number = nextId++;
|
||||||
let p: Component<T, any, any> | null = null;
|
let p: Component<T, any> | null = null;
|
||||||
if (parent instanceof Component) {
|
if (parent instanceof Component) {
|
||||||
p = parent;
|
p = parent;
|
||||||
this.env = parent.env;
|
this.env = parent.env;
|
||||||
@@ -169,14 +184,16 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
parent: p,
|
parent: p,
|
||||||
children: {},
|
children: {},
|
||||||
cmap: {},
|
cmap: {},
|
||||||
renderId: 1,
|
currentFiber: null,
|
||||||
renderPromise: null,
|
|
||||||
renderProps: props || null,
|
|
||||||
boundHandlers: {},
|
boundHandlers: {},
|
||||||
mountedHandlers: {},
|
mountedCB: null,
|
||||||
|
willUnmountCB: null,
|
||||||
|
willPatchCB: null,
|
||||||
|
patchedCB: null,
|
||||||
observer: null,
|
observer: null,
|
||||||
render: null,
|
render: null,
|
||||||
classObj: null
|
classObj: null,
|
||||||
|
refs: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,8 +240,6 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
* It is not called on the initial render. This is useful to get some
|
* It is not called on the initial render. This is useful to get some
|
||||||
* information which are in the DOM. For example, the current position of the
|
* information which are in the DOM. For example, the current position of the
|
||||||
* scrollbar
|
* scrollbar
|
||||||
*
|
|
||||||
* The return value of willPatch will be given to the patched function.
|
|
||||||
*/
|
*/
|
||||||
willPatch(): any {}
|
willPatch(): any {}
|
||||||
|
|
||||||
@@ -240,10 +255,8 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
* One need to be careful, because updates here will cause rerender, which in
|
* One need to be careful, because updates here will cause rerender, which in
|
||||||
* turn will cause other calls to updated. So, we need to be particularly
|
* turn will cause other calls to updated. So, we need to be particularly
|
||||||
* careful at avoiding endless cycles.
|
* careful at avoiding endless cycles.
|
||||||
*
|
|
||||||
* The snapshot parameter is the result of the call to willPatch.
|
|
||||||
*/
|
*/
|
||||||
patched(snapshot: any) {}
|
patched() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* willUnmount is a hook that is called each time just before a component is
|
* willUnmount is a hook that is called each time just before a component is
|
||||||
@@ -279,17 +292,20 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
if (__owl__.isMounted) {
|
if (__owl__.isMounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const fiber = this.__createFiber(false, undefined, undefined, undefined);
|
||||||
if (!__owl__.vnode) {
|
if (!__owl__.vnode) {
|
||||||
const vnode = await this.__prepare();
|
fiber.promise = this.__prepareAndRender(fiber);
|
||||||
|
const vnode = await fiber.promise;
|
||||||
if (__owl__.isDestroyed) {
|
if (__owl__.isDestroyed) {
|
||||||
// component was destroyed before we get here...
|
// component was destroyed before we get here...
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.__patch(vnode);
|
this.__patch(vnode);
|
||||||
} else if (renderBeforeRemount) {
|
} else if (renderBeforeRemount) {
|
||||||
const patchQueue = [];
|
fiber.patchQueue.push(fiber);
|
||||||
await this.__render(false, patchQueue, undefined, undefined);
|
fiber.promise = this.__render(fiber);
|
||||||
this.__applyPatchQueue(<any[]>patchQueue);
|
await fiber.promise;
|
||||||
|
this.__applyPatchQueue(fiber);
|
||||||
}
|
}
|
||||||
target.appendChild(this.el!);
|
target.appendChild(this.el!);
|
||||||
|
|
||||||
@@ -323,15 +339,15 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
if (!__owl__.isMounted) {
|
if (!__owl__.isMounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const patchQueue = [];
|
const fiber = this.__createFiber(force, undefined, undefined, undefined);
|
||||||
|
fiber.patchQueue.push(fiber);
|
||||||
|
fiber.promise = this.__render(fiber);
|
||||||
|
await fiber.promise;
|
||||||
|
|
||||||
const renderId = ++__owl__.renderId;
|
if (__owl__.isMounted && fiber === __owl__.currentFiber) {
|
||||||
await this.__render(force, patchQueue, undefined, undefined);
|
|
||||||
|
|
||||||
if (__owl__.isMounted && renderId === __owl__.renderId) {
|
|
||||||
// we only update the vnode and the actual DOM if no other rendering
|
// we only update the vnode and the actual DOM if no other rendering
|
||||||
// occurred between now and when the render method was initially called.
|
// occurred between now and when the render method was initially called.
|
||||||
this.__applyPatchQueue(<any[]>patchQueue);
|
this.__applyPatchQueue(fiber);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,26 +380,6 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This method is the correct way to update the environment of a component. Doing
|
|
||||||
* this will cause a full rerender of the component and its children, so this is
|
|
||||||
* an operation that should not be done frequently.
|
|
||||||
*
|
|
||||||
* A good usecase for updating the environment would be to update some mostly
|
|
||||||
* static config keys, such as a boolean to determine if we are in mobile
|
|
||||||
* mode or not.
|
|
||||||
*/
|
|
||||||
async updateEnv(nextEnv: Partial<T>): Promise<void> {
|
|
||||||
const __owl__ = this.__owl__;
|
|
||||||
if (__owl__.parent && __owl__.parent.env === this.env) {
|
|
||||||
this.env = Object.create(this.env);
|
|
||||||
}
|
|
||||||
Object.assign(this.env, nextEnv);
|
|
||||||
if (__owl__.isMounted) {
|
|
||||||
await this.render(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emit a custom event of type 'eventType' with the given 'payload' on the
|
* Emit a custom event of type 'eventType' with the given 'payload' on the
|
||||||
* component's el, if it exists. However, note that the event will only bubble
|
* component's el, if it exists. However, note that the event will only bubble
|
||||||
@@ -405,6 +401,27 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
// Private
|
// 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
|
* 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
|
* component. It does not remove the el (this is done only once on the top
|
||||||
@@ -416,7 +433,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
* Note that it does not call the __callWillUnmount method to avoid visiting
|
* Note that it does not call the __callWillUnmount method to avoid visiting
|
||||||
* all children many times.
|
* all children many times.
|
||||||
*/
|
*/
|
||||||
__destroy(parent: Component<any, any, any> | null) {
|
__destroy(parent: Component<any, any> | null) {
|
||||||
const __owl__ = this.__owl__;
|
const __owl__ = this.__owl__;
|
||||||
const isMounted = __owl__.isMounted;
|
const isMounted = __owl__.isMounted;
|
||||||
if (isMounted) {
|
if (isMounted) {
|
||||||
@@ -446,20 +463,22 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
__owl__.isMounted = true;
|
__owl__.isMounted = true;
|
||||||
const handlers = __owl__.mountedHandlers;
|
|
||||||
for (let key in handlers) {
|
|
||||||
handlers[key]();
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
this.mounted();
|
this.mounted();
|
||||||
|
if (__owl__.mountedCB) {
|
||||||
|
__owl__.mountedCB()
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorHandler(e, this);
|
errorHandler(e, this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
__callWillUnmount() {
|
__callWillUnmount() {
|
||||||
this.willUnmount();
|
|
||||||
const __owl__ = this.__owl__;
|
const __owl__ = this.__owl__;
|
||||||
|
if (__owl__.willUnmountCB) {
|
||||||
|
__owl__.willUnmountCB();
|
||||||
|
}
|
||||||
|
this.willUnmount();
|
||||||
__owl__.isMounted = false;
|
__owl__.isMounted = false;
|
||||||
const children = __owl__.children;
|
const children = __owl__.children;
|
||||||
for (let id in children) {
|
for (let id in children) {
|
||||||
@@ -476,12 +495,11 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
*/
|
*/
|
||||||
async __updateProps(
|
async __updateProps(
|
||||||
nextProps: Props,
|
nextProps: Props,
|
||||||
forceUpdate: boolean = false,
|
parentFiber: Fiber<any>,
|
||||||
patchQueue?: any[],
|
|
||||||
scope?: any,
|
scope?: any,
|
||||||
vars?: any
|
vars?: any
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
|
const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps);
|
||||||
if (shouldUpdate) {
|
if (shouldUpdate) {
|
||||||
const defaultProps = (<any>this.constructor).defaultProps;
|
const defaultProps = (<any>this.constructor).defaultProps;
|
||||||
if (defaultProps) {
|
if (defaultProps) {
|
||||||
@@ -489,7 +507,10 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
}
|
}
|
||||||
await this.willUpdateProps(nextProps);
|
await this.willUpdateProps(nextProps);
|
||||||
this.props = nextProps;
|
this.props = nextProps;
|
||||||
await this.__render(forceUpdate, patchQueue, scope, vars);
|
const fiber = this.__createFiber(parentFiber.force, scope, vars, parentFiber);
|
||||||
|
fiber.patchQueue.push(fiber);
|
||||||
|
|
||||||
|
await this.__render(fiber);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,14 +524,18 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
__owl__.vnode = patch(target, vnode);
|
__owl__.vnode = patch(target, vnode);
|
||||||
}
|
}
|
||||||
|
|
||||||
__prepare(scope?: Object, vars?: any): Promise<VNode> {
|
/**
|
||||||
const __owl__ = this.__owl__;
|
* The __prepare method is only called by the t-component directive, when a
|
||||||
__owl__.renderProps = this.props;
|
* subcomponent is created. It gets its scope and vars, if any, from the
|
||||||
__owl__.renderPromise = this.__prepareAndRender(scope, vars);
|
* parent template.
|
||||||
return __owl__.renderPromise;
|
*/
|
||||||
|
__prepare(parentFiber: Fiber<any>, scope: any, vars: any): Promise<VNode> {
|
||||||
|
const fiber = this.__createFiber(parentFiber.force, scope, vars, parentFiber);
|
||||||
|
fiber.promise = this.__prepareAndRender(fiber);
|
||||||
|
return fiber.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
async __prepareAndRender(scope?: Object, vars?: any): Promise<VNode> {
|
async __prepareAndRender(fiber: Fiber<Props>): Promise<VNode> {
|
||||||
try {
|
try {
|
||||||
await this.willStart();
|
await this.willStart();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -544,21 +569,12 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
__owl__.render = qweb.render.bind(qweb, p._template);
|
__owl__.render = qweb.render.bind(qweb, p._template);
|
||||||
this.__observeState();
|
return this.__render(fiber);
|
||||||
|
|
||||||
return this.__render(false, [], scope, vars);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
__render(
|
__render(fiber: Fiber<Props>): Promise<VNode> {
|
||||||
force: boolean = false,
|
|
||||||
patchQueue: any[] = [],
|
|
||||||
scope?: Object,
|
|
||||||
vars?: any
|
|
||||||
): Promise<VNode> {
|
|
||||||
const __owl__ = this.__owl__;
|
const __owl__ = this.__owl__;
|
||||||
const promises: Promise<void>[] = [];
|
const promises: Promise<void>[] = [];
|
||||||
const patch: any[] = [this];
|
|
||||||
patchQueue.push(patch);
|
|
||||||
if (__owl__.observer) {
|
if (__owl__.observer) {
|
||||||
__owl__.observer.allowMutations = false;
|
__owl__.observer.allowMutations = false;
|
||||||
}
|
}
|
||||||
@@ -567,17 +583,13 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
vnode = __owl__.render!(this, {
|
vnode = __owl__.render!(this, {
|
||||||
promises,
|
promises,
|
||||||
handlers: __owl__.boundHandlers,
|
handlers: __owl__.boundHandlers,
|
||||||
mountedHandlers: __owl__.mountedHandlers,
|
fiber: fiber
|
||||||
forceUpdate: force,
|
|
||||||
patchQueue,
|
|
||||||
scope,
|
|
||||||
vars
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
vnode = __owl__.vnode || h("div");
|
vnode = __owl__.vnode || h("div");
|
||||||
errorHandler(e, this);
|
errorHandler(e, this);
|
||||||
}
|
}
|
||||||
patch.push(vnode);
|
fiber.vnode = vnode;
|
||||||
if (__owl__.observer) {
|
if (__owl__.observer) {
|
||||||
__owl__.observer.allowMutations = true;
|
__owl__.observer.allowMutations = true;
|
||||||
}
|
}
|
||||||
@@ -625,19 +637,6 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Enable the observe feature on the state. We only create an observer if
|
|
||||||
* there is some state to be observed.
|
|
||||||
*/
|
|
||||||
__observeState() {
|
|
||||||
if (this.state) {
|
|
||||||
const __owl__ = this.__owl__;
|
|
||||||
__owl__.observer = new Observer();
|
|
||||||
this.state = __owl__.observer.observe(this.state);
|
|
||||||
__owl__.observer.notifyCB = this.render.bind(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply default props (only top level).
|
* Apply default props (only top level).
|
||||||
*
|
*
|
||||||
@@ -655,29 +654,34 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply the given patch queue. A patch is a pair [c, vn], where c is a
|
* Apply the given patch queue from a fiber.
|
||||||
* Component instance and vn a VNode.
|
|
||||||
* 1) Call 'willPatch' on the component of each patch
|
* 1) Call 'willPatch' on the component of each patch
|
||||||
* 2) Call '__patch' on the component of each patch
|
* 2) Call '__patch' on the component of each patch
|
||||||
* 3) Call 'patched' on the component of each patch, in inverse order
|
* 3) Call 'patched' on the component of each patch, in reverse order
|
||||||
*/
|
*/
|
||||||
__applyPatchQueue(patchQueue: any[]) {
|
__applyPatchQueue(fiber: Fiber<Props>) {
|
||||||
let component = this;
|
const patchQueue = fiber.patchQueue;
|
||||||
|
let component: Component<any, any> = this;
|
||||||
try {
|
try {
|
||||||
const patchLen = patchQueue.length;
|
const patchLen = patchQueue.length;
|
||||||
for (let i = 0; i < patchLen; i++) {
|
for (let i = 0; i < patchLen; i++) {
|
||||||
const patch = patchQueue[i];
|
component = patchQueue[i].component;
|
||||||
component = patch[0];
|
if (component.__owl__.willPatchCB) {
|
||||||
patch.push(patch[0].willPatch());
|
component.__owl__.willPatchCB();
|
||||||
|
}
|
||||||
|
component.willPatch();
|
||||||
}
|
}
|
||||||
for (let i = 0; i < patchLen; i++) {
|
for (let i = 0; i < patchLen; i++) {
|
||||||
const patch = patchQueue[i];
|
const fiber = patchQueue[i];
|
||||||
patch[0].__patch(patch[1]);
|
component = fiber.component;
|
||||||
|
component.__patch(fiber.vnode);
|
||||||
}
|
}
|
||||||
for (let i = patchLen - 1; i >= 0; i--) {
|
for (let i = patchLen - 1; i >= 0; i--) {
|
||||||
const patch = patchQueue[i];
|
component = patchQueue[i].component;
|
||||||
component = patch[0];
|
component.patched();
|
||||||
patch[0].patched(patch[2]);
|
if (component.__owl__.patchedCB) {
|
||||||
|
component.__owl__.patchedCB();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorHandler(e, component);
|
errorHandler(e, component);
|
||||||
@@ -697,7 +701,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
* If there are no such component, we destroy everything. This is better than
|
* If there are no such component, we destroy everything. This is better than
|
||||||
* being in a corrupted state.
|
* being in a corrupted state.
|
||||||
*/
|
*/
|
||||||
function errorHandler(error: Error, component: Component<any, any, any>) {
|
function errorHandler(error: Error, component: Component<any, any>) {
|
||||||
let canCatch = false;
|
let canCatch = false;
|
||||||
let qweb = component.env.qweb;
|
let qweb = component.env.qweb;
|
||||||
let root = component;
|
let root = component;
|
||||||
|
|||||||
+33
-18
@@ -1,4 +1,5 @@
|
|||||||
import { QWeb } from "../qweb/index";
|
import { QWeb } from "../qweb/index";
|
||||||
|
import { INTERP_REGEXP } from "../qweb/context";
|
||||||
import { MODS_CODE } from "../qweb/extensions";
|
import { MODS_CODE } from "../qweb/extensions";
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
@@ -262,9 +263,10 @@ QWeb.addDirective({
|
|||||||
let refExpr = "";
|
let refExpr = "";
|
||||||
let refKey: string = "";
|
let refKey: string = "";
|
||||||
if (ref) {
|
if (ref) {
|
||||||
|
ctx.rootContext.shouldDefineRefs = true;
|
||||||
refKey = `ref${ctx.generateID()}`;
|
refKey = `ref${ctx.generateID()}`;
|
||||||
ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`);
|
ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`);
|
||||||
refExpr = `context.refs[${refKey}] = w${componentID};`;
|
refExpr = `context.__owl__.refs[${refKey}] = w${componentID};`;
|
||||||
}
|
}
|
||||||
let transitionsInsertCode = "";
|
let transitionsInsertCode = "";
|
||||||
if (transition) {
|
if (transition) {
|
||||||
@@ -272,7 +274,7 @@ QWeb.addDirective({
|
|||||||
}
|
}
|
||||||
let finalizeComponentCode = `w${componentID}.${keepAlive ? "unmount" : "destroy"}();`;
|
let finalizeComponentCode = `w${componentID}.${keepAlive ? "unmount" : "destroy"}();`;
|
||||||
if (ref && !keepAlive) {
|
if (ref && !keepAlive) {
|
||||||
finalizeComponentCode += `delete context.refs[${refKey}];`;
|
finalizeComponentCode += `delete context.__owl__.refs[${refKey}];`;
|
||||||
}
|
}
|
||||||
if (transition) {
|
if (transition) {
|
||||||
finalizeComponentCode = `let finalize = () => {
|
finalizeComponentCode = `let finalize = () => {
|
||||||
@@ -356,8 +358,12 @@ QWeb.addDirective({
|
|||||||
ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`);
|
ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`);
|
||||||
}
|
}
|
||||||
let shouldProxy = false;
|
let shouldProxy = false;
|
||||||
|
if (async || keepAlive) {
|
||||||
|
ctx.addLine(
|
||||||
|
`const fiber${componentID} = Object.assign(Object.create(extra.fiber), {patchQueue: []});`
|
||||||
|
);
|
||||||
|
}
|
||||||
if (async) {
|
if (async) {
|
||||||
ctx.addLine(`const patchQueue${componentID} = [];`);
|
|
||||||
ctx.addLine(
|
ctx.addLine(
|
||||||
`c${ctx.parentNode}.push(w${componentID} && w${componentID}.__owl__.pvnode || null);`
|
`c${ctx.parentNode}.push(w${componentID} && w${componentID}.__owl__.pvnode || null);`
|
||||||
);
|
);
|
||||||
@@ -380,10 +386,12 @@ QWeb.addDirective({
|
|||||||
ctx.addLine(`let props${componentID} = {${propStr}};`);
|
ctx.addLine(`let props${componentID} = {${propStr}};`);
|
||||||
}
|
}
|
||||||
ctx.addIf(
|
ctx.addIf(
|
||||||
`w${componentID} && w${componentID}.__owl__.renderPromise && !w${componentID}.__owl__.vnode`
|
`w${componentID} && w${componentID}.__owl__.currentFiber && !w${componentID}.__owl__.vnode`
|
||||||
);
|
);
|
||||||
ctx.addIf(`utils.shallowEqual(props${componentID}, w${componentID}.__owl__.renderProps)`);
|
ctx.addIf(
|
||||||
ctx.addLine(`def${defID} = w${componentID}.__owl__.renderPromise;`);
|
`utils.shallowEqual(props${componentID}, w${componentID}.__owl__.currentFiber.props)`
|
||||||
|
);
|
||||||
|
ctx.addLine(`def${defID} = w${componentID}.__owl__.currentFiber.promise;`);
|
||||||
ctx.addElse();
|
ctx.addElse();
|
||||||
ctx.addLine(`w${componentID}.destroy();`);
|
ctx.addLine(`w${componentID}.destroy();`);
|
||||||
ctx.addLine(`w${componentID} = false;`);
|
ctx.addLine(`w${componentID} = false;`);
|
||||||
@@ -392,10 +400,16 @@ QWeb.addDirective({
|
|||||||
|
|
||||||
ctx.addIf(`!w${componentID}`);
|
ctx.addIf(`!w${componentID}`);
|
||||||
// new component
|
// new component
|
||||||
ctx.addLine(`let componentKey${componentID} = ${ctx.interpolate(value)};`);
|
let dynamicFallback = "";
|
||||||
|
if (!value.match(INTERP_REGEXP)) {
|
||||||
|
dynamicFallback = `|| ${ctx.formatExpression(value)}`;
|
||||||
|
}
|
||||||
|
const interpValue = ctx.interpolate(value);
|
||||||
|
ctx.addLine(`let componentKey${componentID} = ${interpValue};`);
|
||||||
ctx.addLine(
|
ctx.addLine(
|
||||||
`let W${componentID} = context.constructor.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}];`
|
`let W${componentID} = context.constructor.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}]${dynamicFallback};`
|
||||||
);
|
);
|
||||||
|
|
||||||
// maybe only do this in dev mode...
|
// maybe only do this in dev mode...
|
||||||
ctx.addLine(
|
ctx.addLine(
|
||||||
`if (!W${componentID}) {throw new Error('Cannot find the definition of component "' + componentKey${componentID} + '"')}`
|
`if (!W${componentID}) {throw new Error('Cannot find the definition of component "' + componentKey${componentID} + '"')}`
|
||||||
@@ -441,14 +455,15 @@ QWeb.addDirective({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let scopeVars = "";
|
let scopeVars;
|
||||||
if (hasSlots) {
|
if (hasSlots) {
|
||||||
scopeVars += ctx.scopeVars.length ? `Object.assign({}, scope)` : varDefs.length ? `{}` : "";
|
let scope = ctx.scopeVars.length ? `Object.assign({}, scope)` : `{}`;
|
||||||
if (varDefs.length) {
|
let vars = varDefs.length ? `{${varDefs.join(",")}}` : "undefined";
|
||||||
scopeVars += `, {${varDefs.join(",")}}`;
|
scopeVars = `${scope}, ${vars}`;
|
||||||
|
} else {
|
||||||
|
scopeVars = "undefined, undefined";
|
||||||
}
|
}
|
||||||
}
|
ctx.addLine(`def${defID} = w${componentID}.__prepare(extra.fiber, ${scopeVars});`);
|
||||||
ctx.addLine(`def${defID} = w${componentID}.__prepare(${scopeVars});`);
|
|
||||||
// hack: specify empty remove hook to prevent the node from being removed from the DOM
|
// hack: specify empty remove hook to prevent the node from being removed from the DOM
|
||||||
let registerCode = `c${ctx.parentNode}[_${dummyID}_index]=pvnode;`;
|
let registerCode = `c${ctx.parentNode}[_${dummyID}_index]=pvnode;`;
|
||||||
if (shouldProxy) {
|
if (shouldProxy) {
|
||||||
@@ -460,19 +475,19 @@ QWeb.addDirective({
|
|||||||
|
|
||||||
ctx.addElse();
|
ctx.addElse();
|
||||||
// need to update component
|
// need to update component
|
||||||
let patchQueueCode = async ? `patchQueue${componentID}` : "extra.patchQueue";
|
let patchQueueCode = async || keepAlive ? `fiber${componentID}` : "extra.fiber";
|
||||||
if (keepAlive) {
|
if (keepAlive) {
|
||||||
// if we have t-keepalive="1", the component could be unmounted, but then
|
// 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
|
// 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
|
// the willPatch/patched hooks of the component in this case, so we
|
||||||
// disable the patch queue
|
// disable the patch queue
|
||||||
patchQueueCode = `w${componentID}.__owl__.isMounted ? ${patchQueueCode} : []`;
|
patchQueueCode = `w${componentID}.__owl__.isMounted ? extra.fiber : fiber${componentID}`;
|
||||||
}
|
}
|
||||||
if (QWeb.dev) {
|
if (QWeb.dev) {
|
||||||
ctx.addLine(`utils.validateProps(w${componentID}.constructor, props${componentID})`);
|
ctx.addLine(`utils.validateProps(w${componentID}.constructor, props${componentID})`);
|
||||||
}
|
}
|
||||||
ctx.addLine(
|
ctx.addLine(
|
||||||
`def${defID} = def${defID} || w${componentID}.__updateProps(props${componentID}, extra.forceUpdate, ${patchQueueCode}${scopeVars &&
|
`def${defID} = def${defID} || w${componentID}.__updateProps(props${componentID}, ${patchQueueCode}${scopeVars &&
|
||||||
", " + scopeVars});`
|
", " + scopeVars});`
|
||||||
);
|
);
|
||||||
let keepAliveCode = "";
|
let keepAliveCode = "";
|
||||||
@@ -492,7 +507,7 @@ QWeb.addDirective({
|
|||||||
|
|
||||||
if (async) {
|
if (async) {
|
||||||
ctx.addLine(
|
ctx.addLine(
|
||||||
`def${defID}.then(w${componentID}.__applyPatchQueue.bind(w${componentID}, patchQueue${componentID}));`
|
`def${defID}.then(w${componentID}.__applyPatchQueue.bind(w${componentID}, fiber${componentID}));`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
ctx.addLine(`extra.promises.push(def${defID});`);
|
ctx.addLine(`extra.promises.push(def${defID});`);
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export class Observer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
observe(value: any, parent?: any): any {
|
observe<T>(value: T, parent?: any): T {
|
||||||
if (value === null || typeof value !== "object" || value instanceof Date) {
|
if (value === null || typeof value !== "object" || value instanceof Date) {
|
||||||
// fun fact: typeof null === 'object'
|
// fun fact: typeof null === 'object'
|
||||||
return value;
|
return value;
|
||||||
|
|||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
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) {
|
||||||
|
return function(cb) {
|
||||||
|
const component: Component<any, any> = Component._current;
|
||||||
|
if (component.__owl__[method]) {
|
||||||
|
const current = component.__owl__[method];
|
||||||
|
if (reverse) {
|
||||||
|
component.__owl__[method] = function() {
|
||||||
|
current.call(component);
|
||||||
|
cb.call(component);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
component.__owl__[method] = function() {
|
||||||
|
cb.call(component);
|
||||||
|
current.call(component);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} 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);
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
@@ -11,17 +11,21 @@ import { ConnectedComponent } from "./store/connected_component";
|
|||||||
import { Store } from "./store/store";
|
import { Store } from "./store/store";
|
||||||
import * as _utils from "./utils";
|
import * as _utils from "./utils";
|
||||||
import * as _tags from "./tags";
|
import * as _tags from "./tags";
|
||||||
|
import * as _hooks from "./hooks";
|
||||||
import { Link } from "./router/Link";
|
import { Link } from "./router/Link";
|
||||||
import { RouteComponent } from "./router/RouteComponent";
|
import { RouteComponent } from "./router/RouteComponent";
|
||||||
import { Router } from "./router/Router";
|
import { Router } from "./router/Router";
|
||||||
|
|
||||||
export { Component } from "./component/component";
|
export { Component } from "./component/component";
|
||||||
export { QWeb };
|
export { QWeb };
|
||||||
|
|
||||||
|
export const useState = _hooks.useState;
|
||||||
export const core = { EventBus, Observer };
|
export const core = { EventBus, Observer };
|
||||||
export const router = { Router, RouteComponent, Link };
|
export const router = { Router, RouteComponent, Link };
|
||||||
export const store = { Store, ConnectedComponent };
|
export const store = { Store, ConnectedComponent };
|
||||||
export const utils = _utils;
|
export const utils = _utils;
|
||||||
export const tags = _tags;
|
export const tags = _tags;
|
||||||
|
export const hooks = _hooks;
|
||||||
export const __info__ = {};
|
export const __info__ = {};
|
||||||
|
|
||||||
Object.defineProperty(__info__, "mode", {
|
Object.defineProperty(__info__, "mode", {
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ QWeb.addDirective({
|
|||||||
varCode = `{${content}}`;
|
varCode = `{${content}}`;
|
||||||
}
|
}
|
||||||
ctx.addLine(
|
ctx.addLine(
|
||||||
`this.recursiveFns['${subTemplateName}'].call(this, context, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, vars: ${varCode}, scope}));`
|
`this.recursiveFns['${subTemplateName}'].call(this, context, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, fiber: {vars: ${varCode}, scope}}));`
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -1,5 +1,6 @@
|
|||||||
import { compileExpr, QWebVar } from "./expression_parser";
|
import { compileExpr, QWebVar } from "./expression_parser";
|
||||||
|
|
||||||
|
export const INTERP_REGEXP = /\{\{.*?\}\}/g;
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Compilation Context
|
// Compilation Context
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
@@ -19,6 +20,7 @@ export class Context {
|
|||||||
shouldDefineParent: boolean = false;
|
shouldDefineParent: boolean = false;
|
||||||
shouldDefineQWeb: boolean = false;
|
shouldDefineQWeb: boolean = false;
|
||||||
shouldDefineUtils: boolean = false;
|
shouldDefineUtils: boolean = false;
|
||||||
|
shouldDefineRefs: boolean = false;
|
||||||
shouldDefineResult: boolean = true;
|
shouldDefineResult: boolean = true;
|
||||||
shouldProtectContext: boolean = false;
|
shouldProtectContext: boolean = false;
|
||||||
shouldTrackScope: boolean = false;
|
shouldTrackScope: boolean = false;
|
||||||
@@ -61,6 +63,9 @@ export class Context {
|
|||||||
if (this.shouldDefineResult) {
|
if (this.shouldDefineResult) {
|
||||||
this.code.unshift(" let result;");
|
this.code.unshift(" let result;");
|
||||||
}
|
}
|
||||||
|
if (this.shouldDefineRefs) {
|
||||||
|
this.code.unshift(" context.__owl__.refs = context.__owl__.refs || {};");
|
||||||
|
}
|
||||||
if (this.shouldDefineOwner) {
|
if (this.shouldDefineOwner) {
|
||||||
// this is necessary to prevent some directives (t-forach for ex) to
|
// this is necessary to prevent some directives (t-forach for ex) to
|
||||||
// pollute the rendering context by adding some keys in it.
|
// pollute the rendering context by adding some keys in it.
|
||||||
@@ -163,7 +168,7 @@ export class Context {
|
|||||||
* '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b')
|
* '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b')
|
||||||
*/
|
*/
|
||||||
interpolate(s: string): string {
|
interpolate(s: string): string {
|
||||||
let matches = s.match(/\{\{.*?\}\}/g);
|
let matches = s.match(INTERP_REGEXP);
|
||||||
if (matches && matches[0].length === s.length) {
|
if (matches && matches[0].length === s.length) {
|
||||||
return `(${this.formatExpression(s.slice(2, -2))})`;
|
return `(${this.formatExpression(s.slice(2, -2))})`;
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-38
@@ -79,9 +79,10 @@ QWeb.addDirective({
|
|||||||
name: "ref",
|
name: "ref",
|
||||||
priority: 95,
|
priority: 95,
|
||||||
atNodeCreation({ ctx, value, addNodeHook }) {
|
atNodeCreation({ ctx, value, addNodeHook }) {
|
||||||
|
ctx.rootContext.shouldDefineRefs = true;
|
||||||
const refKey = `ref${ctx.generateID()}`;
|
const refKey = `ref${ctx.generateID()}`;
|
||||||
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
|
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
|
||||||
addNodeHook("create", `context.refs[${refKey}] = n.elm;`);
|
addNodeHook("create", `context.__owl__.refs[${refKey}] = n.elm;`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -182,42 +183,6 @@ QWeb.addDirective({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// t-mounted
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
QWeb.addDirective({
|
|
||||||
name: "mounted",
|
|
||||||
priority: 97,
|
|
||||||
atNodeCreation({ ctx, fullName, value, nodeID, addNodeHook }) {
|
|
||||||
ctx.rootContext.shouldDefineOwner = true;
|
|
||||||
const eventName = fullName.slice(5);
|
|
||||||
if (!eventName) {
|
|
||||||
throw new Error("Missing event name with t-on directive");
|
|
||||||
}
|
|
||||||
let extraArgs;
|
|
||||||
let handler = value.replace(/\(.*\)/, function(args) {
|
|
||||||
extraArgs = args.slice(1, -1);
|
|
||||||
return "";
|
|
||||||
});
|
|
||||||
let error = `(function () {throw new Error('Missing handler \\'' + '${handler}' + \`\\' when evaluating template '${ctx.templateName.replace(
|
|
||||||
/`/g,
|
|
||||||
"'"
|
|
||||||
)}'\`)})()`;
|
|
||||||
if (extraArgs) {
|
|
||||||
ctx.addLine(
|
|
||||||
`extra.mountedHandlers[${nodeID}] = (context['${handler}'] || ${error}).bind(owner, ${ctx.formatExpression(
|
|
||||||
extraArgs
|
|
||||||
)});`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
ctx.addLine(
|
|
||||||
`extra.mountedHandlers[${nodeID}] = extra.mountedHandlers[${nodeID}] || (context['${handler}'] || ${error}).bind(owner);`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
addNodeHook("insert", `if (context.__owl__.isMounted) { extra.mountedHandlers[${nodeID}](); }`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// t-slot
|
// t-slot
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
@@ -254,7 +219,7 @@ QWeb.addDirective({
|
|||||||
const type = node.getAttribute("type");
|
const type = node.getAttribute("type");
|
||||||
let handler;
|
let handler;
|
||||||
let event = fullName.includes(".lazy") ? "change" : "input";
|
let event = fullName.includes(".lazy") ? "change" : "input";
|
||||||
const expr = ctx.formatExpression(`state.${value}`);
|
const expr = ctx.formatExpression(value);
|
||||||
if (node.tagName === "select") {
|
if (node.tagName === "select") {
|
||||||
ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
|
ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
|
||||||
addNodeHook("create", `n.elm.value=${expr};`);
|
addNodeHook("create", `n.elm.value=${expr};`);
|
||||||
|
|||||||
+26
-8
@@ -2,6 +2,7 @@ import { EventBus } from "../core/event_bus";
|
|||||||
import { h, patch, VNode } from "../vdom/index";
|
import { h, patch, VNode } from "../vdom/index";
|
||||||
import { Context } from "./context";
|
import { Context } from "./context";
|
||||||
import { shallowEqual } from "../utils";
|
import { shallowEqual } from "../utils";
|
||||||
|
import { addNS } from "../vdom/vdom";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Owl QWeb Engine
|
* Owl QWeb Engine
|
||||||
@@ -93,11 +94,17 @@ const UTILS: Utils = {
|
|||||||
}
|
}
|
||||||
return expr;
|
return expr;
|
||||||
},
|
},
|
||||||
shallowEqual
|
shallowEqual,
|
||||||
|
addNameSpace(vnode) {
|
||||||
|
addNS(vnode.data, vnode.children, vnode.sel);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function parseXML(xml: string): Document {
|
function parseXML(xml: string): Document {
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
|
|
||||||
|
// we remove comments from the xml string
|
||||||
|
xml = xml.replace(/<!--[\s\S]*?-->/g, "");
|
||||||
const doc = parser.parseFromString(xml, "text/xml");
|
const doc = parser.parseFromString(xml, "text/xml");
|
||||||
if (doc.getElementsByTagName("parsererror").length) {
|
if (doc.getElementsByTagName("parsererror").length) {
|
||||||
let msg = "Invalid XML in template.";
|
let msg = "Invalid XML in template.";
|
||||||
@@ -241,7 +248,7 @@ export class QWeb extends EventBus {
|
|||||||
this._processTemplate(elem);
|
this._processTemplate(elem);
|
||||||
const template = {
|
const template = {
|
||||||
elem,
|
elem,
|
||||||
fn: (context, extra) => {
|
fn: function(this: QWeb, context, extra) {
|
||||||
const compiledFunction = this._compile(name, elem);
|
const compiledFunction = this._compile(name, elem);
|
||||||
template.fn = compiledFunction;
|
template.fn = compiledFunction;
|
||||||
return compiledFunction.call(this, context, extra);
|
return compiledFunction.call(this, context, extra);
|
||||||
@@ -309,8 +316,8 @@ export class QWeb extends EventBus {
|
|||||||
* to render a full component tree, since this is an asynchronous operation.
|
* to render a full component tree, since this is an asynchronous operation.
|
||||||
* This method can only render templates without components.
|
* This method can only render templates without components.
|
||||||
*/
|
*/
|
||||||
renderToString(name: string, context: EvalContext = {}): string {
|
renderToString(name: string, context: EvalContext = {}, extra?: any): string {
|
||||||
const vnode = this.render(name, context);
|
const vnode = this.render(name, context, extra);
|
||||||
if (vnode.sel === undefined) {
|
if (vnode.sel === undefined) {
|
||||||
return vnode.text!;
|
return vnode.text!;
|
||||||
}
|
}
|
||||||
@@ -344,8 +351,8 @@ export class QWeb extends EventBus {
|
|||||||
if (parentContext) {
|
if (parentContext) {
|
||||||
ctx.templates = Object.create(parentContext.templates);
|
ctx.templates = Object.create(parentContext.templates);
|
||||||
ctx.variables = Object.create(parentContext.variables);
|
ctx.variables = Object.create(parentContext.variables);
|
||||||
ctx.nextID = parentContext.parentNode! + 1;
|
ctx.nextID = parentContext.nextID + 1;
|
||||||
ctx.parentNode = parentContext.parentNode!;
|
ctx.parentNode = parentContext.parentNode || ctx.nextID++;
|
||||||
ctx.allowMultipleRoots = true;
|
ctx.allowMultipleRoots = true;
|
||||||
ctx.hasParentWidget = true;
|
ctx.hasParentWidget = true;
|
||||||
ctx.shouldDefineResult = false;
|
ctx.shouldDefineResult = false;
|
||||||
@@ -354,12 +361,12 @@ export class QWeb extends EventBus {
|
|||||||
for (let v in parentContext.variables) {
|
for (let v in parentContext.variables) {
|
||||||
let variable = <any>parentContext.variables[v];
|
let variable = <any>parentContext.variables[v];
|
||||||
if (variable.id) {
|
if (variable.id) {
|
||||||
ctx.addLine(`let ${variable.id} = extra.vars.${variable.id}`);
|
ctx.addLine(`let ${variable.id} = extra.fiber.vars.${variable.id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (parentContext) {
|
if (parentContext) {
|
||||||
ctx.addLine(" Object.assign(context, extra.scope);");
|
ctx.addLine(" Object.assign(context, extra.fiber.scope);");
|
||||||
}
|
}
|
||||||
this._compileNode(elem, ctx);
|
this._compileNode(elem, ctx);
|
||||||
|
|
||||||
@@ -534,6 +541,17 @@ export class QWeb extends EventBus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this._compileChildren(node, ctx);
|
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) {
|
for (let { directive, value, fullName } of validDirectives) {
|
||||||
if (directive.finalize) {
|
if (directive.finalize) {
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ import { Destination, RouterEnv } from "./Router";
|
|||||||
|
|
||||||
type Props = Destination;
|
type Props = Destination;
|
||||||
|
|
||||||
export class Link<Env extends RouterEnv> extends Component<Env, Props, {}> {
|
export class Link<Env extends RouterEnv> extends Component<Env, Props> {
|
||||||
static template = xml`
|
static template = xml`
|
||||||
<a t-att-class="{'router-link-active': isActive }"
|
<a t-att-class="{'router-link-active': isActive }"
|
||||||
t-att-href="href"
|
t-att-href="href"
|
||||||
|
|||||||
@@ -1,27 +1,18 @@
|
|||||||
import { Component } from "../component/component";
|
import { Component } from "../component/component";
|
||||||
import { xml } from "../tags";
|
import { xml } from "../tags";
|
||||||
|
|
||||||
export class RouteComponent extends Component<any, {}, {}> {
|
export class RouteComponent extends Component<any, {}> {
|
||||||
static template = xml`
|
static template = xml`
|
||||||
<t t-foreach="routes" t-as="route">
|
<t>
|
||||||
<t t-if="env.router.currentRouteName === route.name">
|
<t
|
||||||
<t t-component="{{route.component}}" t-props="env.router.currentParams"/>
|
t-if="routeComponent"
|
||||||
</t>
|
t-component="routeComponent"
|
||||||
|
t-key="env.router.currentRouteName"
|
||||||
|
t-props="env.router.currentParams" />
|
||||||
</t>
|
</t>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
routes: any[] = [];
|
get routeComponent(): any {
|
||||||
constructor(parent, props) {
|
return this.env.router.currentRoute && this.env.router.currentRoute.component;
|
||||||
super(parent, props);
|
|
||||||
const router = this.env.router;
|
|
||||||
for (let name of router.routeIds) {
|
|
||||||
const route = router.routes[name];
|
|
||||||
if (route.component) {
|
|
||||||
this.routes.push({
|
|
||||||
name: route.name,
|
|
||||||
component: "__component__" + route.name
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Component, Env } from "../component/component";
|
import { Component, Env, Fiber } from "../component/component";
|
||||||
|
import { VNode } from "../vdom/index";
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Connect function
|
// Connect function
|
||||||
@@ -6,7 +7,7 @@ import { Component, Env } from "../component/component";
|
|||||||
|
|
||||||
type HashFunction = (a: any, b: any) => number;
|
type HashFunction = (a: any, b: any) => number;
|
||||||
|
|
||||||
export class ConnectedComponent<T extends Env, P, S> extends Component<T, P, S> {
|
export class ConnectedComponent<T extends Env, P> extends Component<T, P> {
|
||||||
deep: boolean = true;
|
deep: boolean = true;
|
||||||
getStore(env) {
|
getStore(env) {
|
||||||
return env.store;
|
return env.store;
|
||||||
@@ -46,10 +47,7 @@ export class ConnectedComponent<T extends Env, P, S> extends Component<T, P, S>
|
|||||||
/**
|
/**
|
||||||
* Need to do this here so 'deep' can be overrided by subcomponent easily
|
* Need to do this here so 'deep' can be overrided by subcomponent easily
|
||||||
*/
|
*/
|
||||||
async __prepareAndRender(
|
async __prepareAndRender(fiber: Fiber<P>): Promise<VNode> {
|
||||||
scope?: Object,
|
|
||||||
vars?: any
|
|
||||||
): ReturnType<Component<any, any, any>["__prepareAndRender"]> {
|
|
||||||
const store = this.getStore(this.env);
|
const store = this.getStore(this.env);
|
||||||
const ownProps = this.props || {};
|
const ownProps = this.props || {};
|
||||||
this.storeProps = (<any>this.constructor).mapStoreToProps(store.state, ownProps, store.getters);
|
this.storeProps = (<any>this.constructor).mapStoreToProps(store.state, ownProps, store.getters);
|
||||||
@@ -62,7 +60,7 @@ export class ConnectedComponent<T extends Env, P, S> extends Component<T, P, S>
|
|||||||
prevStoreProps: this.storeProps
|
prevStoreProps: this.storeProps
|
||||||
});
|
});
|
||||||
(this.__owl__ as any).rev = observer.rev;
|
(this.__owl__ as any).rev = observer.rev;
|
||||||
return super.__prepareAndRender(scope, vars);
|
return super.__prepareAndRender(fiber);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* We do not use the mounted hook here for a subtle reason: we want the
|
* We do not use the mounted hook here for a subtle reason: we want the
|
||||||
@@ -107,9 +105,9 @@ export class ConnectedComponent<T extends Env, P, S> extends Component<T, P, S>
|
|||||||
return (this.__owl__ as any).renderPromise;
|
return (this.__owl__ as any).renderPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
async __updateProps(nextProps: P, f, p, s, v) {
|
async __updateProps(nextProps: P, f, s, v) {
|
||||||
this.__updateStoreProps(nextProps);
|
this.__updateStoreProps(nextProps);
|
||||||
return super.__updateProps(nextProps, f, p, s, v);
|
return super.__updateProps(nextProps, f, s, v);
|
||||||
}
|
}
|
||||||
|
|
||||||
__updateStoreProps(nextProps): boolean {
|
__updateStoreProps(nextProps): boolean {
|
||||||
|
|||||||
+3
-4
@@ -10,8 +10,6 @@ import { QWeb } from "./qweb/index";
|
|||||||
* The plan is to add a few other tags such as css, globalcss.
|
* The plan is to add a few other tags such as css, globalcss.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* XML tag helper for defining templates. With this, one can simply define
|
* XML tag helper for defining templates. With this, one can simply define
|
||||||
* an inline template with just the template xml:
|
* an inline template with just the template xml:
|
||||||
@@ -21,8 +19,9 @@ import { QWeb } from "./qweb/index";
|
|||||||
* }
|
* }
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function xml(strings) {
|
export function xml(strings, ...args) {
|
||||||
const name = `__template__${QWeb.nextId++}`;
|
const name = `__template__${QWeb.nextId++}`;
|
||||||
QWeb.registerTemplate(name, strings[0]);
|
const value = String.raw(strings, ...args);
|
||||||
|
QWeb.registerTemplate(name, value);
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-3
@@ -48,9 +48,7 @@ export async function loadTemplates(url: string): Promise<string> {
|
|||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
throw new Error("Error while fetching xml templates");
|
throw new Error("Error while fetching xml templates");
|
||||||
}
|
}
|
||||||
let templates = await result.text();
|
return await result.text();
|
||||||
templates = templates.replace(/<!--[\s\S]*?-->/g, "");
|
|
||||||
return templates;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function escape(str: string | number | undefined): string {
|
export function escape(str: string | number | undefined): string {
|
||||||
|
|||||||
+7
-11
@@ -564,13 +564,17 @@ type VNodeChildElement = VNode | string | number | undefined | null;
|
|||||||
type ArrayOrElement<T> = T | T[];
|
type ArrayOrElement<T> = T | T[];
|
||||||
type VNodeChildren = ArrayOrElement<VNodeChildElement>;
|
type VNodeChildren = ArrayOrElement<VNodeChildElement>;
|
||||||
|
|
||||||
function addNS(data: any, children: VNodes | undefined, sel: string | undefined): void {
|
export function addNS(data: any, children: VNodes | undefined, sel: string | undefined): void {
|
||||||
data.ns = "http://www.w3.org/2000/svg";
|
data.ns = "http://www.w3.org/2000/svg";
|
||||||
if (sel !== "foreignObject" && children !== undefined) {
|
if (sel !== "foreignObject" && children !== undefined) {
|
||||||
for (let i = 0, iLen = children.length; i < iLen; ++i) {
|
for (let i = 0, iLen = children.length; i < iLen; ++i) {
|
||||||
let childData = children[i].data;
|
const child = children[i];
|
||||||
|
if (child === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let childData = child.data;
|
||||||
if (childData !== undefined) {
|
if (childData !== undefined) {
|
||||||
addNS(childData, (children[i] as VNode).children as VNodes, children[i].sel);
|
addNS(childData, (child as VNode).children as VNodes, child.sel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -612,13 +616,5 @@ export function h(sel: any, b?: any, c?: any): VNode {
|
|||||||
children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
|
children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
sel[0] === "s" &&
|
|
||||||
sel[1] === "v" &&
|
|
||||||
sel[2] === "g" &&
|
|
||||||
(sel.length === 3 || sel[3] === "." || sel[3] === "#")
|
|
||||||
) {
|
|
||||||
addNS(data, children, sel);
|
|
||||||
}
|
|
||||||
return vnode(sel, data, children, text, undefined);
|
return vnode(sel, data, children, text, undefined);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ exports[`animations t-transition combined with component 1`] = `
|
|||||||
let _2_index = c1.length;
|
let _2_index = c1.length;
|
||||||
c1.push(null);
|
c1.push(null);
|
||||||
let props4 = {};
|
let props4 = {};
|
||||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
if (w4 && w4.__owl__.currentFiber && !w4.__owl__.vnode) {
|
||||||
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
if (utils.shallowEqual(props4, w4.__owl__.currentFiber.props)) {
|
||||||
def3 = w4.__owl__.renderPromise;
|
def3 = w4.__owl__.currentFiber.promise;
|
||||||
} else {
|
} else {
|
||||||
w4.destroy();
|
w4.destroy();
|
||||||
w4 = false;
|
w4 = false;
|
||||||
@@ -27,17 +27,17 @@ exports[`animations t-transition combined with component 1`] = `
|
|||||||
}
|
}
|
||||||
if (!w4) {
|
if (!w4) {
|
||||||
let componentKey4 = \`Child\`;
|
let componentKey4 = \`Child\`;
|
||||||
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4];
|
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4]|| context['Child'];
|
||||||
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
||||||
w4 = new W4(parent, props4);
|
w4 = new W4(parent, props4);
|
||||||
parent.__owl__.cmap[4] = w4.__owl__.id;
|
parent.__owl__.cmap[4] = w4.__owl__.id;
|
||||||
def3 = w4.__prepare();
|
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 = () => {
|
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();
|
w4.destroy();
|
||||||
};
|
};
|
||||||
utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
||||||
} else {
|
} else {
|
||||||
def3 = 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;});
|
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
||||||
}
|
}
|
||||||
extra.promises.push(def3);
|
extra.promises.push(def3);
|
||||||
@@ -63,9 +63,9 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
|
|||||||
let _2_index = c1.length;
|
let _2_index = c1.length;
|
||||||
c1.push(null);
|
c1.push(null);
|
||||||
let props4 = {};
|
let props4 = {};
|
||||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
if (w4 && w4.__owl__.currentFiber && !w4.__owl__.vnode) {
|
||||||
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
if (utils.shallowEqual(props4, w4.__owl__.currentFiber.props)) {
|
||||||
def3 = w4.__owl__.renderPromise;
|
def3 = w4.__owl__.currentFiber.promise;
|
||||||
} else {
|
} else {
|
||||||
w4.destroy();
|
w4.destroy();
|
||||||
w4 = false;
|
w4 = false;
|
||||||
@@ -73,17 +73,17 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
|
|||||||
}
|
}
|
||||||
if (!w4) {
|
if (!w4) {
|
||||||
let componentKey4 = \`Child\`;
|
let componentKey4 = \`Child\`;
|
||||||
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4];
|
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4]|| context['Child'];
|
||||||
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
||||||
w4 = new W4(parent, props4);
|
w4 = new W4(parent, props4);
|
||||||
parent.__owl__.cmap[4] = w4.__owl__.id;
|
parent.__owl__.cmap[4] = w4.__owl__.id;
|
||||||
def3 = w4.__prepare();
|
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 = () => {
|
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();
|
w4.destroy();
|
||||||
};
|
};
|
||||||
utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
||||||
} else {
|
} else {
|
||||||
def3 = 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;});
|
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
||||||
}
|
}
|
||||||
extra.promises.push(def3);
|
extra.promises.push(def3);
|
||||||
|
|||||||
+11
-36
@@ -1,10 +1,10 @@
|
|||||||
import { Component, Env } from "../src/component/component";
|
import { Component, Env } from "../src/component/component";
|
||||||
import { QWeb } from "../src/qweb/index";
|
import { QWeb } from "../src/qweb/index";
|
||||||
|
import { useState, useRef } from "../src/hooks";
|
||||||
import {
|
import {
|
||||||
makeDeferred,
|
makeDeferred,
|
||||||
makeTestFixture,
|
makeTestFixture,
|
||||||
makeTestEnv,
|
makeTestEnv,
|
||||||
nextTick,
|
|
||||||
patchNextFrame,
|
patchNextFrame,
|
||||||
renderToDOM,
|
renderToDOM,
|
||||||
unpatchNextFrame
|
unpatchNextFrame
|
||||||
@@ -36,7 +36,7 @@ afterEach(() => {
|
|||||||
fixture.remove();
|
fixture.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
class Widget extends Component<any, any, any> {}
|
class Widget extends Component<any, any> {}
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Tests
|
// Tests
|
||||||
@@ -106,7 +106,7 @@ describe("animations", () => {
|
|||||||
`<div><span t-if="!state.hide" t-transition="chimay">blue</span></div>`
|
`<div><span t-if="!state.hide" t-transition="chimay">blue</span></div>`
|
||||||
);
|
);
|
||||||
class TestWidget extends Widget {
|
class TestWidget extends Widget {
|
||||||
state = { hide: false };
|
state = useState({ hide: false });
|
||||||
}
|
}
|
||||||
const widget = new TestWidget(env);
|
const widget = new TestWidget(env);
|
||||||
|
|
||||||
@@ -148,7 +148,8 @@ describe("animations", () => {
|
|||||||
`<div><span t-ref="span" t-transition="chimay">blue</span></div>`
|
`<div><span t-ref="span" t-transition="chimay">blue</span></div>`
|
||||||
);
|
);
|
||||||
class TestWidget extends Widget {
|
class TestWidget extends Widget {
|
||||||
state = { hide: false };
|
state = useState({ hide: false });
|
||||||
|
span = useRef("span");
|
||||||
}
|
}
|
||||||
const widget = new TestWidget(env);
|
const widget = new TestWidget(env);
|
||||||
|
|
||||||
@@ -163,7 +164,7 @@ describe("animations", () => {
|
|||||||
});
|
});
|
||||||
await widget.mount(fixture);
|
await widget.mount(fixture);
|
||||||
spanNode = widget.el!.children[0];
|
spanNode = widget.el!.children[0];
|
||||||
expect(widget.refs.span).toBe(spanNode);
|
expect(widget.span.el).toBe(spanNode);
|
||||||
expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
|
expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
|
||||||
await def; // wait for the mocked repaint to be done
|
await def; // wait for the mocked repaint to be done
|
||||||
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||||
@@ -217,7 +218,7 @@ describe("animations", () => {
|
|||||||
class Child extends Widget {}
|
class Child extends Widget {}
|
||||||
class Parent extends Widget {
|
class Parent extends Widget {
|
||||||
static components = { Child: Child };
|
static components = { Child: Child };
|
||||||
state = { display: true };
|
state = useState({ display: true });
|
||||||
}
|
}
|
||||||
const widget = new Parent(env);
|
const widget = new Parent(env);
|
||||||
|
|
||||||
@@ -263,28 +264,6 @@ describe("animations", () => {
|
|||||||
expect(fixture.innerHTML).toBe("<div></div>");
|
expect(fixture.innerHTML).toBe("<div></div>");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("t-transition combined with t-mounted", async () => {
|
|
||||||
env.qweb.addTemplate(
|
|
||||||
"TestWidget",
|
|
||||||
`<div><span t-if="state.flag" t-mounted="f" t-transition="chimay">blue</span></div>`
|
|
||||||
);
|
|
||||||
class TestWidget extends Widget {
|
|
||||||
state = { flag: false };
|
|
||||||
f() {}
|
|
||||||
}
|
|
||||||
const widget = new TestWidget(env);
|
|
||||||
widget.f = jest.fn();
|
|
||||||
await widget.mount(fixture);
|
|
||||||
|
|
||||||
patchNextFrame(cb => cb());
|
|
||||||
expect(widget.f).toHaveBeenCalledTimes(0);
|
|
||||||
|
|
||||||
widget.state.flag = true;
|
|
||||||
await nextTick();
|
|
||||||
expect(widget.f).toHaveBeenCalledTimes(1);
|
|
||||||
unpatchNextFrame();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("t-transition, remove and re-add before transitionend", async () => {
|
test("t-transition, remove and re-add before transitionend", async () => {
|
||||||
expect.assertions(11);
|
expect.assertions(11);
|
||||||
|
|
||||||
@@ -297,10 +276,8 @@ describe("animations", () => {
|
|||||||
</templates>`
|
</templates>`
|
||||||
);
|
);
|
||||||
class Parent extends Widget {
|
class Parent extends Widget {
|
||||||
constructor(parent) {
|
state = useState({ flag: false });
|
||||||
super(parent);
|
|
||||||
this.state = { flag: false };
|
|
||||||
}
|
|
||||||
toggle() {
|
toggle() {
|
||||||
this.state.flag = !this.state.flag;
|
this.state.flag = !this.state.flag;
|
||||||
}
|
}
|
||||||
@@ -357,10 +334,8 @@ describe("animations", () => {
|
|||||||
class Child extends Widget {}
|
class Child extends Widget {}
|
||||||
class Parent extends Widget {
|
class Parent extends Widget {
|
||||||
static components = { Child };
|
static components = { Child };
|
||||||
constructor(parent) {
|
state = useState({ flag: false });
|
||||||
super(parent);
|
|
||||||
this.state = { flag: false };
|
|
||||||
}
|
|
||||||
toggle() {
|
toggle() {
|
||||||
this.state.flag = !this.state.flag;
|
this.state.flag = !this.state.flag;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -21,9 +21,9 @@ exports[`props validation props are validated in dev mode (code snapshot) 1`] =
|
|||||||
let _2_index = c1.length;
|
let _2_index = c1.length;
|
||||||
c1.push(null);
|
c1.push(null);
|
||||||
let props4 = {message:1};
|
let props4 = {message:1};
|
||||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
if (w4 && w4.__owl__.currentFiber && !w4.__owl__.vnode) {
|
||||||
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
if (utils.shallowEqual(props4, w4.__owl__.currentFiber.props)) {
|
||||||
def3 = w4.__owl__.renderPromise;
|
def3 = w4.__owl__.currentFiber.promise;
|
||||||
} else {
|
} else {
|
||||||
w4.destroy();
|
w4.destroy();
|
||||||
w4 = false;
|
w4 = false;
|
||||||
@@ -31,16 +31,16 @@ exports[`props validation props are validated in dev mode (code snapshot) 1`] =
|
|||||||
}
|
}
|
||||||
if (!w4) {
|
if (!w4) {
|
||||||
let componentKey4 = \`Child\`;
|
let componentKey4 = \`Child\`;
|
||||||
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4];
|
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4]|| context['Child'];
|
||||||
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
||||||
utils.validateProps(W4, props4)
|
utils.validateProps(W4, props4)
|
||||||
w4 = new W4(parent, props4);
|
w4 = new W4(parent, props4);
|
||||||
parent.__owl__.cmap[4] = w4.__owl__.id;
|
parent.__owl__.cmap[4] = w4.__owl__.id;
|
||||||
def3 = w4.__prepare();
|
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;});
|
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 {
|
} else {
|
||||||
utils.validateProps(w4.constructor, props4)
|
utils.validateProps(w4.constructor, props4)
|
||||||
def3 = 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;});
|
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
||||||
}
|
}
|
||||||
extra.promises.push(def3);
|
extra.promises.push(def3);
|
||||||
|
|||||||
+283
-351
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,7 @@ afterEach(() => {
|
|||||||
QWeb.dev = dev;
|
QWeb.dev = dev;
|
||||||
});
|
});
|
||||||
|
|
||||||
class Widget extends Component<any, any, any> {}
|
class Widget extends Component<any, any> {}
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Tests
|
// Tests
|
||||||
@@ -334,7 +334,8 @@ describe("default props", () => {
|
|||||||
const w = new TestWidget(env, { p: 1 });
|
const w = new TestWidget(env, { p: 1 });
|
||||||
await w.mount(fixture);
|
await w.mount(fixture);
|
||||||
expect(fixture.innerHTML).toMatchSnapshot();
|
expect(fixture.innerHTML).toMatchSnapshot();
|
||||||
await w.__updateProps({});
|
const fiber = w.__createFiber(false, undefined, undefined, undefined);
|
||||||
|
await w.__updateProps({}, fiber);
|
||||||
await w.render();
|
await w.render();
|
||||||
expect(w.props.p).toBe(4);
|
expect(w.props.p).toBe(4);
|
||||||
expect(fixture.innerHTML).toMatchSnapshot();
|
expect(fixture.innerHTML).toMatchSnapshot();
|
||||||
|
|||||||
+8
-3
@@ -99,10 +99,15 @@ export function renderToDOM(
|
|||||||
* Note that the result of renderToString is guaranteed to be the same as the
|
* Note that the result of renderToString is guaranteed to be the same as the
|
||||||
* one from QWeb.
|
* one from QWeb.
|
||||||
*/
|
*/
|
||||||
export function renderToString(qweb: QWeb, t: string, context: EvalContext = {}): string {
|
export function renderToString(
|
||||||
const node = renderToDOM(qweb, t, context);
|
qweb: QWeb,
|
||||||
|
t: string,
|
||||||
|
context: EvalContext = {},
|
||||||
|
extra?: any
|
||||||
|
): string {
|
||||||
|
const node = renderToDOM(qweb, t, context, extra);
|
||||||
const result = node instanceof Text ? node.textContent! : node.outerHTML;
|
const result = node instanceof Text ? node.textContent! : node.outerHTML;
|
||||||
if (result !== qweb.renderToString(t, context)) {
|
if (result !== qweb.renderToString(t, context, extra)) {
|
||||||
throw new Error("HTML string returned by renderToString helper does not match QWeb render");
|
throw new Error("HTML string returned by renderToString helper does not match QWeb render");
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -0,0 +1,359 @@
|
|||||||
|
import { makeTestEnv, makeTestFixture, nextTick } from "./helpers";
|
||||||
|
import { Component, Env } from "../src/component/component";
|
||||||
|
import {
|
||||||
|
useState,
|
||||||
|
onMounted,
|
||||||
|
onWillUnmount,
|
||||||
|
useRef,
|
||||||
|
onPatched,
|
||||||
|
onWillPatch,
|
||||||
|
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("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("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("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>");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -676,6 +676,54 @@ exports[`misc global 1`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`properly support svg add proper namespace to g tags 1`] = `
|
||||||
|
"function anonymous(context,extra
|
||||||
|
) {
|
||||||
|
let utils = this.constructor.utils;
|
||||||
|
var h = this.h;
|
||||||
|
let c1 = [], p1 = {key:1};
|
||||||
|
var vn1 = h('g', p1, c1);
|
||||||
|
result = vn1;
|
||||||
|
var _2 = '50';
|
||||||
|
var _3 = '50';
|
||||||
|
var _4 = '4';
|
||||||
|
var _5 = 'green';
|
||||||
|
var _6 = '1';
|
||||||
|
var _7 = 'yellow';
|
||||||
|
let c8 = [], p8 = {key:8,attrs:{cx: _2,cy: _3,r: _4,stroke: _5,\\"stroke-width\\": _6,fill: _7}};
|
||||||
|
var vn8 = h('circle', p8, c8);
|
||||||
|
c1.push(vn8);
|
||||||
|
c1.push({text: \` \`});
|
||||||
|
utils.addNameSpace(vn1);
|
||||||
|
return vn1;
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`properly support svg add proper namespace to svg 1`] = `
|
||||||
|
"function anonymous(context,extra
|
||||||
|
) {
|
||||||
|
let utils = this.constructor.utils;
|
||||||
|
var h = this.h;
|
||||||
|
var _1 = '100px';
|
||||||
|
var _2 = '90px';
|
||||||
|
let c3 = [], p3 = {key:3,attrs:{width: _1,height: _2}};
|
||||||
|
var vn3 = h('svg', p3, c3);
|
||||||
|
result = vn3;
|
||||||
|
var _4 = '50';
|
||||||
|
var _5 = '50';
|
||||||
|
var _6 = '4';
|
||||||
|
var _7 = 'green';
|
||||||
|
var _8 = '1';
|
||||||
|
var _9 = 'yellow';
|
||||||
|
let c10 = [], p10 = {key:10,attrs:{cx: _4,cy: _5,r: _6,stroke: _7,\\"stroke-width\\": _8,fill: _9}};
|
||||||
|
var vn10 = h('circle', p10, c10);
|
||||||
|
c3.push(vn10);
|
||||||
|
c3.push({text: \` \`});
|
||||||
|
utils.addNameSpace(vn3);
|
||||||
|
return vn3;
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`special cases for some boolean html attributes/properties input type= checkbox, with t-att-checked 1`] = `
|
exports[`special cases for some boolean html attributes/properties input type= checkbox, with t-att-checked 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
@@ -768,6 +816,18 @@ exports[`static templates empty div 1`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`static templates ignore comments 1`] = `
|
||||||
|
"function anonymous(context,extra
|
||||||
|
) {
|
||||||
|
var h = this.h;
|
||||||
|
let c1 = [], p1 = {key:1};
|
||||||
|
var vn1 = h('div', p1, c1);
|
||||||
|
result = vn1;
|
||||||
|
c1.push({text: \`hello owl\`});
|
||||||
|
return vn1;
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`static templates simple dynamic value 1`] = `
|
exports[`static templates simple dynamic value 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
@@ -872,7 +932,7 @@ exports[`t-call (template calling recursive template, part 1 1`] = `
|
|||||||
c1.push(vn2);
|
c1.push(vn2);
|
||||||
c2.push({text: \`hey\`});
|
c2.push({text: \`hey\`});
|
||||||
if (false) {
|
if (false) {
|
||||||
this.recursiveFns['__3'].call(this, context, Object.assign({}, extra, {parentNode: c1, vars: {}, scope}));
|
this.recursiveFns['__3'].call(this, context, Object.assign({}, extra, {parentNode: c1, fiber: {vars: {}, scope}}));
|
||||||
}
|
}
|
||||||
return vn1;
|
return vn1;
|
||||||
}"
|
}"
|
||||||
@@ -884,16 +944,16 @@ exports[`t-call (template calling recursive template, part 1 2`] = `
|
|||||||
let owner = context;
|
let owner = context;
|
||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = extra.parentNode;
|
let c1 = extra.parentNode;
|
||||||
Object.assign(context, extra.scope);
|
Object.assign(context, extra.fiber.scope);
|
||||||
let c2 = [], p2 = {key:2};
|
let c5 = [], p5 = {key:5};
|
||||||
var vn2 = h('div', p2, c2);
|
var vn5 = h('div', p5, c5);
|
||||||
c1.push(vn2);
|
c1.push(vn5);
|
||||||
let c3 = [], p3 = {key:3};
|
let c6 = [], p6 = {key:6};
|
||||||
var vn3 = h('span', p3, c3);
|
var vn6 = h('span', p6, c6);
|
||||||
c2.push(vn3);
|
c5.push(vn6);
|
||||||
c3.push({text: \`hey\`});
|
c6.push({text: \`hey\`});
|
||||||
if (false) {
|
if (false) {
|
||||||
this.recursiveFns['__3'].call(this, context, Object.assign({}, extra, {parentNode: c2, vars: {}, scope}));
|
this.recursiveFns['__3'].call(this, context, Object.assign({}, extra, {parentNode: c5, fiber: {vars: {}, scope}}));
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
@@ -939,7 +999,7 @@ exports[`t-call (template calling recursive template, part 2 1`] = `
|
|||||||
scope.subtree = context.subtree;
|
scope.subtree = context.subtree;
|
||||||
context.subtree_value = _8[i];
|
context.subtree_value = _8[i];
|
||||||
scope.subtree_value = context.subtree_value;
|
scope.subtree_value = context.subtree_value;
|
||||||
this.recursiveFns['__10'].call(this, context, Object.assign({}, extra, {parentNode: c3, vars: {_v0: context['subtree']}, scope}));
|
this.recursiveFns['__10'].call(this, context, Object.assign({}, extra, {parentNode: c3, fiber: {vars: {_v0: context['subtree']}, scope}}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return vn1;
|
return vn1;
|
||||||
@@ -954,38 +1014,38 @@ exports[`t-call (template calling recursive template, part 2 2`] = `
|
|||||||
const scope = Object.create(null);
|
const scope = Object.create(null);
|
||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c3 = extra.parentNode;
|
let c3 = extra.parentNode;
|
||||||
let _v0 = extra.vars._v0
|
let _v0 = extra.fiber.vars._v0
|
||||||
Object.assign(context, extra.scope);
|
Object.assign(context, extra.fiber.scope);
|
||||||
let c4 = [], p4 = {key:4};
|
let c12 = [], p12 = {key:12};
|
||||||
var vn4 = h('div', p4, c4);
|
var vn12 = h('div', p12, c12);
|
||||||
c3.push(vn4);
|
c3.push(vn12);
|
||||||
let c5 = [], p5 = {key:5};
|
let c13 = [], p13 = {key:13};
|
||||||
var vn5 = h('p', p5, c5);
|
var vn13 = h('p', p13, c13);
|
||||||
c4.push(vn5);
|
c12.push(vn13);
|
||||||
var _6 = _v0.val;
|
var _14 = _v0.val;
|
||||||
if (_6 || _6 === 0) {
|
if (_14 || _14 === 0) {
|
||||||
c5.push({text: _6});
|
c13.push({text: _14});
|
||||||
}
|
}
|
||||||
var _7 = _v0.children||[];
|
var _15 = _v0.children||[];
|
||||||
if (!_7) { throw new Error('QWeb error: Invalid loop expression')}
|
if (!_15) { throw new Error('QWeb error: Invalid loop expression')}
|
||||||
var _8 = _9 = _7;
|
var _16 = _17 = _15;
|
||||||
if (!(_7 instanceof Array)) {
|
if (!(_15 instanceof Array)) {
|
||||||
_8 = Object.keys(_7);
|
_16 = Object.keys(_15);
|
||||||
_9 = Object.values(_7);
|
_17 = Object.values(_15);
|
||||||
}
|
}
|
||||||
var _length8 = _8.length;
|
var _length16 = _16.length;
|
||||||
for (let i = 0; i < _length8; i++) {
|
for (let i = 0; i < _length16; i++) {
|
||||||
context.subtree_first = i === 0;
|
context.subtree_first = i === 0;
|
||||||
scope.subtree_first = context.subtree_first;
|
scope.subtree_first = context.subtree_first;
|
||||||
context.subtree_last = i === _length8 - 1;
|
context.subtree_last = i === _length16 - 1;
|
||||||
scope.subtree_last = context.subtree_last;
|
scope.subtree_last = context.subtree_last;
|
||||||
context.subtree_index = i;
|
context.subtree_index = i;
|
||||||
scope.subtree_index = context.subtree_index;
|
scope.subtree_index = context.subtree_index;
|
||||||
context.subtree = _8[i];
|
context.subtree = _16[i];
|
||||||
scope.subtree = context.subtree;
|
scope.subtree = context.subtree;
|
||||||
context.subtree_value = _9[i];
|
context.subtree_value = _17[i];
|
||||||
scope.subtree_value = context.subtree_value;
|
scope.subtree_value = context.subtree_value;
|
||||||
this.recursiveFns['__10'].call(this, context, Object.assign({}, extra, {parentNode: c4, vars: {_v0: context['subtree']}, scope}));
|
this.recursiveFns['__10'].call(this, context, Object.assign({}, extra, {parentNode: c12, fiber: {vars: {_v0: context['subtree']}, scope}}));
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
@@ -1031,7 +1091,7 @@ exports[`t-call (template calling recursive template, part 3 1`] = `
|
|||||||
scope.subtree = context.subtree;
|
scope.subtree = context.subtree;
|
||||||
context.subtree_value = _8[i];
|
context.subtree_value = _8[i];
|
||||||
scope.subtree_value = context.subtree_value;
|
scope.subtree_value = context.subtree_value;
|
||||||
this.recursiveFns['__10'].call(this, context, Object.assign({}, extra, {parentNode: c3, vars: {_v0: context['subtree']}, scope}));
|
this.recursiveFns['__10'].call(this, context, Object.assign({}, extra, {parentNode: c3, fiber: {vars: {_v0: context['subtree']}, scope}}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return vn1;
|
return vn1;
|
||||||
@@ -1046,38 +1106,38 @@ exports[`t-call (template calling recursive template, part 3 2`] = `
|
|||||||
const scope = Object.create(null);
|
const scope = Object.create(null);
|
||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c3 = extra.parentNode;
|
let c3 = extra.parentNode;
|
||||||
let _v0 = extra.vars._v0
|
let _v0 = extra.fiber.vars._v0
|
||||||
Object.assign(context, extra.scope);
|
Object.assign(context, extra.fiber.scope);
|
||||||
let c4 = [], p4 = {key:4};
|
let c12 = [], p12 = {key:12};
|
||||||
var vn4 = h('div', p4, c4);
|
var vn12 = h('div', p12, c12);
|
||||||
c3.push(vn4);
|
c3.push(vn12);
|
||||||
let c5 = [], p5 = {key:5};
|
let c13 = [], p13 = {key:13};
|
||||||
var vn5 = h('p', p5, c5);
|
var vn13 = h('p', p13, c13);
|
||||||
c4.push(vn5);
|
c12.push(vn13);
|
||||||
var _6 = _v0.val;
|
var _14 = _v0.val;
|
||||||
if (_6 || _6 === 0) {
|
if (_14 || _14 === 0) {
|
||||||
c5.push({text: _6});
|
c13.push({text: _14});
|
||||||
}
|
}
|
||||||
var _7 = _v0.children||[];
|
var _15 = _v0.children||[];
|
||||||
if (!_7) { throw new Error('QWeb error: Invalid loop expression')}
|
if (!_15) { throw new Error('QWeb error: Invalid loop expression')}
|
||||||
var _8 = _9 = _7;
|
var _16 = _17 = _15;
|
||||||
if (!(_7 instanceof Array)) {
|
if (!(_15 instanceof Array)) {
|
||||||
_8 = Object.keys(_7);
|
_16 = Object.keys(_15);
|
||||||
_9 = Object.values(_7);
|
_17 = Object.values(_15);
|
||||||
}
|
}
|
||||||
var _length8 = _8.length;
|
var _length16 = _16.length;
|
||||||
for (let i = 0; i < _length8; i++) {
|
for (let i = 0; i < _length16; i++) {
|
||||||
context.subtree_first = i === 0;
|
context.subtree_first = i === 0;
|
||||||
scope.subtree_first = context.subtree_first;
|
scope.subtree_first = context.subtree_first;
|
||||||
context.subtree_last = i === _length8 - 1;
|
context.subtree_last = i === _length16 - 1;
|
||||||
scope.subtree_last = context.subtree_last;
|
scope.subtree_last = context.subtree_last;
|
||||||
context.subtree_index = i;
|
context.subtree_index = i;
|
||||||
scope.subtree_index = context.subtree_index;
|
scope.subtree_index = context.subtree_index;
|
||||||
context.subtree = _8[i];
|
context.subtree = _16[i];
|
||||||
scope.subtree = context.subtree;
|
scope.subtree = context.subtree;
|
||||||
context.subtree_value = _9[i];
|
context.subtree_value = _17[i];
|
||||||
scope.subtree_value = context.subtree_value;
|
scope.subtree_value = context.subtree_value;
|
||||||
this.recursiveFns['__10'].call(this, context, Object.assign({}, extra, {parentNode: c4, vars: {_v0: context['subtree']}, scope}));
|
this.recursiveFns['__10'].call(this, context, Object.assign({}, extra, {parentNode: c12, fiber: {vars: {_v0: context['subtree']}, scope}}));
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
@@ -1118,6 +1178,21 @@ exports[`t-call (template calling t-call with t-if 1`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`t-call (template calling t-call, global templates 1`] = `
|
||||||
|
"function anonymous(context,extra
|
||||||
|
) {
|
||||||
|
var h = this.h;
|
||||||
|
let c1 = [], p1 = {key:1};
|
||||||
|
var vn1 = h('div', p1, c1);
|
||||||
|
result = vn1;
|
||||||
|
let c2 = [], p2 = {key:2};
|
||||||
|
var vn2 = h('span', p2, c2);
|
||||||
|
c1.push(vn2);
|
||||||
|
c2.push({text: \`desk\`});
|
||||||
|
return vn1;
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`t-call (template calling with unused body 1`] = `
|
exports[`t-call (template calling with unused body 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
@@ -1840,6 +1915,7 @@ exports[`t-raw variable 1`] = `
|
|||||||
exports[`t-ref can get a dynamic ref on a node 1`] = `
|
exports[`t-ref can get a dynamic ref on a node 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
|
context.__owl__.refs = context.__owl__.refs || {};
|
||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
@@ -1850,7 +1926,7 @@ exports[`t-ref can get a dynamic ref on a node 1`] = `
|
|||||||
const ref3 = \`myspan\${context['id']}\`;
|
const ref3 = \`myspan\${context['id']}\`;
|
||||||
p2.hook = {
|
p2.hook = {
|
||||||
create: (_, n) => {
|
create: (_, n) => {
|
||||||
context.refs[ref3] = n.elm;
|
context.__owl__.refs[ref3] = n.elm;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return vn1;
|
return vn1;
|
||||||
@@ -1860,6 +1936,7 @@ exports[`t-ref can get a dynamic ref on a node 1`] = `
|
|||||||
exports[`t-ref can get a ref on a node 1`] = `
|
exports[`t-ref can get a ref on a node 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
|
context.__owl__.refs = context.__owl__.refs || {};
|
||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
@@ -1870,7 +1947,7 @@ exports[`t-ref can get a ref on a node 1`] = `
|
|||||||
const ref3 = \`myspan\`;
|
const ref3 = \`myspan\`;
|
||||||
p2.hook = {
|
p2.hook = {
|
||||||
create: (_, n) => {
|
create: (_, n) => {
|
||||||
context.refs[ref3] = n.elm;
|
context.__owl__.refs[ref3] = n.elm;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return vn1;
|
return vn1;
|
||||||
@@ -1880,6 +1957,7 @@ exports[`t-ref can get a ref on a node 1`] = `
|
|||||||
exports[`t-ref refs in a loop 1`] = `
|
exports[`t-ref refs in a loop 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
|
context.__owl__.refs = context.__owl__.refs || {};
|
||||||
context = Object.create(context);
|
context = Object.create(context);
|
||||||
var h = this.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
@@ -1906,7 +1984,7 @@ exports[`t-ref refs in a loop 1`] = `
|
|||||||
const ref6 = (context['item']);
|
const ref6 = (context['item']);
|
||||||
p5.hook = {
|
p5.hook = {
|
||||||
create: (_, n) => {
|
create: (_, n) => {
|
||||||
context.refs[ref6] = n.elm;
|
context.__owl__.refs[ref6] = n.elm;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
var _7 = context['item'];
|
var _7 = context['item'];
|
||||||
|
|||||||
+42
-5
@@ -49,6 +49,11 @@ describe("static templates", () => {
|
|||||||
qweb.addTemplate("test", "<div><span>word</span></div>");
|
qweb.addTemplate("test", "<div><span>word</span></div>");
|
||||||
expect(renderToString(qweb, "test")).toBe("<div><span>word</span></div>");
|
expect(renderToString(qweb, "test")).toBe("<div><span>word</span></div>");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("ignore comments", () => {
|
||||||
|
qweb.addTemplate("test", "<div>hello <!-- comment-->owl</div>");
|
||||||
|
expect(renderToString(qweb, "test")).toBe("<div>hello owl</div>");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("error handling", () => {
|
describe("error handling", () => {
|
||||||
@@ -631,7 +636,9 @@ describe("t-call (template calling", () => {
|
|||||||
`);
|
`);
|
||||||
const root = { val: "a", children: [{ val: "b" }, { val: "c" }] };
|
const root = { val: "a", children: [{ val: "b" }, { val: "c" }] };
|
||||||
const expected = "<div><div><p>a</p><div><p>b</p></div><div><p>c</p></div></div></div>";
|
const expected = "<div><div><p>a</p><div><p>b</p></div><div><p>c</p></div></div></div>";
|
||||||
expect(renderToString(qweb, "Parent", { root })).toBe(expected);
|
expect(renderToString(qweb, "Parent", { root }, { fiber: { vars: {}, scope: {} } })).toBe(
|
||||||
|
expected
|
||||||
|
);
|
||||||
const recursiveFn = Object.values(qweb.recursiveFns)[0];
|
const recursiveFn = Object.values(qweb.recursiveFns)[0];
|
||||||
expect(recursiveFn.toString()).toMatchSnapshot();
|
expect(recursiveFn.toString()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
@@ -658,10 +665,17 @@ describe("t-call (template calling", () => {
|
|||||||
const root = { val: "a", children: [{ val: "b", children: [{ val: "d" }] }, { val: "c" }] };
|
const root = { val: "a", children: [{ val: "b", children: [{ val: "d" }] }, { val: "c" }] };
|
||||||
const expected =
|
const expected =
|
||||||
"<div><div><p>a</p><div><p>b</p><div><p>d</p></div></div><div><p>c</p></div></div></div>";
|
"<div><div><p>a</p><div><p>b</p><div><p>d</p></div></div><div><p>c</p></div></div></div>";
|
||||||
expect(renderToString(qweb, "Parent", { root })).toBe(expected);
|
expect(renderToString(qweb, "Parent", { root }, { fiber: {} })).toBe(expected);
|
||||||
const recursiveFn = Object.values(qweb.recursiveFns)[0];
|
const recursiveFn = Object.values(qweb.recursiveFns)[0];
|
||||||
expect(recursiveFn.toString()).toMatchSnapshot();
|
expect(recursiveFn.toString()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("t-call, global templates", () => {
|
||||||
|
QWeb.registerTemplate("abcd", '<div><t t-call="john"/></div>');
|
||||||
|
qweb.addTemplate("john", `<span>desk</span>`);
|
||||||
|
const expected = "<div><span>desk</span></div>";
|
||||||
|
expect(trim(renderToString(qweb, "abcd"))).toBe(expected);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("foreach", () => {
|
describe("foreach", () => {
|
||||||
@@ -1078,14 +1092,14 @@ describe("t-ref", () => {
|
|||||||
test("can get a ref on a node", () => {
|
test("can get a ref on a node", () => {
|
||||||
qweb.addTemplate("test", `<div><span t-ref="myspan"/></div>`);
|
qweb.addTemplate("test", `<div><span t-ref="myspan"/></div>`);
|
||||||
let refs: any = {};
|
let refs: any = {};
|
||||||
renderToDOM(qweb, "test", { refs });
|
renderToDOM(qweb, "test", { __owl__: { refs } });
|
||||||
expect(refs.myspan.tagName).toBe("SPAN");
|
expect(refs.myspan.tagName).toBe("SPAN");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can get a dynamic ref on a node", () => {
|
test("can get a dynamic ref on a node", () => {
|
||||||
qweb.addTemplate("test", `<div><span t-ref="myspan{{id}}"/></div>`);
|
qweb.addTemplate("test", `<div><span t-ref="myspan{{id}}"/></div>`);
|
||||||
let refs: any = {};
|
let refs: any = {};
|
||||||
renderToDOM(qweb, "test", { refs, id: 3 });
|
renderToDOM(qweb, "test", { id: 3, __owl__: { refs } });
|
||||||
expect(refs.myspan3.tagName).toBe("SPAN");
|
expect(refs.myspan3.tagName).toBe("SPAN");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1100,7 +1114,7 @@ describe("t-ref", () => {
|
|||||||
</div>`
|
</div>`
|
||||||
);
|
);
|
||||||
let refs: any = {};
|
let refs: any = {};
|
||||||
renderToDOM(qweb, "test", { refs, items: [1, 2, 3] });
|
renderToDOM(qweb, "test", { items: [1, 2, 3], __owl__: { refs } });
|
||||||
expect(Object.keys(refs)).toEqual(["1", "2", "3"]);
|
expect(Object.keys(refs)).toEqual(["1", "2", "3"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1298,3 +1312,26 @@ describe("global template registration", () => {
|
|||||||
expect((vnode as any).children[0].text).toBe("global");
|
expect((vnode as any).children[0].text).toBe("global");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("properly support svg", () => {
|
||||||
|
test("add proper namespace to svg", () => {
|
||||||
|
qweb.addTemplate(
|
||||||
|
"test",
|
||||||
|
`<svg width="100px" height="90px"><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/> </svg>`
|
||||||
|
);
|
||||||
|
expect(renderToString(qweb, "test")).toBe(
|
||||||
|
`<svg width=\"100px\" height=\"90px\"><circle cx=\"50\" cy=\"50\" r=\"4\" stroke=\"green\" stroke-width=\"1\" fill=\"yellow\"></circle> </svg>`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("add proper namespace to g tags", () => {
|
||||||
|
// this is necessary if one wants to use components in a svg
|
||||||
|
qweb.addTemplate(
|
||||||
|
"test",
|
||||||
|
`<g><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/> </g>`
|
||||||
|
);
|
||||||
|
expect(renderToString(qweb, "test")).toBe(
|
||||||
|
`<g><circle cx=\"50\" cy=\"50\" r=\"4\" stroke=\"green\" stroke-width=\"1\" fill=\"yellow\"></circle> </g>`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ describe("Link component", () => {
|
|||||||
</div>
|
</div>
|
||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
class App extends Component<any, any, any> {
|
class App extends Component<any, any> {
|
||||||
static components = { Link: Link };
|
static components = { Link: Link };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ describe("Link component", () => {
|
|||||||
</div>
|
</div>
|
||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
class App extends Component<any, any, any> {
|
class App extends Component<any, any> {
|
||||||
static components = { Link: Link };
|
static components = { Link: Link };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ describe("RouteComponent", () => {
|
|||||||
<span t-name="Users">Users</span>
|
<span t-name="Users">Users</span>
|
||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
class About extends Component<any, any, any> {}
|
class About extends Component<any, any> {}
|
||||||
class Users extends Component<any, any, any> {}
|
class Users extends Component<any, any> {}
|
||||||
class App extends Component<any, any, any> {
|
class App extends Component<any, any> {
|
||||||
static components = { RouteComponent };
|
static components = { RouteComponent };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,8 +64,8 @@ describe("RouteComponent", () => {
|
|||||||
<span t-name="Book">Book <t t-esc="props.title"/></span>
|
<span t-name="Book">Book <t t-esc="props.title"/></span>
|
||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
class Book extends Component<any, any, any> {}
|
class Book extends Component<any, any> {}
|
||||||
class App extends Component<any, any, any> {
|
class App extends Component<any, any> {
|
||||||
static components = { RouteComponent };
|
static components = { RouteComponent };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,12 +86,12 @@ describe("RouteComponent", () => {
|
|||||||
<span t-name="Book">Book <t t-esc="props.title"/>|<t t-esc="incVal"/></span>
|
<span t-name="Book">Book <t t-esc="props.title"/>|<t t-esc="incVal"/></span>
|
||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
class Book extends Component<any, any, any> {
|
class Book extends Component<any, any> {
|
||||||
get incVal() {
|
get incVal() {
|
||||||
return this.props.val + 1;
|
return this.props.val + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class App extends Component<any, any, any> {
|
class App extends Component<any, any> {
|
||||||
static components = { RouteComponent };
|
static components = { RouteComponent };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,52 +8,37 @@ exports[`RouteComponent can render simple cases 1`] = `
|
|||||||
let parent = context;
|
let parent = context;
|
||||||
let owner = context;
|
let owner = context;
|
||||||
let result;
|
let result;
|
||||||
context = Object.create(context);
|
|
||||||
var h = this.h;
|
var h = this.h;
|
||||||
var _1 = context['routes'];
|
if (context['routeComponent']) {
|
||||||
if (!_1) { throw new Error('QWeb error: Invalid loop expression')}
|
|
||||||
var _2 = _3 = _1;
|
|
||||||
if (!(_1 instanceof Array)) {
|
|
||||||
_2 = Object.keys(_1);
|
|
||||||
_3 = Object.values(_1);
|
|
||||||
}
|
|
||||||
var _length2 = _2.length;
|
|
||||||
for (let i = 0; i < _length2; i++) {
|
|
||||||
context.route_first = i === 0;
|
|
||||||
context.route_last = i === _length2 - 1;
|
|
||||||
context.route_index = i;
|
|
||||||
context.route = _2[i];
|
|
||||||
context.route_value = _3[i];
|
|
||||||
if (context['env'].router.currentRouteName===context['route'].name) {
|
|
||||||
//COMPONENT
|
//COMPONENT
|
||||||
let def5;
|
let key4 = 'key' + context['env'].router.currentRouteName;
|
||||||
let templateId7 = String(-6 - i);
|
let def2;
|
||||||
let w6 = templateId7 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId7]] : false;
|
let templateId5 = key4;
|
||||||
let vn8 = {};
|
let w3 = templateId5 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId5]] : false;
|
||||||
result = vn8;
|
let vn6 = {};
|
||||||
let props6 = Object.assign({}, context['env'].router.currentParams);
|
result = vn6;
|
||||||
if (w6 && w6.__owl__.renderPromise && !w6.__owl__.vnode) {
|
let props3 = Object.assign({}, context['env'].router.currentParams);
|
||||||
if (utils.shallowEqual(props6, w6.__owl__.renderProps)) {
|
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
|
||||||
def5 = w6.__owl__.renderPromise;
|
if (utils.shallowEqual(props3, w3.__owl__.currentFiber.props)) {
|
||||||
|
def2 = w3.__owl__.currentFiber.promise;
|
||||||
} else {
|
} else {
|
||||||
w6.destroy();
|
w3.destroy();
|
||||||
w6 = false;
|
w3 = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!w6) {
|
if (!w3) {
|
||||||
let componentKey6 = (context['route'].component);
|
let componentKey3 = \`routeComponent\`;
|
||||||
let W6 = context.constructor.components[componentKey6] || QWeb.components[componentKey6];
|
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['routeComponent'];
|
||||||
if (!W6) {throw new Error('Cannot find the definition of component \\"' + componentKey6 + '\\"')}
|
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
|
||||||
w6 = new W6(parent, props6);
|
w3 = new W3(parent, props3);
|
||||||
parent.__owl__.cmap[templateId7] = w6.__owl__.id;
|
parent.__owl__.cmap[templateId5] = w3.__owl__.id;
|
||||||
def5 = w6.__prepare();
|
def2 = w3.__prepare(extra.fiber, undefined, undefined);
|
||||||
def5 = def5.then(vnode=>{if (w6.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: templateId7, hook: {insert(vn) {let nvn=w6.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w6.destroy();}}});utils.defineProxy(vn8, pvnode);w6.__owl__.pvnode = pvnode;});
|
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 {
|
} else {
|
||||||
def5 = def5 || w6.__updateProps(props6, extra.forceUpdate, extra.patchQueue);
|
def2 = def2 || w3.__updateProps(props3, extra.fiber, undefined, undefined);
|
||||||
def5 = def5.then(()=>{if (w6.__owl__.isDestroyed) {return};let pvnode=w6.__owl__.pvnode;utils.defineProxy(vn8, pvnode);});
|
def2 = def2.then(()=>{if (w3.__owl__.isDestroyed) {return};let pvnode=w3.__owl__.pvnode;utils.defineProxy(vn6, pvnode);});
|
||||||
}
|
|
||||||
extra.promises.push(def5);
|
|
||||||
}
|
}
|
||||||
|
extra.promises.push(def2);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}"
|
}"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Component, Env } from "../../src/component/component";
|
import { Component, Env } from "../../src/component/component";
|
||||||
import { ConnectedComponent } from "../../src/store/connected_component";
|
import { ConnectedComponent } from "../../src/store/connected_component";
|
||||||
import { Store } from "../../src/store/store";
|
import { Store } from "../../src/store/store";
|
||||||
|
import { useState } from "../../src/hooks";
|
||||||
import { makeTestEnv, makeTestFixture, nextTick, makeDeferred } from "../helpers";
|
import { makeTestEnv, makeTestFixture, nextTick, makeDeferred } from "../helpers";
|
||||||
|
|
||||||
describe("connecting a component to store", () => {
|
describe("connecting a component to store", () => {
|
||||||
@@ -27,8 +28,8 @@ describe("connecting a component to store", () => {
|
|||||||
<span t-name="Todo"><t t-esc="props.msg"/></span>
|
<span t-name="Todo"><t t-esc="props.msg"/></span>
|
||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
class Todo extends Component<any, any, any> {}
|
class Todo extends Component<any, any> {}
|
||||||
class App extends ConnectedComponent<any, any, any> {
|
class App extends ConnectedComponent<any, any> {
|
||||||
static components = { Todo };
|
static components = { Todo };
|
||||||
static mapStoreToProps(s) {
|
static mapStoreToProps(s) {
|
||||||
return { todos: s.todos };
|
return { todos: s.todos };
|
||||||
@@ -70,7 +71,7 @@ describe("connecting a component to store", () => {
|
|||||||
};
|
};
|
||||||
const store = new Store({ state, actions });
|
const store = new Store({ state, actions });
|
||||||
|
|
||||||
class App extends ConnectedComponent<any, any, any> {
|
class App extends ConnectedComponent<any, any> {
|
||||||
static mapStoreToProps(s) {
|
static mapStoreToProps(s) {
|
||||||
return { todos: s.todos };
|
return { todos: s.todos };
|
||||||
}
|
}
|
||||||
@@ -111,7 +112,7 @@ describe("connecting a component to store", () => {
|
|||||||
<span t-name="Todo"><t t-esc="props.msg"/></span>
|
<span t-name="Todo"><t t-esc="props.msg"/></span>
|
||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
class Todo extends Component<any, any, any> {}
|
class Todo extends Component<any, any> {}
|
||||||
|
|
||||||
const store = new Store({
|
const store = new Store({
|
||||||
state: { todos: [] },
|
state: { todos: [] },
|
||||||
@@ -121,7 +122,7 @@ describe("connecting a component to store", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
class App extends ConnectedComponent<any, any, any> {
|
class App extends ConnectedComponent<any, any> {
|
||||||
static components = { Todo };
|
static components = { Todo };
|
||||||
static mapStoreToProps(s) {
|
static mapStoreToProps(s) {
|
||||||
return { todos: s.todos };
|
return { todos: s.todos };
|
||||||
@@ -160,7 +161,7 @@ describe("connecting a component to store", () => {
|
|||||||
});
|
});
|
||||||
(<any>env).store = store;
|
(<any>env).store = store;
|
||||||
|
|
||||||
class App extends ConnectedComponent<any, any, any> {
|
class App extends ConnectedComponent<any, any> {
|
||||||
static mapStoreToProps(s) {
|
static mapStoreToProps(s) {
|
||||||
return { value: s.value };
|
return { value: s.value };
|
||||||
}
|
}
|
||||||
@@ -187,7 +188,7 @@ describe("connecting a component to store", () => {
|
|||||||
<div t-name="Child"/>
|
<div t-name="Child"/>
|
||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
class Child extends ConnectedComponent<any, any, any> {
|
class Child extends ConnectedComponent<any, any> {
|
||||||
static mapStoreToProps(s) {
|
static mapStoreToProps(s) {
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
@@ -199,12 +200,12 @@ describe("connecting a component to store", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Parent extends Component<any, any, any> {
|
class Parent extends Component<any, any> {
|
||||||
static components = { Child };
|
static components = { Child };
|
||||||
|
state: any;
|
||||||
constructor(env: Env) {
|
constructor(env: Env) {
|
||||||
super(env);
|
super(env);
|
||||||
this.state = { child: true };
|
this.state = useState({ child: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,14 +241,14 @@ describe("connecting a component to store", () => {
|
|||||||
</div>
|
</div>
|
||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
class TodoItem extends ConnectedComponent<any, any, any> {
|
class TodoItem extends ConnectedComponent<any, any> {
|
||||||
static mapStoreToProps(state, props) {
|
static mapStoreToProps(state, props) {
|
||||||
const todo = state.todos.find(t => t.id === props.id);
|
const todo = state.todos.find(t => t.id === props.id);
|
||||||
return todo;
|
return todo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TodoList extends ConnectedComponent<any, any, any> {
|
class TodoList extends ConnectedComponent<any, any> {
|
||||||
static components = { TodoItem };
|
static components = { TodoItem };
|
||||||
static mapStoreToProps(state) {
|
static mapStoreToProps(state) {
|
||||||
return { todos: state.todos };
|
return { todos: state.todos };
|
||||||
@@ -294,7 +295,7 @@ describe("connecting a component to store", () => {
|
|||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
class TodoItem extends ConnectedComponent<any, any, any> {
|
class TodoItem extends ConnectedComponent<any, any> {
|
||||||
static mapStoreToProps(state, props, getters) {
|
static mapStoreToProps(state, props, getters) {
|
||||||
const todo = state.todos.find(t => t.id === props.id);
|
const todo = state.todos.find(t => t.id === props.id);
|
||||||
return {
|
return {
|
||||||
@@ -304,7 +305,7 @@ describe("connecting a component to store", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TodoList extends ConnectedComponent<any, any, any> {
|
class TodoList extends ConnectedComponent<any, any> {
|
||||||
static components = { TodoItem };
|
static components = { TodoItem };
|
||||||
static mapStoreToProps(state) {
|
static mapStoreToProps(state) {
|
||||||
return { todos: state.todos };
|
return { todos: state.todos };
|
||||||
@@ -330,15 +331,15 @@ describe("connecting a component to store", () => {
|
|||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
class Beer extends ConnectedComponent<any, any, any> {
|
class Beer extends ConnectedComponent<any, any> {
|
||||||
static mapStoreToProps(state, props) {
|
static mapStoreToProps(state, props) {
|
||||||
return state.beers[props.id];
|
return state.beers[props.id];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class App extends Component<any, any, any> {
|
class App extends Component<any, any> {
|
||||||
static components = { Beer };
|
static components = { Beer };
|
||||||
state = { beerId: 1 };
|
state = useState({ beerId: 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = { beers: { 1: { name: "jupiler" }, 2: { name: "kwak" } } };
|
const state = { beers: { 1: { name: "jupiler" }, 2: { name: "kwak" } } };
|
||||||
@@ -363,7 +364,7 @@ describe("connecting a component to store", () => {
|
|||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
class App extends ConnectedComponent<any, any, any> {
|
class App extends ConnectedComponent<any, any> {
|
||||||
static mapStoreToProps(state) {
|
static mapStoreToProps(state) {
|
||||||
return { beers: state.beers, otherKey: 1 };
|
return { beers: state.beers, otherKey: 1 };
|
||||||
}
|
}
|
||||||
@@ -403,7 +404,7 @@ describe("connecting a component to store", () => {
|
|||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
class Beer extends ConnectedComponent<any, any, any> {
|
class Beer extends ConnectedComponent<any, any> {
|
||||||
static mapStoreToProps(state, props) {
|
static mapStoreToProps(state, props) {
|
||||||
return {
|
return {
|
||||||
selected: state.beers[props.id],
|
selected: state.beers[props.id],
|
||||||
@@ -413,9 +414,9 @@ describe("connecting a component to store", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class App extends Component<any, any, any> {
|
class App extends Component<any, any> {
|
||||||
static components = { Beer };
|
static components = { Beer };
|
||||||
state = { beerId: 0 };
|
state = useState({ beerId: 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
@@ -470,7 +471,7 @@ describe("connecting a component to store", () => {
|
|||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
class Beer extends ConnectedComponent<any, any, any> {
|
class Beer extends ConnectedComponent<any, any> {
|
||||||
static mapStoreToProps(storeState, props) {
|
static mapStoreToProps(storeState, props) {
|
||||||
return {
|
return {
|
||||||
selected: storeState.beers[props.id],
|
selected: storeState.beers[props.id],
|
||||||
@@ -480,9 +481,9 @@ describe("connecting a component to store", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class App extends Component<any, any, any> {
|
class App extends Component<any, any> {
|
||||||
static components = { Beer };
|
static components = { Beer };
|
||||||
state = { beerId: 0 };
|
state = useState({ beerId: 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
@@ -559,13 +560,13 @@ describe("connecting a component to store", () => {
|
|||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
class Child extends ConnectedComponent<any, any, any> {
|
class Child extends ConnectedComponent<any, any> {
|
||||||
static mapStoreToProps(s, props) {
|
static mapStoreToProps(s, props) {
|
||||||
steps.push("child");
|
steps.push("child");
|
||||||
return { msg: s.msg[props.key] };
|
return { msg: s.msg[props.key] };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class Parent extends ConnectedComponent<any, any, any> {
|
class Parent extends ConnectedComponent<any, any> {
|
||||||
static components = { Child };
|
static components = { Child };
|
||||||
static mapStoreToProps(s) {
|
static mapStoreToProps(s) {
|
||||||
steps.push("parent");
|
steps.push("parent");
|
||||||
@@ -608,13 +609,13 @@ describe("connecting a component to store", () => {
|
|||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
class Child extends ConnectedComponent<any, any, any> {
|
class Child extends ConnectedComponent<any, any> {
|
||||||
static mapStoreToProps(s, props) {
|
static mapStoreToProps(s, props) {
|
||||||
steps.push("child");
|
steps.push("child");
|
||||||
return { msg: s.messages[props.someId] };
|
return { msg: s.messages[props.someId] };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class Parent extends ConnectedComponent<any, any, any> {
|
class Parent extends ConnectedComponent<any, any> {
|
||||||
static components = { Child };
|
static components = { Child };
|
||||||
static mapStoreToProps(s) {
|
static mapStoreToProps(s) {
|
||||||
steps.push("parent");
|
steps.push("parent");
|
||||||
@@ -686,7 +687,7 @@ describe("connecting a component to store", () => {
|
|||||||
let renderCount = 0;
|
let renderCount = 0;
|
||||||
let fCount = 0;
|
let fCount = 0;
|
||||||
|
|
||||||
class TodoItem extends ConnectedComponent<any, any, any> {
|
class TodoItem extends ConnectedComponent<any, any> {
|
||||||
state = { isEditing: false };
|
state = { isEditing: false };
|
||||||
static mapStoreToProps(state, ownProps) {
|
static mapStoreToProps(state, ownProps) {
|
||||||
fCount++;
|
fCount++;
|
||||||
@@ -698,12 +699,12 @@ describe("connecting a component to store", () => {
|
|||||||
editTodo() {
|
editTodo() {
|
||||||
this.env.store.dispatch("editTodo");
|
this.env.store.dispatch("editTodo");
|
||||||
}
|
}
|
||||||
__render(...args) {
|
__render(f) {
|
||||||
renderCount++;
|
renderCount++;
|
||||||
return super.__render(...args);
|
return super.__render(f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class TodoApp extends ConnectedComponent<any, any, any> {
|
class TodoApp extends ConnectedComponent<any, any> {
|
||||||
static components = { TodoItem };
|
static components = { TodoItem };
|
||||||
static mapStoreToProps(state) {
|
static mapStoreToProps(state) {
|
||||||
return {
|
return {
|
||||||
@@ -764,7 +765,7 @@ describe("connecting a component to store", () => {
|
|||||||
let renderCount = 0;
|
let renderCount = 0;
|
||||||
let fCount = 0;
|
let fCount = 0;
|
||||||
|
|
||||||
class TodoItem extends ConnectedComponent<any, any, any> {
|
class TodoItem extends ConnectedComponent<any, any> {
|
||||||
state = { isEditing: false };
|
state = { isEditing: false };
|
||||||
|
|
||||||
static mapStoreToProps(state, ownProps) {
|
static mapStoreToProps(state, ownProps) {
|
||||||
@@ -776,13 +777,13 @@ describe("connecting a component to store", () => {
|
|||||||
removeTodo() {
|
removeTodo() {
|
||||||
this.env.store.dispatch("removeTodo");
|
this.env.store.dispatch("removeTodo");
|
||||||
}
|
}
|
||||||
__render(...args) {
|
__render(f) {
|
||||||
renderCount++;
|
renderCount++;
|
||||||
return super.__render(...args);
|
return super.__render(f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TodoApp extends ConnectedComponent<any, any, any> {
|
class TodoApp extends ConnectedComponent<any, any> {
|
||||||
static components = { TodoItem };
|
static components = { TodoItem };
|
||||||
static mapStoreToProps(state) {
|
static mapStoreToProps(state) {
|
||||||
return {
|
return {
|
||||||
@@ -817,7 +818,7 @@ describe("connecting a component to store", () => {
|
|||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
class App extends ConnectedComponent<any, any, any> {
|
class App extends ConnectedComponent<any, any> {
|
||||||
static mapStoreToProps(s) {
|
static mapStoreToProps(s) {
|
||||||
return { msg: s.msg };
|
return { msg: s.msg };
|
||||||
}
|
}
|
||||||
@@ -871,11 +872,11 @@ describe("connected components and default values", () => {
|
|||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
class Greeter extends ConnectedComponent<any, any, any> {
|
class Greeter extends ConnectedComponent<any, any> {
|
||||||
static defaultProps = { recipient: "John" };
|
static defaultProps = { recipient: "John" };
|
||||||
}
|
}
|
||||||
|
|
||||||
class App extends Component<any, any, any> {
|
class App extends Component<any, any> {
|
||||||
static components = { Greeter };
|
static components = { Greeter };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -895,11 +896,11 @@ describe("connected components and default values", () => {
|
|||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
class Greeter extends ConnectedComponent<any, any, any> {
|
class Greeter extends ConnectedComponent<any, any> {
|
||||||
static defaultProps = { recipient: "John" };
|
static defaultProps = { recipient: "John" };
|
||||||
}
|
}
|
||||||
|
|
||||||
class App extends Component<any, any, any> {
|
class App extends Component<any, any> {
|
||||||
static components = { Greeter };
|
static components = { Greeter };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -910,11 +911,12 @@ describe("connected components and default values", () => {
|
|||||||
await app.mount(fixture);
|
await app.mount(fixture);
|
||||||
expect(fixture.innerHTML).toBe("<div><div>Hello, John</div></div>");
|
expect(fixture.innerHTML).toBe("<div><div>Hello, John</div></div>");
|
||||||
|
|
||||||
await app.__updateProps({ initialRecipient: "James" }, true);
|
const fiber = app.__createFiber(true, undefined, undefined, undefined);
|
||||||
|
await app.__updateProps({ initialRecipient: "James" }, fiber);
|
||||||
await app.render();
|
await app.render();
|
||||||
expect(fixture.innerHTML).toBe("<div><div>Hello, James</div></div>");
|
expect(fixture.innerHTML).toBe("<div><div>Hello, James</div></div>");
|
||||||
|
|
||||||
await app.__updateProps({ initialRecipient: undefined }, true);
|
await app.__updateProps({ initialRecipient: undefined }, fiber);
|
||||||
await app.render();
|
await app.render();
|
||||||
expect(fixture.innerHTML).toBe("<div><div>Hello, John</div></div>");
|
expect(fixture.innerHTML).toBe("<div><div>Hello, John</div></div>");
|
||||||
});
|
});
|
||||||
@@ -935,7 +937,7 @@ describe("connected components and default values", () => {
|
|||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
class Message extends ConnectedComponent<any, any, any> {
|
class Message extends ConnectedComponent<any, any> {
|
||||||
static defaultProps = { showId: true };
|
static defaultProps = { showId: true };
|
||||||
static mapStoreToProps = function(state, ownProps) {
|
static mapStoreToProps = function(state, ownProps) {
|
||||||
return {
|
return {
|
||||||
@@ -944,7 +946,7 @@ describe("connected components and default values", () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
class Thread extends ConnectedComponent<any, any, any> {
|
class Thread extends ConnectedComponent<any, any> {
|
||||||
static components = { Message };
|
static components = { Message };
|
||||||
static defaultProps = { showMessages: true };
|
static defaultProps = { showMessages: true };
|
||||||
static mapStoreToProps = function(state, ownProps) {
|
static mapStoreToProps = function(state, ownProps) {
|
||||||
@@ -955,7 +957,7 @@ describe("connected components and default values", () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
class App extends Component<any, any, any> {
|
class App extends Component<any, any> {
|
||||||
static components = { Thread };
|
static components = { Thread };
|
||||||
static defaultProps = { threadId: 1 };
|
static defaultProps = { threadId: 1 };
|
||||||
}
|
}
|
||||||
@@ -997,7 +999,8 @@ describe("connected components and default values", () => {
|
|||||||
"<div><div><div>100Message100</div><div>101Message101</div></div></div>"
|
"<div><div><div>100Message100</div><div>101Message101</div></div></div>"
|
||||||
);
|
);
|
||||||
|
|
||||||
await app.__updateProps({ threadId: 2 }, true);
|
const fiber = app.__createFiber(true, undefined, undefined, undefined);
|
||||||
|
await app.__updateProps({ threadId: 2 }, fiber);
|
||||||
await app.render();
|
await app.render();
|
||||||
expect(fixture.innerHTML).toBe("<div><div><div>200Message200</div></div></div>");
|
expect(fixture.innerHTML).toBe("<div><div><div>200Message200</div></div></div>");
|
||||||
|
|
||||||
@@ -1016,15 +1019,15 @@ describe("connected components and default values", () => {
|
|||||||
<div t-name="Child"><t t-esc="storeProps.val"/></div>
|
<div t-name="Child"><t t-esc="storeProps.val"/></div>
|
||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
class Child extends ConnectedComponent<any, any, any> {
|
class Child extends ConnectedComponent<any, any> {
|
||||||
static mapStoreToProps(s) {
|
static mapStoreToProps(s) {
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Parent extends Component<any, any, any> {
|
class Parent extends Component<any, any> {
|
||||||
static components = { Child };
|
static components = { Child };
|
||||||
state = { child: true };
|
state = useState({ child: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestStore extends Store {
|
class TestStore extends Store {
|
||||||
@@ -1060,7 +1063,7 @@ describe("connected components and default values", () => {
|
|||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
class App extends ConnectedComponent<any, any, any> {
|
class App extends ConnectedComponent<any, any> {
|
||||||
static mapStoreToProps = function(state) {
|
static mapStoreToProps = function(state) {
|
||||||
return {
|
return {
|
||||||
counter: state.counter
|
counter: state.counter
|
||||||
@@ -1141,14 +1144,14 @@ describe("various scenarios", () => {
|
|||||||
</div>
|
</div>
|
||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
class Attachment extends ConnectedComponent<any, any, any> {
|
class Attachment extends ConnectedComponent<any, any> {
|
||||||
static mapStoreToProps(state, ownProps) {
|
static mapStoreToProps(state, ownProps) {
|
||||||
return {
|
return {
|
||||||
name: state.attachments[ownProps.id].name
|
name: state.attachments[ownProps.id].name
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class Message extends ConnectedComponent<any, any, any> {
|
class Message extends ConnectedComponent<any, any> {
|
||||||
static mapStoreToProps(state) {
|
static mapStoreToProps(state) {
|
||||||
return {
|
return {
|
||||||
attachmentIds: state.messages[10].attachmentIds
|
attachmentIds: state.messages[10].attachmentIds
|
||||||
|
|||||||
+7
-11
@@ -1,5 +1,5 @@
|
|||||||
import { h, patch } from "../src/vdom";
|
import { h, patch } from "../src/vdom";
|
||||||
import { init } from "../src/vdom/vdom";
|
import { init, addNS } from "../src/vdom/vdom";
|
||||||
|
|
||||||
function map(list, fn) {
|
function map(list, fn) {
|
||||||
var ret: any[] = [];
|
var ret: any[] = [];
|
||||||
@@ -206,19 +206,15 @@ describe("snabbdom", function() {
|
|||||||
expect(elm.firstChild.namespaceURI).toBe(SVGNamespace);
|
expect(elm.firstChild.namespaceURI).toBe(SVGNamespace);
|
||||||
|
|
||||||
// verify that svg tag automatically gets svg namespace
|
// verify that svg tag automatically gets svg namespace
|
||||||
elm = patch(vnode0, h("svg", [h("foreignObject", [h("div", ["I am HTML embedded in SVG"])])]))
|
const vnode = h("svg", [h("foreignObject", [h("div", ["I am HTML embedded in SVG"])])]);
|
||||||
.elm;
|
// need to add namespace manually. it is usually done by the template
|
||||||
|
// compiler
|
||||||
|
addNS(vnode.data, (vnode as any).children, vnode.sel);
|
||||||
|
|
||||||
|
elm = patch(vnode0, vnode).elm;
|
||||||
expect(elm.namespaceURI).toBe(SVGNamespace);
|
expect(elm.namespaceURI).toBe(SVGNamespace);
|
||||||
expect(elm.firstChild.namespaceURI).toBe(SVGNamespace);
|
expect(elm.firstChild.namespaceURI).toBe(SVGNamespace);
|
||||||
expect(elm.firstChild.firstChild.namespaceURI).toBe(XHTMLNamespace);
|
expect(elm.firstChild.firstChild.namespaceURI).toBe(XHTMLNamespace);
|
||||||
|
|
||||||
// verify that svg tag with extra selectors gets svg namespace
|
|
||||||
elm = patch(vnode0, h("svg#some-id")).elm;
|
|
||||||
expect(elm.namespaceURI).toBe(SVGNamespace);
|
|
||||||
|
|
||||||
// verify that non-svg tag beginning with 'svg' does NOT get namespace
|
|
||||||
elm = patch(vnode0, h("svg-custom-el")).elm;
|
|
||||||
expect(elm.namespaceURI).not.toBe(SVGNamespace);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("receives classes in selector", function() {
|
test("receives classes in selector", function() {
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Likes Counter Widget
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
class Counter extends owl.Component {
|
||||||
|
state = { counter: 0 };
|
||||||
|
|
||||||
|
increment() {
|
||||||
|
this.state.counter++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Message Widget
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
class Message extends owl.Component {
|
||||||
|
static components = { Counter };
|
||||||
|
|
||||||
|
shouldUpdate(nextProps) {
|
||||||
|
return nextProps.message !== this.props.message;
|
||||||
|
}
|
||||||
|
removeMessage() {
|
||||||
|
this.trigger("remove-message", {
|
||||||
|
id: this.props.message.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Root Widget
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
class App extends owl.Component {
|
||||||
|
static components = { Message };
|
||||||
|
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
benchmark(message, fn, callback) {
|
||||||
|
if (this.state.multipleFlag) {
|
||||||
|
const N = 20;
|
||||||
|
let n = N;
|
||||||
|
let total = 0;
|
||||||
|
let cb = info => {
|
||||||
|
let finalize = () => {
|
||||||
|
n--;
|
||||||
|
total += info.delta;
|
||||||
|
if (n === 0) {
|
||||||
|
const avg = total / N;
|
||||||
|
this.log(`Average: ${formatNumber(avg)}ms`, true);
|
||||||
|
if (callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this._benchmark(message, fn, cb);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.state.clearAfterFlag) {
|
||||||
|
this._benchmark(
|
||||||
|
"clear",
|
||||||
|
() => {
|
||||||
|
this.state.messages = [];
|
||||||
|
},
|
||||||
|
finalize,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
finalize();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this._benchmark(message, fn, cb);
|
||||||
|
} else {
|
||||||
|
this._benchmark(message, fn, callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_benchmark(message, fn, cb, log = true) {
|
||||||
|
setTimeout(() => {
|
||||||
|
startMeasure(message);
|
||||||
|
fn();
|
||||||
|
stopMeasure(info => {
|
||||||
|
if (log) {
|
||||||
|
this.log(info.msg);
|
||||||
|
}
|
||||||
|
if (cb) {
|
||||||
|
cb(info);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
addMessages(n) {
|
||||||
|
this.benchmark("add " + n, () => {
|
||||||
|
const newMessages = buildData(n);
|
||||||
|
this.state.messages.push.apply(this.state.messages, newMessages);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this._benchmark("clear", () => {
|
||||||
|
this.state.messages = [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSomeMessages() {
|
||||||
|
this.benchmark("update every 10th", () => {
|
||||||
|
const messages = this.state.messages;
|
||||||
|
for (let i = 0; i < messages.length; i += 10) {
|
||||||
|
const msg = Object.assign({}, messages[i]);
|
||||||
|
msg.author += "!!!";
|
||||||
|
messages[i] = msg;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeMessage(event) {
|
||||||
|
this.benchmark("remove message", () => {
|
||||||
|
const index = this.state.messages.findIndex(m => m.id === event.detail.id);
|
||||||
|
this.state.messages.splice(index, 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
log(str, isBold) {
|
||||||
|
const div = document.createElement("div");
|
||||||
|
if (isBold) {
|
||||||
|
div.classList.add("bold");
|
||||||
|
}
|
||||||
|
div.textContent = `> ${str}`;
|
||||||
|
this.refs.log.appendChild(div);
|
||||||
|
this.refs.log.scrollTop = this.refs.log.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearLog() {
|
||||||
|
this.refs.log.innerHTML = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Application initialization
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
async function start() {
|
||||||
|
const templates = await owl.utils.loadTemplates("templates.xml");
|
||||||
|
const env = {
|
||||||
|
qweb: new owl.QWeb(templates)
|
||||||
|
};
|
||||||
|
const app = new App(env);
|
||||||
|
app.mount(document.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
start();
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>OWL 0.21.0 Benchmark</title>
|
||||||
|
<link href="../shared/main.css" rel="stylesheet"/>
|
||||||
|
<script src='owl.js'></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src='app.js' type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
|||||||
|
<templates>
|
||||||
|
<div t-name="App" class="main">
|
||||||
|
<div class="left-thing">
|
||||||
|
<div class="title">Actions</div>
|
||||||
|
<div class="panel">
|
||||||
|
<button t-on-click="addMessages(100)">Add 100 messages</button>
|
||||||
|
<button t-on-click="addMessages(1000)">Add 1k messages</button>
|
||||||
|
<button t-on-click="addMessages(10000)">Add 10k messages</button>
|
||||||
|
<button t-on-click="addMessages(30000)">Add 30k messages</button>
|
||||||
|
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
|
||||||
|
<button t-on-click="clear">Clear</button>
|
||||||
|
</div>
|
||||||
|
<div class="flags">
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" id="multipleflag" t-model="multipleFlag"/>
|
||||||
|
<label for="multipleflag">Do it 20x</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" id="clearFlag" t-model="clearAfterFlag" />
|
||||||
|
<label for="clearFlag">Clear after</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
|
||||||
|
<hr/>
|
||||||
|
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
|
||||||
|
<div class="log">
|
||||||
|
<div class="log-content" t-ref="log"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="right-thing">
|
||||||
|
<div class="content" t-on-remove-message="removeMessage">
|
||||||
|
<t t-foreach="state.messages" t-as="message">
|
||||||
|
<Message t-key="message.id" message="message"/>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div t-name="Message" class="message">
|
||||||
|
<span class="author"><t t-esc="props.message.author"/></span>
|
||||||
|
<span class="msg"><t t-esc="props.message.msg"/></span>
|
||||||
|
<button class="remove" t-on-click="removeMessage">Remove</button>
|
||||||
|
<Counter/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div t-name="Counter">
|
||||||
|
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</templates>
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
|
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
|
||||||
|
|
||||||
|
const { useState, useRef } = owl.hooks;
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Likes Counter Widget
|
// Likes Counter Widget
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
class Counter extends owl.Component {
|
class Counter extends owl.Component {
|
||||||
state = { counter: 0 };
|
state = useState({ counter: 0 });
|
||||||
|
|
||||||
increment() {
|
increment() {
|
||||||
this.state.counter++;
|
this.state.counter++;
|
||||||
@@ -32,7 +33,8 @@ class Message extends owl.Component {
|
|||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
class App extends owl.Component {
|
class App extends owl.Component {
|
||||||
static components = { Message };
|
static components = { Message };
|
||||||
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
|
state = useState({ messages: [], multipleFlag: false, clearAfterFlag: false });
|
||||||
|
logRef = useRef("log");
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
|
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
|
||||||
@@ -129,12 +131,12 @@ class App extends owl.Component {
|
|||||||
div.classList.add("bold");
|
div.classList.add("bold");
|
||||||
}
|
}
|
||||||
div.textContent = `> ${str}`;
|
div.textContent = `> ${str}`;
|
||||||
this.refs.log.appendChild(div);
|
this.logRef.el.appendChild(div);
|
||||||
this.refs.log.scrollTop = this.refs.log.scrollHeight;
|
this.logRef.el.scrollTop = this.logRef.el.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
clearLog() {
|
clearLog() {
|
||||||
this.refs.log.innerHTML = "";
|
this.logRef.el.innerHTML = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flags">
|
<div class="flags">
|
||||||
<div>
|
<div>
|
||||||
<input type="checkbox" id="multipleflag" t-model="multipleFlag"/>
|
<input type="checkbox" id="multipleflag" t-model="state.multipleFlag"/>
|
||||||
<label for="multipleflag">Do it 20x</label>
|
<label for="multipleflag">Do it 20x</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input type="checkbox" id="clearFlag" t-model="clearAfterFlag" />
|
<input type="checkbox" id="clearFlag" t-model="state.clearAfterFlag" />
|
||||||
<label for="clearFlag">Clear after</label>
|
<label for="clearFlag">Clear after</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
<li><a href="benchmarks/owl-0.16.0">OWL 0.16.0</a></li>
|
<li><a href="benchmarks/owl-0.16.0">OWL 0.16.0</a></li>
|
||||||
<li><a href="benchmarks/owl-0.17.0">OWL 0.17.0</a></li>
|
<li><a href="benchmarks/owl-0.17.0">OWL 0.17.0</a></li>
|
||||||
<li><a href="benchmarks/owl-0.18.0">OWL 0.18.0</a></li>
|
<li><a href="benchmarks/owl-0.18.0">OWL 0.18.0</a></li>
|
||||||
|
<li><a href="benchmarks/owl-0.21.0">OWL 0.21.0</a></li>
|
||||||
<li><a href="benchmarks/owl-master">OWL Master</a></li>
|
<li><a href="benchmarks/owl-master">OWL Master</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul>
|
<ul>
|
||||||
|
|||||||
+11
-10
@@ -1,5 +1,5 @@
|
|||||||
import { SAMPLES } from "./samples.js";
|
import { SAMPLES } from "./samples.js";
|
||||||
|
const {useState, useRef} = owl.hooks;
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Constants, helpers, utils
|
// Constants, helpers, utils
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
@@ -156,18 +156,18 @@ Promise.all([loadTemplates(), owl.utils.whenReady()]).then(start);
|
|||||||
class TabbedEditor extends owl.Component {
|
class TabbedEditor extends owl.Component {
|
||||||
constructor(parent, props) {
|
constructor(parent, props) {
|
||||||
super(parent, props);
|
super(parent, props);
|
||||||
this.state = {
|
this.state = useState({
|
||||||
currentTab: props.js ? "js" : props.xml ? "xml" : "css"
|
currentTab: props.js ? "js" : props.xml ? "xml" : "css"
|
||||||
};
|
});
|
||||||
this.setTab = owl.utils.debounce(this.setTab, 250, true);
|
this.setTab = owl.utils.debounce(this.setTab, 250, true);
|
||||||
|
|
||||||
this.sessions = {};
|
this.sessions = {};
|
||||||
this._setupSessions(props);
|
this._setupSessions(props);
|
||||||
this.editor = null;
|
this.editorNode = useRef("editor");
|
||||||
}
|
}
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.editor = this.editor || ace.edit(this.refs.editor);
|
this.editor = this.editor || ace.edit(this.editorNode.el);
|
||||||
|
|
||||||
this.editor.setValue(this.props[this.state.currentTab], -1);
|
this.editor.setValue(this.props[this.state.currentTab], -1);
|
||||||
this.editor.setFontSize("12px");
|
this.editor.setFontSize("12px");
|
||||||
@@ -250,7 +250,7 @@ class App extends owl.Component {
|
|||||||
this.version = owl.__info__.version;
|
this.version = owl.__info__.version;
|
||||||
this.SAMPLES = SAMPLES;
|
this.SAMPLES = SAMPLES;
|
||||||
|
|
||||||
this.state = {
|
this.state = useState({
|
||||||
js: SAMPLES[0].code,
|
js: SAMPLES[0].code,
|
||||||
css: SAMPLES[0].css || "",
|
css: SAMPLES[0].css || "",
|
||||||
xml: SAMPLES[0].xml || DEFAULT_XML,
|
xml: SAMPLES[0].xml || DEFAULT_XML,
|
||||||
@@ -259,18 +259,19 @@ class App extends owl.Component {
|
|||||||
splitLayout: true,
|
splitLayout: true,
|
||||||
leftPaneWidth: Math.ceil(window.innerWidth / 2),
|
leftPaneWidth: Math.ceil(window.innerWidth / 2),
|
||||||
topPanelHeight: null
|
topPanelHeight: null
|
||||||
};
|
});
|
||||||
|
|
||||||
this.toggleLayout = owl.utils.debounce(this.toggleLayout, 250, true);
|
this.toggleLayout = owl.utils.debounce(this.toggleLayout, 250, true);
|
||||||
this.runCode = owl.utils.debounce(this.runCode, 250, true);
|
this.runCode = owl.utils.debounce(this.runCode, 250, true);
|
||||||
this.downloadCode = owl.utils.debounce(this.downloadCode, 250, true);
|
this.downloadCode = owl.utils.debounce(this.downloadCode, 250, true);
|
||||||
|
this.content = useRef("content");
|
||||||
}
|
}
|
||||||
|
|
||||||
displayError(error) {
|
displayError(error) {
|
||||||
this.state.error = error;
|
this.state.error = error;
|
||||||
if (error) {
|
if (error) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.refs.content.innerHTML = "";
|
this.content.el.innerHTML = "";
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -296,8 +297,8 @@ class App extends owl.Component {
|
|||||||
} else {
|
} else {
|
||||||
this.state.error = false;
|
this.state.error = false;
|
||||||
}
|
}
|
||||||
this.refs.content.innerHTML = "";
|
this.content.el.innerHTML = "";
|
||||||
this.refs.content.appendChild(subiframe);
|
this.content.el.appendChild(subiframe);
|
||||||
}
|
}
|
||||||
|
|
||||||
setSample(ev) {
|
setSample(ev) {
|
||||||
|
|||||||
@@ -173,8 +173,10 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.right-pane .note {
|
.right-pane .note {
|
||||||
font-size: 20px;
|
font-size: 18px;
|
||||||
|
text-align: left;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
|
padding: 5%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-pane .error {
|
.right-pane .error {
|
||||||
|
|||||||
+272
-53
@@ -1,17 +1,18 @@
|
|||||||
const COMPONENTS = `// In this example, we show how components can be defined and created.
|
const COMPONENTS = `// In this example, we show how components can be defined and created.
|
||||||
|
const { Component, useState } = owl;
|
||||||
|
|
||||||
class Greeter extends owl.Component {
|
class Greeter extends Component {
|
||||||
state = { word: 'Hello' };
|
state = useState({ word: 'Hello' });
|
||||||
|
|
||||||
toggle() {
|
toggle() {
|
||||||
this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi'
|
this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main root component
|
// Main root component
|
||||||
class App extends owl.Component {
|
class App extends Component {
|
||||||
static components = { Greeter };
|
static components = { Greeter };
|
||||||
state = { name: 'World'};
|
state = useState({ name: 'World'});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Application setup
|
// Application setup
|
||||||
@@ -45,17 +46,18 @@ const COMPONENTS_CSS = `.greeter {
|
|||||||
|
|
||||||
const ANIMATION = `// The goal of this component is to see how the t-transition directive can be
|
const ANIMATION = `// The goal of this component is to see how the t-transition directive can be
|
||||||
// used to generate simple transition effects.
|
// used to generate simple transition effects.
|
||||||
|
const { Component, useState } = owl;
|
||||||
|
|
||||||
class Counter extends owl.Component {
|
class Counter extends Component {
|
||||||
state = { value: 0 };
|
state = useState({ value: 0 });
|
||||||
|
|
||||||
increment() {
|
increment() {
|
||||||
this.state.value++;
|
this.state.value++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class App extends owl.Component {
|
class App extends Component {
|
||||||
state = { flag: false, componentFlag: false, numbers: [] };
|
state = useState({ flag: false, componentFlag: false, numbers: [] });
|
||||||
static components = { Counter };
|
static components = { Counter };
|
||||||
|
|
||||||
toggle(key) {
|
toggle(key) {
|
||||||
@@ -183,11 +185,12 @@ const LIFECYCLE_DEMO = `// This example shows all the possible lifecycle hooks
|
|||||||
// methods in the console. Try modifying its state by clicking on it, or by
|
// methods in the console. Try modifying its state by clicking on it, or by
|
||||||
// clicking on the two main buttons, and look into the console to see what
|
// clicking on the two main buttons, and look into the console to see what
|
||||||
// happens.
|
// happens.
|
||||||
|
const { Component, useState } = owl;
|
||||||
|
|
||||||
class DemoComponent extends owl.Component {
|
class DemoComponent extends Component {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(...arguments);
|
super(...arguments);
|
||||||
this.state = { n: 0 };
|
this.state = useState({ n: 0 });
|
||||||
console.log("constructor");
|
console.log("constructor");
|
||||||
}
|
}
|
||||||
async willStart() {
|
async willStart() {
|
||||||
@@ -213,9 +216,9 @@ class DemoComponent extends owl.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class App extends owl.Component {
|
class App extends Component {
|
||||||
static components = { DemoComponent };
|
static components = { DemoComponent };
|
||||||
state = { n: 0, flag: true };
|
state = useState({ n: 0, flag: true });
|
||||||
|
|
||||||
increment() {
|
increment() {
|
||||||
this.state.n++;
|
this.state.n++;
|
||||||
@@ -259,12 +262,70 @@ const LIFECYCLE_CSS = `button {
|
|||||||
width: 250px;
|
width: 250px;
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
|
const HOOKS_DEMO = `// In this example, we show how hooks can be used or defined.
|
||||||
|
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 {
|
||||||
|
// simple state hook (reactive object)
|
||||||
|
counter = useState({ value: 0 });
|
||||||
|
|
||||||
|
// this hooks is bound to the 'mouse' property.
|
||||||
|
mouse = useMouse();
|
||||||
|
|
||||||
|
increment() {
|
||||||
|
this.counter.value++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Application setup
|
||||||
|
const qweb = new owl.QWeb(TEMPLATES);
|
||||||
|
const app = new App({ qweb });
|
||||||
|
app.mount(document.body);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const HOOKS_DEMO_XML = `<templates>
|
||||||
|
<div t-name="App">
|
||||||
|
<button t-on-click="increment">Click! <t t-esc="counter.value"/></button>
|
||||||
|
<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>
|
||||||
|
</div>
|
||||||
|
</templates>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const HOOKS_CSS = `button {
|
||||||
|
width: 120px;
|
||||||
|
height: 35px;
|
||||||
|
font-size: 16px;
|
||||||
|
}`;
|
||||||
|
|
||||||
const TODO_APP_STORE = `// This example is an implementation of the TodoList application, from the
|
const TODO_APP_STORE = `// This example is an implementation of the TodoList application, from the
|
||||||
// www.todomvc.com project. This is a non trivial application with some
|
// www.todomvc.com project. This is a non trivial application with some
|
||||||
// interesting user interactions. It uses the local storage for persistence.
|
// interesting user interactions. It uses the local storage for persistence.
|
||||||
//
|
//
|
||||||
// In this implementation, we use the owl Store class to manage the state. It
|
// In this implementation, we use the owl Store class to manage the state. It
|
||||||
// is very similar to the VueX store.
|
// is very similar to the VueX store.
|
||||||
|
const { Component, useState } = owl;
|
||||||
|
const { useRef } = owl.hooks;
|
||||||
|
|
||||||
const ENTER_KEY = 13;
|
const ENTER_KEY = 13;
|
||||||
const ESC_KEY = 27;
|
const ESC_KEY = 27;
|
||||||
@@ -335,8 +396,9 @@ function makeStore() {
|
|||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// TodoItem
|
// TodoItem
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
class TodoItem extends owl.Component {
|
class TodoItem extends Component {
|
||||||
state = { isEditing: false };
|
state = useState({ isEditing: false });
|
||||||
|
inputRef = useRef("input");
|
||||||
|
|
||||||
removeTodo() {
|
removeTodo() {
|
||||||
this.env.store.dispatch("removeTodo", this.props.id);
|
this.env.store.dispatch("removeTodo", this.props.id);
|
||||||
@@ -351,9 +413,9 @@ class TodoItem extends owl.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
focusInput() {
|
focusInput() {
|
||||||
this.refs.input.value = "";
|
this.inputRef.el.value = "";
|
||||||
this.refs.input.focus();
|
this.inputRef.el.focus();
|
||||||
this.refs.input.value = this.props.title;
|
this.inputRef.el.value = this.props.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyup(ev) {
|
handleKeyup(ev) {
|
||||||
@@ -389,7 +451,7 @@ class TodoItem extends owl.Component {
|
|||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
class TodoApp extends owl.store.ConnectedComponent {
|
class TodoApp extends owl.store.ConnectedComponent {
|
||||||
static components = { TodoItem };
|
static components = { TodoItem };
|
||||||
state = { filter: "all" };
|
state = useState({ filter: "all" });
|
||||||
|
|
||||||
static mapStoreToProps(state) {
|
static mapStoreToProps(state) {
|
||||||
return {
|
return {
|
||||||
@@ -1052,17 +1114,18 @@ const SLOTS = `// We show here how slots can be used to create generic component
|
|||||||
//
|
//
|
||||||
// Note that the t-on-click event, defined in the App template, is executed in
|
// Note that the t-on-click event, defined in the App template, is executed in
|
||||||
// the context of the App component, even though it is inside the Card component
|
// the context of the App component, even though it is inside the Card component
|
||||||
|
const { Component, useState } = owl;
|
||||||
|
|
||||||
class Card extends owl.Component {
|
class Card extends Component {
|
||||||
state = { showContent: true };
|
state = useState({ showContent: true });
|
||||||
|
|
||||||
toggleDisplay() {
|
toggleDisplay() {
|
||||||
this.state.showContent = !this.state.showContent;
|
this.state.showContent = !this.state.showContent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Counter extends owl.Component {
|
class Counter extends Component {
|
||||||
state = {val: 1};
|
state = useState({val: 1});
|
||||||
|
|
||||||
inc() {
|
inc() {
|
||||||
this.state.val++;
|
this.state.val++;
|
||||||
@@ -1070,9 +1133,9 @@ class Counter extends owl.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Main root component
|
// Main root component
|
||||||
class App extends owl.Component {
|
class App extends Component {
|
||||||
static components = {Card, Counter};
|
static components = {Card, Counter};
|
||||||
state = {a: 1, b: 3};
|
state = useState({a: 1, b: 3});
|
||||||
|
|
||||||
inc(key, delta) {
|
inc(key, delta) {
|
||||||
this.state[key] += delta;
|
this.state[key] += delta;
|
||||||
@@ -1168,8 +1231,9 @@ const ASYNC_COMPONENTS = `// This example will not work if your browser does not
|
|||||||
// However, we don't want renderings of the other sub component to be delayed
|
// However, we don't want renderings of the other sub component to be delayed
|
||||||
// because of the slow component. We use the 't-asyncroot' directive for this
|
// because of the slow component. We use the 't-asyncroot' directive for this
|
||||||
// purpose. Try removing it to see the difference.
|
// purpose. Try removing it to see the difference.
|
||||||
|
const { Component, useState } = owl;
|
||||||
|
|
||||||
class SlowComponent extends owl.Component {
|
class SlowComponent extends Component {
|
||||||
willUpdateProps() {
|
willUpdateProps() {
|
||||||
// simulate a component that needs to perform async stuff (e.g. an RPC)
|
// simulate a component that needs to perform async stuff (e.g. an RPC)
|
||||||
// with the updated props before re-rendering itself
|
// with the updated props before re-rendering itself
|
||||||
@@ -1177,11 +1241,11 @@ class SlowComponent extends owl.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NotificationList extends owl.Component {}
|
class NotificationList extends Component {}
|
||||||
|
|
||||||
class App extends owl.Component {
|
class App extends Component {
|
||||||
static components = {SlowComponent, NotificationList};
|
static components = {SlowComponent, NotificationList};
|
||||||
state = { value: 0, notifs: [] };
|
state = useState({ value: 0, notifs: [] });
|
||||||
|
|
||||||
increment() {
|
increment() {
|
||||||
this.state.value++;
|
this.state.value++;
|
||||||
@@ -1250,15 +1314,16 @@ const FORM = `// This example illustrate how the t-model directive can be used t
|
|||||||
// data between html inputs (and select/textareas) and the state of a component.
|
// data between html inputs (and select/textareas) and the state of a component.
|
||||||
// Note that there are two controls with t-model="color": they are totally
|
// Note that there are two controls with t-model="color": they are totally
|
||||||
// synchronized.
|
// synchronized.
|
||||||
|
const { Component, useState } = owl;
|
||||||
|
|
||||||
class Form extends owl.Component {
|
class Form extends Component {
|
||||||
state = {
|
state = useState({
|
||||||
text: "",
|
text: "",
|
||||||
othertext: "",
|
othertext: "",
|
||||||
number: 11,
|
number: 11,
|
||||||
color: "",
|
color: "",
|
||||||
bool: false
|
bool: false
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Application setup
|
// Application setup
|
||||||
@@ -1271,20 +1336,20 @@ const FORM_XML = `<templates>
|
|||||||
<div t-name="Form">
|
<div t-name="Form">
|
||||||
<h1>Form</h1>
|
<h1>Form</h1>
|
||||||
<div>
|
<div>
|
||||||
Text (immediate): <input t-model="text"/>
|
Text (immediate): <input t-model="state.text"/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
Other text (lazy): <input t-model.lazy="othertext"/>
|
Other text (lazy): <input t-model.lazy="state.othertext"/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
Number: <input t-model.number="number"/>
|
Number: <input t-model.number="state.number"/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
Boolean: <input type="checkbox" t-model="bool"/>
|
Boolean: <input type="checkbox" t-model="state.bool"/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
Color, with a select:
|
Color, with a select:
|
||||||
<select t-model="color">
|
<select t-model="state.color">
|
||||||
<option value="">Select a color</option>
|
<option value="">Select a color</option>
|
||||||
<option value="red">Red</option>
|
<option value="red">Red</option>
|
||||||
<option value="blue">Blue</option>
|
<option value="blue">Blue</option>
|
||||||
@@ -1292,8 +1357,8 @@ const FORM_XML = `<templates>
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
Color, with radio buttons:
|
Color, with radio buttons:
|
||||||
<span><input type="radio" name="color" id="red" value="red" t-model="color"/><label for="red">Red</label></span>
|
<span><input type="radio" name="color" id="red" value="red" t-model="state.color"/><label for="red">Red</label></span>
|
||||||
<span><input type="radio" name="color" id="blue" value="blue" t-model="color"/><label for="blue">Blue</label></span>
|
<span><input type="radio" name="color" id="blue" value="blue" t-model="state.color"/><label for="blue">Blue</label></span>
|
||||||
</div>
|
</div>
|
||||||
<hr/>
|
<hr/>
|
||||||
<h1>State</h1>
|
<h1>State</h1>
|
||||||
@@ -1316,26 +1381,31 @@ const WMS = `// This example is slightly more complex than usual. We demonstrate
|
|||||||
// - minimal width/height
|
// - minimal width/height
|
||||||
// - better heuristic for initial window position
|
// - better heuristic for initial window position
|
||||||
// - ...
|
// - ...
|
||||||
|
const { Component, useState } = owl;
|
||||||
|
const { useRef } = owl.hooks;
|
||||||
|
|
||||||
class HelloWorld extends owl.Component {}
|
class HelloWorld extends Component {}
|
||||||
|
|
||||||
class Counter extends owl.Component {
|
class Counter extends Component {
|
||||||
state = { value: 0 };
|
state = useState({ value: 0 });
|
||||||
|
|
||||||
inc() {
|
inc() {
|
||||||
this.state.value++;
|
this.state.value++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Window extends owl.Component {
|
class Window extends Component {
|
||||||
|
|
||||||
get style() {
|
get style() {
|
||||||
let { width, height, top, left, zindex } = this.props.info;
|
let { width, height, top, left, zindex } = this.props.info;
|
||||||
|
|
||||||
return \`width: \${width}px;height: \${height}px;top:\${top}px;left:\${left}px;z-index:\${zindex}\`;
|
return \`width: \${width}px;height: \${height}px;top:\${top}px;left:\${left}px;z-index:\${zindex}\`;
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.trigger("close-window", { id: this.props.info.id });
|
this.trigger("close-window", { id: this.props.info.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
startDragAndDrop(ev) {
|
startDragAndDrop(ev) {
|
||||||
this.updateZIndex();
|
this.updateZIndex();
|
||||||
this.el.classList.add('dragging');
|
this.el.classList.add('dragging');
|
||||||
@@ -1361,31 +1431,37 @@ class Window extends owl.Component {
|
|||||||
self.trigger("set-window-position", options);
|
self.trigger("set-window-position", options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateZIndex() {
|
updateZIndex() {
|
||||||
this.trigger("update-z-index", { id: this.props.info.id });
|
this.trigger("update-z-index", { id: this.props.info.id });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WindowManager extends owl.Component {
|
class WindowManager extends Component {
|
||||||
static components = { Window };
|
static components = { Window };
|
||||||
windows = [];
|
windows = [];
|
||||||
nextId = 1;
|
nextId = 1;
|
||||||
currentZindex = 1;
|
currentZindex = 1;
|
||||||
|
nextLeft = 0;
|
||||||
|
nextTop = 0;
|
||||||
|
|
||||||
addWindow(name) {
|
addWindow(name) {
|
||||||
const info = this.env.windows.find(w => w.name === name);
|
const info = this.env.windows.find(w => w.name === name);
|
||||||
const id = \`w\${this.nextId++}\`;
|
this.nextLeft = this.nextLeft + 30;
|
||||||
|
this.nextTop = this.nextTop + 30;
|
||||||
this.windows.push({
|
this.windows.push({
|
||||||
id: id,
|
id: this.nextId++,
|
||||||
title: info.title,
|
title: info.title,
|
||||||
width: info.defaultWidth,
|
width: info.defaultWidth,
|
||||||
height: info.defaultHeight,
|
height: info.defaultHeight,
|
||||||
top: 0,
|
top: this.nextTop,
|
||||||
left: 0,
|
left: this.nextLeft,
|
||||||
zindex: this.currentZindex++
|
zindex: this.currentZindex++,
|
||||||
|
component: info.component
|
||||||
});
|
});
|
||||||
this.constructor.components[id] = info.component;
|
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
closeWindow(ev) {
|
closeWindow(ev) {
|
||||||
const id = ev.detail.id;
|
const id = ev.detail.id;
|
||||||
delete this.constructor.components[id];
|
delete this.constructor.components[id];
|
||||||
@@ -1393,12 +1469,14 @@ class WindowManager extends owl.Component {
|
|||||||
this.windows.splice(index, 1);
|
this.windows.splice(index, 1);
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
setWindowPosition(ev) {
|
setWindowPosition(ev) {
|
||||||
const id = ev.detail.id;
|
const id = ev.detail.id;
|
||||||
const w = this.windows.find(w => w.id === id);
|
const w = this.windows.find(w => w.id === id);
|
||||||
w.top = ev.detail.top;
|
w.top = ev.detail.top;
|
||||||
w.left = ev.detail.left;
|
w.left = ev.detail.left;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateZIndex(ev) {
|
updateZIndex(ev) {
|
||||||
const id = ev.detail.id;
|
const id = ev.detail.id;
|
||||||
const w = this.windows.find(w => w.id === id);
|
const w = this.windows.find(w => w.id === id);
|
||||||
@@ -1407,11 +1485,12 @@ class WindowManager extends owl.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class App extends owl.Component {
|
class App extends Component {
|
||||||
static components = { WindowManager };
|
static components = { WindowManager };
|
||||||
|
wmRef = useRef("wm");
|
||||||
|
|
||||||
addWindow(name) {
|
addWindow(name) {
|
||||||
this.refs.wm.addWindow(name);
|
this.wmRef.comp.addWindow(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1452,7 +1531,7 @@ const WMS_XML = `<templates>
|
|||||||
t-on-update-z-index="updateZIndex"
|
t-on-update-z-index="updateZIndex"
|
||||||
t-on-set-window-position="setWindowPosition">
|
t-on-set-window-position="setWindowPosition">
|
||||||
<Window t-foreach="windows" t-as="w" t-key="w.id" info="w">
|
<Window t-foreach="windows" t-as="w" t-key="w.id" info="w">
|
||||||
<t t-component="{{w.id}}"/>
|
<t t-component="w.component"/>
|
||||||
</Window>
|
</Window>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1546,6 +1625,135 @@ const WMS_CSS = `body {
|
|||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
|
const MONKEYPATCH = `
|
||||||
|
// In this example, we show a possible way components can be monkey patched.
|
||||||
|
// This involves the functions patch/unpatch, which modifies the prototype of a
|
||||||
|
// class (not only an owl component). Sadly, the patch code cannot make use of
|
||||||
|
// the syntactic sugar that JS engines provides to classes, so it needs to use
|
||||||
|
// the "this._super()" syntax instead of the standard "super.someMethod()".
|
||||||
|
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
// Patching Code
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
const patchMap = new WeakMap();
|
||||||
|
|
||||||
|
function patch(C, patchName, patch) {
|
||||||
|
let metadata = patchMap.get(C.prototype);
|
||||||
|
if (!metadata) {
|
||||||
|
metadata = {
|
||||||
|
origMethods: {},
|
||||||
|
patches: {},
|
||||||
|
current: []
|
||||||
|
};
|
||||||
|
patchMap.set(C.prototype, metadata);
|
||||||
|
}
|
||||||
|
const proto = C.prototype;
|
||||||
|
if (metadata.patches[patchName]) {
|
||||||
|
throw new Error(\`Patch [\${patchName}] already exists\`);
|
||||||
|
}
|
||||||
|
metadata.patches[patchName] = patch;
|
||||||
|
applyPatch(proto, patch);
|
||||||
|
metadata.current.push(patchName);
|
||||||
|
|
||||||
|
function applyPatch(proto, patch) {
|
||||||
|
Object.keys(patch).forEach(function(methodName) {
|
||||||
|
const method = patch[methodName];
|
||||||
|
if (typeof method === "function") {
|
||||||
|
const original = proto[methodName];
|
||||||
|
if (!(methodName in metadata.origMethods)) {
|
||||||
|
metadata.origMethods[methodName] = original;
|
||||||
|
}
|
||||||
|
proto[methodName] = function(...args) {
|
||||||
|
this._super = original;
|
||||||
|
return method.call(this, ...args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we define here an unpatch function. This is mostly useful if we want to
|
||||||
|
// remove a patch. For example, for testing purposes
|
||||||
|
function unpatch(C, patchName) {
|
||||||
|
const proto = C.prototype;
|
||||||
|
let metadata = patchMap.get(proto);
|
||||||
|
if (!metdata) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
patchMap.delete(proto);
|
||||||
|
|
||||||
|
// reset to original
|
||||||
|
for (let k in metadata.origMethods) {
|
||||||
|
proto[k] = metadata.origMethods[k];
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply other patches
|
||||||
|
for (let name of metadata.current) {
|
||||||
|
if (name !== patchName) {
|
||||||
|
patch(C, name, metadata.patches[name]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
// Components
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
const { Component, useState } = owl;
|
||||||
|
|
||||||
|
class Counter extends Component {
|
||||||
|
state = useState({ value: 0 });
|
||||||
|
|
||||||
|
increment() {
|
||||||
|
this.state.value++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main root component
|
||||||
|
class App extends Component {
|
||||||
|
static components = { Counter };
|
||||||
|
}
|
||||||
|
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
// Patching code
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// In an Odoo application, this code would be located in another odoo module,
|
||||||
|
// to customize some behaviour in an existing class/component
|
||||||
|
patch(Counter, "double_value", {
|
||||||
|
increment() {
|
||||||
|
this._super();
|
||||||
|
this.state.value = 2 * this.state.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
// Application setup
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
const qweb = new owl.QWeb(TEMPLATES);
|
||||||
|
const app = new App({ qweb });
|
||||||
|
app.mount(document.body);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MONKEYPATCH_XML = `
|
||||||
|
<templates>
|
||||||
|
<button t-name="Counter" t-on-click="increment">
|
||||||
|
Click [<t t-esc="state.value"/>]
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div t-name="App">
|
||||||
|
<Counter />
|
||||||
|
</div>
|
||||||
|
</templates>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MONKEYPATCH_CSS = `
|
||||||
|
button {
|
||||||
|
font-size: 20px;
|
||||||
|
width: 100px;
|
||||||
|
height: 50px;
|
||||||
|
}`;
|
||||||
|
|
||||||
export const SAMPLES = [
|
export const SAMPLES = [
|
||||||
{
|
{
|
||||||
description: "Components",
|
description: "Components",
|
||||||
@@ -1570,6 +1778,12 @@ export const SAMPLES = [
|
|||||||
xml: LIFECYCLE_DEMO_XML,
|
xml: LIFECYCLE_DEMO_XML,
|
||||||
css: LIFECYCLE_CSS
|
css: LIFECYCLE_CSS
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
description: "Hooks",
|
||||||
|
code: HOOKS_DEMO,
|
||||||
|
xml: HOOKS_DEMO_XML,
|
||||||
|
css: HOOKS_CSS
|
||||||
|
},
|
||||||
{
|
{
|
||||||
description: "Todo List App (with store)",
|
description: "Todo List App (with store)",
|
||||||
code: TODO_APP_STORE,
|
code: TODO_APP_STORE,
|
||||||
@@ -1599,5 +1813,10 @@ export const SAMPLES = [
|
|||||||
code: ASYNC_COMPONENTS,
|
code: ASYNC_COMPONENTS,
|
||||||
xml: ASYNC_COMPONENTS_XML,
|
xml: ASYNC_COMPONENTS_XML,
|
||||||
css: ASYNC_COMPONENTS_CSS
|
css: ASYNC_COMPONENTS_CSS
|
||||||
|
}, {
|
||||||
|
description: "Monkey patching components",
|
||||||
|
code: MONKEYPATCH,
|
||||||
|
xml: MONKEYPATCH_XML,
|
||||||
|
css: MONKEYPATCH_CSS
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -45,7 +45,11 @@
|
|||||||
<div>🦉 Odoo Web Library 🦉</div>
|
<div>🦉 Odoo Web Library 🦉</div>
|
||||||
<div>v<t t-esc="version"/></div>
|
<div>v<t t-esc="version"/></div>
|
||||||
<div class="url"><a href="https://github.com/odoo/owl">https://github.com/odoo/owl</a></div>
|
<div class="url"><a href="https://github.com/odoo/owl">https://github.com/odoo/owl</a></div>
|
||||||
<div class="note">Note: these examples require a recent browser to work without a transpilation step. </div>
|
<div class="note">
|
||||||
|
<p>Note: these examples are using recent features of Javascript, and require a recent browser to work without a transpilation step!
|
||||||
|
For example, it makes use of class fields and class static fields. These examples should work in a recent Chrome version.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div t-if="state.error" class="error">
|
<div t-if="state.error" class="error">
|
||||||
<h3>Error</h3>
|
<h3>Error</h3>
|
||||||
|
|||||||
Reference in New Issue
Block a user