mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 70a70161e7 | |||
| e690991a6a | |||
| a621a14028 | |||
| 9754cc7521 | |||
| aa268937a7 | |||
| 3db6f0a643 | |||
| 635a7fc72c | |||
| 6cd5ec10f0 | |||
| 0a276fbf1d | |||
| d593131cc1 | |||
| 3c3dcb44a9 | |||
| 6ecd1d6ea2 | |||
| 1a931a2a08 | |||
| bf705fcd99 | |||
| bb46346767 | |||
| cd0762b436 | |||
| ca920a0091 | |||
| e5b1ba24d8 | |||
| eaf912bed7 | |||
| fd0fb5be00 | |||
| f9f730ab7b | |||
| b30b72fc36 | |||
| ab08e84bff | |||
| cd9cac1246 | |||
| f6369f9750 | |||
| f18c73e59f | |||
| 93d0a7dd46 | |||
| b1d022b583 | |||
| 5382e824b1 | |||
| 15a3f59744 | |||
| 4a90ec7ff0 | |||
| 9f9b6b174e | |||
| 545ceefc3d | |||
| 591508e769 | |||
| d9cbd23cd9 | |||
| f4cd111cc8 | |||
| ea015af741 | |||
| bfc7c81c0d | |||
| e24ff8f8aa | |||
| 9f1e64d399 | |||
| 8e1aa71436 | |||
| be2fc965b1 | |||
| 7245ccf8f9 | |||
| 1c8e1af86d | |||
| 60a6cca960 | |||
| ad55b42ccb | |||
| 50c0a4b126 | |||
| 2ca42e7470 | |||
| d381e85d94 | |||
| af7520d869 | |||
| 0095bfa61f | |||
| 1b12cf9b91 | |||
| dbfc7e4acd | |||
| e838e879c0 | |||
| ee63f6bb0f | |||
| 16bbb8bc9f | |||
| 4a5db0c283 | |||
| cc50a2e3bb | |||
| 1e2b204fdd | |||
| 4a889b7b6b | |||
| 34695883c2 | |||
| 3d2e2a1873 | |||
| af6aca83a2 | |||
| 8c8ffb6a6b | |||
| 63a8fcd7e2 | |||
| e6a5934162 | |||
| 5524c2e323 | |||
| 35c1de26b8 | |||
| d1cf6b1b8d | |||
| bfa2c681cd |
@@ -32,7 +32,7 @@ find some more information [here](doc/comparison.md).
|
|||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
Here is a short example to illustrate interactive widgets:
|
Here is a short example to illustrate interactive components:
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<templates>
|
<templates>
|
||||||
@@ -64,8 +64,8 @@ string. More interesting examples can be found on the
|
|||||||
|
|
||||||
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.15.0.js](https://github.com/odoo/owl/releases/download/v0.15.0/owl.js)
|
- [owl-0.18.0.js](https://github.com/odoo/owl/releases/download/v0.18.0/owl.js)
|
||||||
- [owl-0.15.0.min.js](https://github.com/odoo/owl/releases/download/v0.15.0/owl.min.js)
|
- [owl-0.18.0.min.js](https://github.com/odoo/owl/releases/download/v0.18.0/owl.min.js)
|
||||||
|
|
||||||
Some npm scripts are available:
|
Some npm scripts are available:
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -87,7 +87,7 @@ For example, a simple fade in/out effect can be done with this:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The `t-transition` directive can be combined with `t-widget`.
|
The `t-transition` directive can be applied on a node element or on a component.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -91,17 +91,17 @@ there is a syntax highlighter for jsx here on github!
|
|||||||
This is actually a big difference between OWL and React/Vue: components in OWL
|
This is actually a big difference between OWL and React/Vue: components in OWL
|
||||||
are totally asynchronous. They have two asynchronous hooks in their lifecycle:
|
are totally asynchronous. They have two asynchronous hooks in their lifecycle:
|
||||||
|
|
||||||
- `willStart` (before the widget starts rendering)
|
- `willStart` (before the component starts rendering)
|
||||||
- `willUpdateProps` (before new props are set)
|
- `willUpdateProps` (before new props are set)
|
||||||
|
|
||||||
Both these methods can be implemented and return a promise. The rendering will
|
Both these methods can be implemented and return a promise. The rendering will
|
||||||
then wait for these promises to be completed before patching the DOM. This is
|
then wait for these promises to be completed before patching the DOM. This is
|
||||||
useful for some use cases: for example, a widget may want to fetch an external
|
useful for some use cases: for example, a component may want to fetch an external
|
||||||
library (a calendar widget may need a specialized calendar rendering library),
|
library (a calendar component may need a specialized calendar rendering library),
|
||||||
in its willStart hook.
|
in its willStart hook.
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
class MyCalendarWidget extends owl.Component {
|
class MyCalendarComponent extends owl.Component {
|
||||||
...
|
...
|
||||||
|
|
||||||
willStart() {
|
willStart() {
|
||||||
|
|||||||
+257
-150
@@ -4,12 +4,13 @@
|
|||||||
|
|
||||||
- [Overview](#overview)
|
- [Overview](#overview)
|
||||||
- [Example](#example)
|
- [Example](#example)
|
||||||
- [Root Widget And Environment](#root-widget-and-environment)
|
|
||||||
- [Reference](#reference)
|
- [Reference](#reference)
|
||||||
- [Properties](#properties)
|
- [Properties](#properties)
|
||||||
- [Static Properties](#static-properties)
|
- [Static Properties](#static-properties)
|
||||||
- [Methods](#methods)
|
- [Methods](#methods)
|
||||||
- [Lifecycle](#lifecycle)
|
- [Lifecycle](#lifecycle)
|
||||||
|
- [Root Component](#root-component)
|
||||||
|
- [Environment](#environment)
|
||||||
- [Composition](#composition)
|
- [Composition](#composition)
|
||||||
- [Event Handling](#event-handling)
|
- [Event Handling](#event-handling)
|
||||||
- [Form Input Bindings](#form-input-bindings)
|
- [Form Input Bindings](#form-input-bindings)
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
- [References](#references)
|
- [References](#references)
|
||||||
- [Slots](#slots)
|
- [Slots](#slots)
|
||||||
- [Asynchronous Rendering](#asynchronous-rendering)
|
- [Asynchronous Rendering](#asynchronous-rendering)
|
||||||
|
- [Error Handling](#error-handling)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -28,11 +30,11 @@ OWL components are the building blocks for user interface. They are designed to
|
|||||||
1. **declarative:** the user interface should be described in term of the state
|
1. **declarative:** the user interface should be described in term of the state
|
||||||
of the application, not as a sequence of imperative steps.
|
of the application, not as a sequence of imperative steps.
|
||||||
|
|
||||||
2. **composable:** each widget can seamlessly be created in a parent widget by
|
2. **composable:** each component can seamlessly be created in a parent component by
|
||||||
a simple directive in its template.
|
a simple tag or directive in its template.
|
||||||
|
|
||||||
3. **asynchronous rendering:** the framework will transparently wait for each
|
3. **asynchronous rendering:** the framework will transparently wait for each
|
||||||
subwidgets to be ready before applying the rendering. It uses native promises
|
sub components to be ready before applying the rendering. It uses native promises
|
||||||
under the hood.
|
under the hood.
|
||||||
|
|
||||||
4. **uses QWeb as a template system:** the templates are described in XML
|
4. **uses QWeb as a template system:** the templates are described in XML
|
||||||
@@ -41,7 +43,7 @@ OWL components are the building blocks for user interface. They are designed to
|
|||||||
OWL components are defined as a subclass of Component. The rendering is
|
OWL components are defined as a subclass of Component. The rendering is
|
||||||
exclusively done by a [QWeb](qweb.md) template (which needs to be preloaded in QWeb).
|
exclusively done by a [QWeb](qweb.md) template (which needs to be preloaded in QWeb).
|
||||||
Rendering a component generates a virtual dom representation
|
Rendering a component generates a virtual dom representation
|
||||||
of the widget, 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
|
OWL components observe their states, and rerender themselves whenever it is
|
||||||
changed. This is done by an [observer](observer.md).
|
changed. This is done by an [observer](observer.md).
|
||||||
@@ -76,33 +78,9 @@ a state object is defined. It is not mandatory to use the state object, but it
|
|||||||
is certainly encouraged. The state object is [observed](observer.md), and any
|
is certainly encouraged. The state object is [observed](observer.md), and any
|
||||||
change to it will cause a rerendering.
|
change to it will cause a rerendering.
|
||||||
|
|
||||||
## Root Widget And Environment
|
|
||||||
|
|
||||||
Most of the time, Owl widget will be created automatically by the `t-widget`
|
|
||||||
directive in a template. There is however an obvious exception: the root widget
|
|
||||||
of an Owl application has to be created manually:
|
|
||||||
|
|
||||||
```js
|
|
||||||
class App extends owl.Component { ... }
|
|
||||||
|
|
||||||
const qweb = new owl.QWeb(TEMPLATES);
|
|
||||||
const env = { qweb: qweb };
|
|
||||||
const app = new App(env);
|
|
||||||
app.mount(document.body);
|
|
||||||
```
|
|
||||||
|
|
||||||
The root widget needs an environment. In Owl, an environment is an object with
|
|
||||||
a `qweb` key, which has to be a [QWeb](qweb.md) instance. This qweb instance will
|
|
||||||
be used to render everything.
|
|
||||||
|
|
||||||
The environment will be given to each child, unchanged, in the `env` property.
|
|
||||||
This can be very useful to share common information/methods. For example, all
|
|
||||||
rpcs can be made through a `rpc` method in the environment. This makes it very
|
|
||||||
easy to test a component.
|
|
||||||
|
|
||||||
## Reference
|
## Reference
|
||||||
|
|
||||||
An Owl component is a small class which represent a widget or some UI element.
|
An Owl component is a small class which represent a component or some UI element.
|
||||||
It exists in the context of an environment (`env`), which is propagated from a
|
It exists in the context of an environment (`env`), which is propagated from a
|
||||||
parent to its children. The environment needs to have a QWeb instance, which
|
parent to its children. The environment needs to have a QWeb instance, which
|
||||||
will be used to render the component template.
|
will be used to render the component template.
|
||||||
@@ -123,7 +101,7 @@ find a template with the component name (or one of its ancestor).
|
|||||||
|
|
||||||
- **`state`** (Object): this is the location of the component's state, if there is
|
- **`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
|
any. After the willStart method, the `state` property is observed, and each
|
||||||
change will cause the widget to rerender itself.
|
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,
|
||||||
@@ -131,7 +109,7 @@ find a template with the component name (or one of its ancestor).
|
|||||||
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
|
- **`refs`** (Object): the `refs` object contains all references to sub DOM nodes
|
||||||
or sub widgets defined by a `t-ref` directive in the component's template.
|
or sub components defined by a `t-ref` directive in the component's template.
|
||||||
|
|
||||||
### Static Properties
|
### Static Properties
|
||||||
|
|
||||||
@@ -139,18 +117,40 @@ find a template with the component name (or one of its ancestor).
|
|||||||
type and shape of the (actual) props given to the component. If Owl mode is
|
type and shape of the (actual) props given to the component. If Owl mode is
|
||||||
`dev`, this will be used to validate the props each time the component is
|
`dev`, this will be used to validate the props each time the component is
|
||||||
created/updated. See [Props Validation](#props-validation) for more information.
|
created/updated. See [Props Validation](#props-validation) for more information.
|
||||||
- **`defaultProps`** (Object, optional): if given, this object define default
|
|
||||||
|
```js
|
||||||
|
class Counter extends owl.Component {
|
||||||
|
static props = {
|
||||||
|
initialValue: Number,
|
||||||
|
optional: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
* **`defaultProps`** (Object, optional): if given, this object define default
|
||||||
values for (top-level) props. Whenever `props` are given to the object, they
|
values for (top-level) props. Whenever `props` are given to the object, they
|
||||||
will be altered to add default value (if missing). Note that it does not
|
will be altered to add default value (if missing). Note that it does not
|
||||||
change the initial object, a new object will be created instead.
|
change the initial object, a new object will be created instead.
|
||||||
|
|
||||||
|
```js
|
||||||
|
class Counter extends owl.Component {
|
||||||
|
static defaultProps = {
|
||||||
|
initialValue: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Methods
|
### Methods
|
||||||
|
|
||||||
|
We explain here all the public methods of the `Component` class.
|
||||||
|
|
||||||
- **`mount(target)`** (async): this is the main way a component's hierarchy is added to the
|
- **`mount(target)`** (async): this is the main way a component's hierarchy is added to the
|
||||||
DOM: the root component is mounted to a target HTMLElement. Obviously, this
|
DOM: the root component is mounted to a target HTMLElement. Obviously, this
|
||||||
is asynchronous, since each children need to be created as well. Most applications
|
is asynchronous, since each children need to be created as well. Most applications
|
||||||
will need to call `mount` exactly once, on the root component.
|
will need to call `mount` exactly once, on the root component.
|
||||||
|
|
||||||
|
Note that a component can be mounted and unmounted multiple times if needed.
|
||||||
|
|
||||||
- **`unmount()`**: in case a component need to be detached/removed from the DOM, this
|
- **`unmount()`**: in case a component need to be detached/removed from the DOM, this
|
||||||
method can be used. Most applications should not call `unmount`, this is more
|
method can be used. Most applications should not call `unmount`, this is more
|
||||||
useful to the underlying component system.
|
useful to the underlying component system.
|
||||||
@@ -163,7 +163,7 @@ find a template with the component name (or one of its ancestor).
|
|||||||
DOM in the same stack frame.
|
DOM in the same stack frame.
|
||||||
|
|
||||||
- **`shouldUpdate(nextProps)`**: this method is called each time a component's props
|
- **`shouldUpdate(nextProps)`**: this method is called each time a component's props
|
||||||
are updated. It returns a boolean, which indicates if the widget should
|
are updated. It returns a boolean, which indicates if the component should
|
||||||
ignore a props update. If it returns false, then `willUpdateProps` will not
|
ignore a props update. If it returns false, then `willUpdateProps` will not
|
||||||
be called, and no rendering will occur. Its default implementation is to
|
be called, and no rendering will occur. Its default implementation is to
|
||||||
always return true. This is an optimization, similar to React's `shouldComponentUpdate`. Most of the time, this should not be used, but it
|
always return true. This is an optimization, similar to React's `shouldComponentUpdate`. Most of the time, this should not be used, but it
|
||||||
@@ -180,6 +180,11 @@ find a template with the component name (or one of its ancestor).
|
|||||||
called directly (except maybe on the root component), but should be done by the
|
called directly (except maybe on the root component), but should be done by the
|
||||||
framework instead.
|
framework instead.
|
||||||
|
|
||||||
|
Obviously, these methods are reserved for Owl, and should not be used by Owl
|
||||||
|
users, unless they want to override them. Also, Owl reserves all method names
|
||||||
|
starting with `__`, in order to prevent possible future conflicts with user code
|
||||||
|
whenever Owl needs to change.
|
||||||
|
|
||||||
### Lifecycle
|
### Lifecycle
|
||||||
|
|
||||||
A solid and robust component system needs useful hooks/methods to help
|
A solid and robust component system needs useful hooks/methods to help
|
||||||
@@ -187,7 +192,7 @@ developers write components. Here is a complete description of the lifecycle of
|
|||||||
a owl component:
|
a owl component:
|
||||||
|
|
||||||
| Method | Description |
|
| Method | Description |
|
||||||
| ------------------------------------------------ | ----------------------------------------------------- |
|
| ------------------------------------------------ | ------------------------------------------------------------ |
|
||||||
| **[constructor](#constructorparent-props)** | constructor |
|
| **[constructor](#constructorparent-props)** | constructor |
|
||||||
| **[willStart](#willstart)** | async, before first rendering |
|
| **[willStart](#willstart)** | async, before first rendering |
|
||||||
| **[mounted](#mounted)** | just after component is rendered and added to the DOM |
|
| **[mounted](#mounted)** | just after component is rendered and added to the DOM |
|
||||||
@@ -195,6 +200,7 @@ a owl component:
|
|||||||
| **[willPatch](#willpatch)** | just before the DOM is patched |
|
| **[willPatch](#willpatch)** | just before the DOM is patched |
|
||||||
| **[patched](#patchedsnapshot)** | just after the DOM is patched |
|
| **[patched](#patchedsnapshot)** | just after the DOM is patched |
|
||||||
| **[willUnmount](#willunmount)** | just before removing component from DOM |
|
| **[willUnmount](#willunmount)** | just before removing component from DOM |
|
||||||
|
| **[catchError](#catcherrorerror)** | catch errors (see [error handling section](#error-handling)) |
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
@@ -239,7 +245,7 @@ perform some action before the initial rendering of a component.
|
|||||||
|
|
||||||
It will be called exactly once before the initial rendering. It is useful
|
It will be called exactly once before the initial rendering. It is useful
|
||||||
in some cases, for example, to load external assets (such as a JS library)
|
in some cases, for example, to load external assets (such as a JS library)
|
||||||
before the widget is rendered. Another use case is to load data from a server.
|
before the component is rendered. Another use case is to load data from a server.
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
async willStart() {
|
async willStart() {
|
||||||
@@ -251,7 +257,8 @@ 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.
|
||||||
|
|
||||||
The widget rendering will take place after `willStart` is completed.
|
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()`
|
||||||
|
|
||||||
@@ -296,8 +303,8 @@ scrollbar.
|
|||||||
|
|
||||||
Note that modifying the state object is not allowed here. This method is called just
|
Note that modifying the state object 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 widget 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 widgets with `t-keepalive`).
|
happen with components with `t-keepalive`).
|
||||||
|
|
||||||
The return value of this method will be given as the first argument of the
|
The return value of this method will be given as the first argument of the
|
||||||
corresponding `patched` call.
|
corresponding `patched` call.
|
||||||
@@ -309,12 +316,12 @@ likely via a change in its state/props or environment).
|
|||||||
|
|
||||||
This method is not called on the initial render. It is useful to interact
|
This method is not called on the initial render. It is useful to interact
|
||||||
with the DOM (for example, through an external library) whenever the
|
with the DOM (for example, through an external library) whenever the
|
||||||
component was patched. Note that this hook will not be called if the widget is
|
component was patched. Note that this hook will not be called if the compoent is
|
||||||
not in the DOM (this can happen with widgets 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.
|
The `snapshot` parameter is the result of the previous `willPatch` call.
|
||||||
|
|
||||||
Updating the widget 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
|
||||||
careful at avoiding endless cycles.
|
careful at avoiding endless cycles.
|
||||||
@@ -335,91 +342,143 @@ the DOM. This is a good place to remove some listeners, for example.
|
|||||||
|
|
||||||
This is the opposite method of `mounted`.
|
This is the opposite method of `mounted`.
|
||||||
|
|
||||||
|
#### `catchError(error)`
|
||||||
|
|
||||||
|
The `catchError` method is useful when we need to intercept and properly react
|
||||||
|
to (rendering) errors that occur in some sub components. See the section on
|
||||||
|
[error handling](#error-handling)
|
||||||
|
|
||||||
|
### Root Component
|
||||||
|
|
||||||
|
Most of the time, an Owl component will be created automatically by a tag (or the `t-component`
|
||||||
|
directive) in a template. There is however an obvious exception: the root component
|
||||||
|
of an Owl application has to be created manually:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class App extends owl.Component { ... }
|
||||||
|
|
||||||
|
const qweb = new owl.QWeb(TEMPLATES);
|
||||||
|
const env = { qweb: qweb };
|
||||||
|
const app = new App(env);
|
||||||
|
app.mount(document.body);
|
||||||
|
```
|
||||||
|
|
||||||
|
The root component needs an environment.
|
||||||
|
|
||||||
|
### Environment
|
||||||
|
|
||||||
|
In Owl, an environment is an object with a `qweb` key, which has to be a
|
||||||
|
[QWeb](qweb.md) instance. This qweb instance will be used to render everything.
|
||||||
|
|
||||||
|
The environment is meant to contain (mostly) static global information and
|
||||||
|
methods for the whole application. For example, settings keys (`mode` to determine
|
||||||
|
if we are in desktop or mobile mode, or `theme`: dark or light), `rpc` methods,
|
||||||
|
session information, ...
|
||||||
|
|
||||||
|
The environment will be given to each child, unchanged, in the `env` property.
|
||||||
|
This can be very useful to share common information/methods. For example, all
|
||||||
|
rpcs can be made through a `rpc` method in the environment. This makes it very
|
||||||
|
easy to test a component.
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
`update` event on the QWeb instance. For example, a responsive environment
|
||||||
|
could be programmed like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
function setupResponsivePlugin(env) {
|
||||||
|
const isMobile = () => window.innerWidth <= 768;
|
||||||
|
env.isMobile = isMobile();
|
||||||
|
const updateEnv = owl.utils.debounce(() => {
|
||||||
|
if (env.isMobile !== isMobile()) {
|
||||||
|
env.isMobile = !env.isMobile;
|
||||||
|
env.qweb.trigger("update");
|
||||||
|
}
|
||||||
|
}, 15);
|
||||||
|
window.addEventListener("resize", updateEnv);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Composition
|
### Composition
|
||||||
|
|
||||||
The example above shows a QWeb template with a `t-on-click` directive. Widget
|
The example above shows a QWeb template with a sub component. In a template,
|
||||||
templates are standard [QWeb](qweb.md) templates, but with an extra directive:
|
components are declared with a tagname corresponding to the class name. It has
|
||||||
`t-widget`. With the `t-widget` directive, widget templates can declare sub
|
to be capitalized.
|
||||||
widgets:
|
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<div t-name="ParentWidget">
|
<div t-name="ParentComponent">
|
||||||
<span>some text</span>
|
<span>some text</span>
|
||||||
<t t-widget="MyWidget" info="13"/>
|
<MyComponent info="13" />
|
||||||
</div>
|
</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
```js
|
```js
|
||||||
class ParentWidget extends owl.Component {
|
class ParentComponent extends owl.Component {
|
||||||
widgets = { MyWidget: MyWidget};
|
components = { MyComponent: MyComponent};
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
In this example, the `ParentWidget`'s template creates a widget `MyWidget` just
|
In this example, the `ParentComponent`'s template creates a component `MyComponent` just
|
||||||
after the span. The `info` key will be added to the subwidget's props. Each
|
after the span. The `info` key will be added to the subcomponent's `props`. Each
|
||||||
props is a string which represents a javascript (QWeb) expression, so it is
|
`props` is a string which represents a javascript (QWeb) expression, so it is
|
||||||
dynamic. If it is necessary to give a string, this can be done by quoting it:
|
dynamic. If it is necessary to give a string, this can be done by quoting it:
|
||||||
`someString="'somevalue'"`. See the
|
`someString="'somevalue'"`.
|
||||||
[QWeb](qweb.md) documentation for more information on the `t-widget` directive.
|
|
||||||
|
|
||||||
Note that the rendering context for the template is the widget 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 widget.
|
that the template can access `state`, `props`, `env`, or any methods defined in the component.
|
||||||
|
|
||||||
The `t-widget` directive is the key to a declarative component
|
|
||||||
system. It allows a template to define where and how a sub widget is created
|
|
||||||
and/or updated. For example:
|
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<div t-name="ParentWidget">
|
<div t-name="ParentComponent">
|
||||||
<t t-widget="ChildWidget" count="state.val"/>
|
<ChildComponent count="state.val" />
|
||||||
</div>
|
</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
```js
|
```js
|
||||||
class ParentWidget {
|
class ParentComponent {
|
||||||
widgets = { ChildWidget };
|
components = { ChildComponent };
|
||||||
state = { val: 4 };
|
state = { val: 4 };
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Whenever the template is rendered, it will automatically create the subwidget
|
Whenever the template is rendered, it will automatically create the subcomponent
|
||||||
`ChildWidget` at the correct place. It needs to find the reference to the
|
`ChildComponent` at the correct place. It needs to find the reference to the
|
||||||
actual component class in the special `widgets` key, or the class registered in
|
actual component class in the special `components` key, or the class registered in
|
||||||
QWeb's global registry (see `register` function of QWeb). It first looks inside
|
QWeb's global registry (see `register` function of QWeb). It first looks inside
|
||||||
the local `widgets` key, then fallbacks on the global registry.
|
the local `components` key, then fallbacks on the global registry.
|
||||||
|
|
||||||
_Props_: In this example, the child widget will receive the object `{count: 4}` in its
|
_Props_: In this example, the child component will receive the object `{count: 4}` in its
|
||||||
constructor. This will be assigned to the `props` variable, which can be accessed
|
constructor. This will be assigned to the `props` variable, which can be accessed
|
||||||
on the widget (and also, in the template). Whenever the state is updated, then
|
on the component (and also, in the template). Whenever the state is updated, then
|
||||||
the subwidget will also be updated automatically.
|
the sub component will also be updated automatically.
|
||||||
|
|
||||||
Note that there are some restrictions on prop names: `class`, `style` and any
|
Note that there are some restrictions on prop names: `class`, `style` and any
|
||||||
string which starts with `t-` are not allowed.
|
string which starts with `t-` are not allowed.
|
||||||
|
|
||||||
The `t-widget` directive also accepts dynamic values with string interpolation
|
The `t-component` directive can also be used to accept dynamic values with string interpolation (like the [`t-attf-`](qweb.md#dynamic-attributes) directive):
|
||||||
(like the [`t-attf-`](qweb.md#dynamic-attributes) directive):
|
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<div t-name="ParentWidget">
|
<div t-name="ParentComponent">
|
||||||
<t t-widget="ChildWidget{{id}}"/>
|
<t t-component="ChildComponent{{id}}" />
|
||||||
</div>
|
</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
```js
|
```js
|
||||||
class ParentWidget {
|
class ParentComponent {
|
||||||
widgets = { ChildWidget1, ChildWidget2 };
|
components = { ChildComponent1, ChildComponent2 };
|
||||||
state = { id: 1 };
|
state = { id: 1 };
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**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 widget: 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 widget element.
|
root component element.
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<div t-name="ParentWidget">
|
<div t-name="ParentComponent">
|
||||||
<t t-widget="MyWidget" class="someClass" style="font-weight:bold;" info="13"/>
|
<MyComponent class="someClass" style="font-weight:bold;" info="13" />
|
||||||
</div>
|
</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -431,7 +490,7 @@ class that need to be removed. This is why we only support the explicit syntax
|
|||||||
with a class object:
|
with a class object:
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<t t-widget="MyWidget" t-att-class="{a: state.flagA, b: state.flagB}" />
|
<MyComponent t-att-class="{a: state.flagA, b: state.flagB}" />
|
||||||
```
|
```
|
||||||
|
|
||||||
### Event Handling
|
### Event Handling
|
||||||
@@ -454,7 +513,7 @@ A _pure_ DOM event is directly triggered by a user interaction (e.g. a `click`).
|
|||||||
This will be roughly translated in javascript like this:
|
This will be roughly translated in javascript like this:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
button.addEventListener("click", widget.someMethod.bind(widget));
|
button.addEventListener("click", component.someMethod.bind(component));
|
||||||
```
|
```
|
||||||
|
|
||||||
The suffix (`click` in this example) is simply the name of the actual DOM
|
The suffix (`click` in this example) is simply the name of the actual DOM
|
||||||
@@ -463,11 +522,11 @@ event.
|
|||||||
A _business_ DOM event is triggered by a call to `trigger` on a component.
|
A _business_ DOM event is triggered by a call to `trigger` on a component.
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<t t-widget="MyWidget" t-on-menu-loaded="someMethod"/>
|
<MyComponent t-on-menu-loaded="someMethod" />
|
||||||
```
|
```
|
||||||
|
|
||||||
```js
|
```js
|
||||||
class MyWidget {
|
class MyComponent {
|
||||||
someWhere() {
|
someWhere() {
|
||||||
const payload = ...;
|
const payload = ...;
|
||||||
this.trigger('menu-loaded', payload);
|
this.trigger('menu-loaded', payload);
|
||||||
@@ -477,12 +536,12 @@ A _business_ DOM event is triggered by a call to `trigger` on a component.
|
|||||||
|
|
||||||
The call to `trigger` generates a [_CustomEvent_](https://developer.mozilla.org/docs/Web/Guide/Events/Creating_and_triggering_events)
|
The call to `trigger` generates a [_CustomEvent_](https://developer.mozilla.org/docs/Web/Guide/Events/Creating_and_triggering_events)
|
||||||
of type `menu-loaded` and dispatches it on the component's DOM element
|
of type `menu-loaded` and dispatches it on the component's DOM element
|
||||||
(`this.el`). The event bubbles and is cancelable. The parent widget listening
|
(`this.el`). The event bubbles and is cancelable. The parent component listening
|
||||||
to event `menu-loaded` will receive the payload in its `someMethod` handler
|
to event `menu-loaded` will receive the payload in its `someMethod` handler
|
||||||
(in the `detail` property of the event), whenever the event is triggered.
|
(in the `detail` property of the event), whenever the event is triggered.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
class ParentWidget {
|
class ParentComponent {
|
||||||
someMethod(ev) {
|
someMethod(ev) {
|
||||||
const payload = ev.detail;
|
const payload = ev.detail;
|
||||||
...
|
...
|
||||||
@@ -646,7 +705,7 @@ is inserted into the DOM.
|
|||||||
```
|
```
|
||||||
|
|
||||||
```js
|
```js
|
||||||
class MyWidget extends owl.Component {
|
class MyComponent extends owl.Component {
|
||||||
...
|
...
|
||||||
focusMe() {
|
focusMe() {
|
||||||
this.refs.someInput.focus();
|
this.refs.someInput.focus();
|
||||||
@@ -677,27 +736,27 @@ component (with some code like `app.mount(document.body)`).
|
|||||||
|
|
||||||
2. when it is done, template `A` is rendered.
|
2. when it is done, template `A` is rendered.
|
||||||
|
|
||||||
- widget `B` is created
|
- component `B` is created
|
||||||
1. `willStart` is called on `B`
|
1. `willStart` is called on `B`
|
||||||
2. template `B` is rendered
|
2. template `B` is rendered
|
||||||
- widget `C` is created
|
- component `C` is created
|
||||||
1. `willStart` is called on `C`
|
1. `willStart` is called on `C`
|
||||||
2. template `C` is rendered
|
2. template `C` is rendered
|
||||||
- widget `D` is created
|
- component `D` is created
|
||||||
1. `willStart` is called on `D`
|
1. `willStart` is called on `D`
|
||||||
2. template `D` is rendered
|
2. template `D` is rendered
|
||||||
- widget `E` is created
|
- component `E` is created
|
||||||
1. `willStart` is called on `E`
|
1. `willStart` is called on `E`
|
||||||
2. template `E` is rendered
|
2. template `E` is rendered
|
||||||
|
|
||||||
3. widget `A` is patched into a detached DOM element. This will create the actual
|
3. component `A` is patched into a detached DOM element. This will create the actual
|
||||||
widget `A` DOM structure. The patching process will cause recursively the
|
component `A` DOM structure. The patching process will cause recursively the
|
||||||
patching of the `B`, `C`, `D` and `E` DOM trees. (so the actual full DOM tree is created
|
patching of the `B`, `C`, `D` and `E` DOM trees. (so the actual full DOM tree is created
|
||||||
in one pass)
|
in one pass)
|
||||||
|
|
||||||
4. the widget `A` root element is actually appended to `document.body`
|
4. the component `A` root element is actually appended to `document.body`
|
||||||
|
|
||||||
5. The method `mounted` is called recursively on all widgets in the following
|
5. The method `mounted` is called recursively on all components in the following
|
||||||
order: `B`, `D`, `E`, `C`, `A`.
|
order: `B`, `D`, `E`, `C`, `A`.
|
||||||
|
|
||||||
**Scenario 2: rerendering a component**. Now, let's assume that the user clicked on some
|
**Scenario 2: rerendering a component**. Now, let's assume that the user clicked on some
|
||||||
@@ -705,7 +764,7 @@ button in `C`, and this results in a state update, which is supposed to:
|
|||||||
|
|
||||||
- update `D`,
|
- update `D`,
|
||||||
- remove `E`,
|
- remove `E`,
|
||||||
- add new widget `F`.
|
- add new component `F`.
|
||||||
|
|
||||||
So, the component tree should look like this:
|
So, the component tree should look like this:
|
||||||
|
|
||||||
@@ -722,17 +781,17 @@ Here is what Owl will do:
|
|||||||
1. because of a state change, the method `render` is called on `C`
|
1. because of a state change, the method `render` is called on `C`
|
||||||
2. template `C` is rendered again
|
2. template `C` is rendered again
|
||||||
|
|
||||||
- widget `D` is updated:
|
- component `D` is updated:
|
||||||
1. hook `willUpdateProps` is called on `D` (async)
|
1. hook `willUpdateProps` is called on `D` (async)
|
||||||
2. template `D` is rerendered
|
2. template `D` is rerendered
|
||||||
- widget `F` is created:
|
- component `F` is created:
|
||||||
1. hook `willStart` is called on `E` (async)
|
1. hook `willStart` is called on `E` (async)
|
||||||
2. template `F` is rendered
|
2. template `F` is rendered
|
||||||
|
|
||||||
3. `willPatch` hooks are called recursively on widgets `C`, `D` (not on `F`,
|
3. `willPatch` hooks are called recursively on components `C`, `D` (not on `F`,
|
||||||
because it is not mounted yet)
|
because it is not mounted yet)
|
||||||
|
|
||||||
4. widget `C` is patched, which will cause recursively:
|
4. component `C` is patched, which will cause recursively:
|
||||||
|
|
||||||
2. `willUnmount` hook on `E`, then destruction of `E`,
|
2. `willUnmount` hook on `E`, then destruction of `E`,
|
||||||
3. (initial) patching of `F`, then hook `mounted` is called on `F`
|
3. (initial) patching of `F`, then hook `mounted` is called on `F`
|
||||||
@@ -756,7 +815,8 @@ of the props. Here is how it works in Owl:
|
|||||||
- props are validated whenever a component is created/updated
|
- props are validated whenever a component is created/updated
|
||||||
- props are only validated in `dev` mode (see [tooling page](tooling.md#development-mode))
|
- props are only validated in `dev` mode (see [tooling page](tooling.md#development-mode))
|
||||||
- if a key does not match the description, an error is thrown
|
- if a key does not match the description, an error is thrown
|
||||||
- it only validates keys defined in (static) `props`. Additional keys in (component) `props` are not validated.
|
- it validates keys defined in (static) `props`. Additional keys given by the
|
||||||
|
parent will cause an error.
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
@@ -784,15 +844,16 @@ class ComponentB extends owl.Component {
|
|||||||
|
|
||||||
- it is an object or a list of strings
|
- it is an object or a list of strings
|
||||||
- a list of strings is a simplified props definition, which only lists the name
|
- a list of strings is a simplified props definition, which only lists the name
|
||||||
of the props.
|
of the props. Also, if the name ends with `?`, it is considered optional.
|
||||||
- all props are by default required, unless they are defined with `optional: true`
|
- all props are by default required, unless they are defined with `optional: true`
|
||||||
(in that case, validation is only done if there is a value)
|
(in that case, validation is only done if there is a value)
|
||||||
- valid types are: `Number, String, Boolean, Object, Array, Date, Function`, and all
|
- valid types are: `Number, String, Boolean, Object, Array, Date, Function`, and all
|
||||||
constructor functions (so, if you have a `Person` class, it can be used as a type)
|
constructor functions (so, if you have a `Person` class, it can be used as a type)
|
||||||
- arrays are homogeneous (all elements have the same type/shape)
|
- arrays are homogeneous (all elements have the same type/shape)
|
||||||
|
|
||||||
For each key, a `prop` definition is either a constructor, a list of constructors, or an object:
|
For each key, a `prop` definition is either a boolean, a constructor, a list of constructors, or an object:
|
||||||
|
|
||||||
|
- a boolean: indicate that the props exists, and is mandatory.
|
||||||
- a constructor: this should describe the type, for example: `id: Number` describe
|
- a constructor: this should describe the type, for example: `id: Number` describe
|
||||||
the props `id` as a number
|
the props `id` as a number
|
||||||
- a list of constructors. In that case, this means that we allow more than one
|
- a list of constructors. In that case, this means that we allow more than one
|
||||||
@@ -810,6 +871,11 @@ Examples:
|
|||||||
static props = ['message', 'id', 'date'];
|
static props = ['message', 'id', 'date'];
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
// size is optional
|
||||||
|
static props = ['message', 'size?'];
|
||||||
|
```
|
||||||
|
|
||||||
```js
|
```js
|
||||||
static props = {
|
static props = {
|
||||||
messageIds: {type: Array, element: Number}, // list of number
|
messageIds: {type: Array, element: Number}, // list of number
|
||||||
@@ -824,7 +890,8 @@ Examples:
|
|||||||
url: String
|
url: String
|
||||||
]}, // object, with keys id (number), name (string, optional) and url (string)
|
]}, // object, with keys id (number), name (string, optional) and url (string)
|
||||||
someFlag: Boolean, // a boolean, mandatory (even if `false`)
|
someFlag: Boolean, // a boolean, mandatory (even if `false`)
|
||||||
someVal: [Boolean, Date] // either a boolean or a date
|
someVal: [Boolean, Date], // either a boolean or a date
|
||||||
|
otherValue: true, // indicates that it is a prop
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -836,42 +903,42 @@ Like the `t-on` directive, it can work either on a DOM node, or on a component:
|
|||||||
```xml
|
```xml
|
||||||
<div>
|
<div>
|
||||||
<div t-ref="someDiv"/>
|
<div t-ref="someDiv"/>
|
||||||
<t t-widget="SubWidget" t-ref="someWidget"/>
|
<SubComponent t-ref="someComponent"/>
|
||||||
</div>
|
</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
In this example, the widget will be able to access the `div` and the component
|
In this example, the component will be able to access the `div` and the component
|
||||||
inside the special `refs` variable:
|
inside the special `refs` variable:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
this.refs.someDiv;
|
this.refs.someDiv;
|
||||||
this.refs.someWidget;
|
this.refs.someComponent;
|
||||||
```
|
```
|
||||||
|
|
||||||
This is useful for various usecases: for example, integrating with an external
|
This is useful for various usecases: for example, integrating with an external
|
||||||
library that needs to render itself inside an actual DOM node. Or for calling
|
library that needs to render itself inside an actual DOM node. Or for calling
|
||||||
some method on a sub widget.
|
some method on a sub component.
|
||||||
|
|
||||||
Note: if used on a component, the reference will be set in the `refs`
|
Note: if used on a component, the reference will be set in the `refs`
|
||||||
variable between `willPatch` and `patched`.
|
variable between `willPatch` and `patched`.
|
||||||
|
|
||||||
The `t-ref` directive also accepts dynamic values with string interpolation
|
The `t-ref` directive also accepts dynamic values with string interpolation
|
||||||
(like the [`t-attf-`](qweb.md#dynamic-attributes) and
|
(like the [`t-attf-`](qweb.md#dynamic-attributes) and
|
||||||
`t-widget` directives). For example, if we have
|
`t-component` directives). For example, if we have
|
||||||
`id` set to 44 in the rendering context,
|
`id` set to 44 in the rendering context,
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<div t-ref="widget_{{id}}"/>
|
<div t-ref="component_{{id}}"/>
|
||||||
```
|
```
|
||||||
|
|
||||||
```js
|
```js
|
||||||
this.refs.widget_44;
|
this.refs.component_44;
|
||||||
```
|
```
|
||||||
|
|
||||||
### Slots
|
### Slots
|
||||||
|
|
||||||
To make generic components, it is useful to be able for a parent widget to _inject_
|
To make generic components, it is useful to be able for a parent component to _inject_
|
||||||
some sub template, but still be the owner. For example, a generic dialog widget
|
some sub template, but still be the owner. For example, a generic dialog component
|
||||||
will need to render some content, some footer, but with the parent as the
|
will need to render some content, some footer, but with the parent as the
|
||||||
rendering context.
|
rendering context.
|
||||||
|
|
||||||
@@ -892,50 +959,31 @@ This is what _slots_ are for.
|
|||||||
Slots are defined by the caller, with the `t-set` directive:
|
Slots are defined by the caller, with the `t-set` directive:
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<div t-name="SomeWidget">
|
<div t-name="SomeComponent">
|
||||||
<div>some widget</div>
|
<div>some component</div>
|
||||||
<t t-widget="Dialog" title="Some Dialog">
|
<Dialog title="Some Dialog">
|
||||||
<t t-set="content">
|
<t t-set="content">
|
||||||
<div>hey</div>
|
<div>hey</div>
|
||||||
</t>
|
</t>
|
||||||
<t t-set="footer">
|
<t t-set="footer">
|
||||||
<button t-on-click="doSomething">ok</button>
|
<button t-on-click="doSomething">ok</button>
|
||||||
</t>
|
</t>
|
||||||
</t>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
In this example, the widget `Dialog` will render the slots `content` and `footer`
|
In this example, the component `Dialog` will render the slots `content` and `footer`
|
||||||
with its parent as rendering context. This means that clicking on the button
|
with its parent as rendering context. This means that clicking on the button
|
||||||
will execute the `doSomething` method on the parent, not on the dialog.
|
will execute the `doSomething` method on the parent, not on the dialog.
|
||||||
|
|
||||||
Warning! Slots have a technical constraint: the result of the slot rendering
|
Default slot: the first element inside the component which is not a named slot will
|
||||||
should have exactly one root node. So,
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<t t-set="content">
|
|
||||||
<div>A</div>
|
|
||||||
<div>B</div>
|
|
||||||
</t>
|
|
||||||
```
|
|
||||||
|
|
||||||
is not allowed. A workaround could be to wrap the content in a div:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<div t-set="content">
|
|
||||||
<div>A</div>
|
|
||||||
<div>B</div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
Default slot: the first element inside the widget which is not a named slot will
|
|
||||||
be considered the `default` slot. For example:
|
be considered the `default` slot. For example:
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<div t-name="Parent">
|
<div t-name="Parent">
|
||||||
<t t-widget="Child">
|
<Child>
|
||||||
<span>some content</span>
|
<span>some content</span>
|
||||||
</t>
|
</Child>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div t-name="Child">
|
<div t-name="Child">
|
||||||
@@ -952,23 +1000,23 @@ components.
|
|||||||
|
|
||||||
There are two different common problems with Owl asynchronous rendering model:
|
There are two different common problems with Owl asynchronous rendering model:
|
||||||
|
|
||||||
- any widget can delay the rendering (initial and subsequent) of the whole
|
- any component can delay the rendering (initial and subsequent) of the whole
|
||||||
application
|
application
|
||||||
- for a given widget, there are two independant situations that will trigger an
|
- for a given component, there are two independant situations that will trigger an
|
||||||
asynchronous rerendering: a change in the state, or a change in the props.
|
asynchronous rerendering: a change in the state, or a change in the props.
|
||||||
These changes may be done at different times, and Owl has no way of knowing
|
These changes may be done at different times, and Owl has no way of knowing
|
||||||
how to reconcile the resulting renderings.
|
how to reconcile the resulting renderings.
|
||||||
|
|
||||||
Here are a few tips on how to work with asynchronous widgets:
|
Here are a few tips on how to work with asynchronous components:
|
||||||
|
|
||||||
1. Minimize the use of asynchronous widgets!
|
1. Minimize the use of asynchronous components!
|
||||||
2. Maybe move the asynchronous logic in a store, which then triggers (mostly)
|
2. Maybe move the asynchronous logic in a store, which then triggers (mostly)
|
||||||
synchronous renderings
|
synchronous renderings
|
||||||
3. Lazy loading external libraries is a good use case for async rendering. This
|
3. Lazy loading external libraries is a good use case for async rendering. This
|
||||||
is mostly fine, because we can assume that it will only takes a fraction of a
|
is mostly fine, because we can assume that it will only takes a fraction of a
|
||||||
second, and only once (see `owl.utils.loadJS`)
|
second, and only once (see `owl.utils.loadJS`)
|
||||||
4. For all the other cases, the `t-asyncroot` directive (to use alongside
|
4. For all the other cases, the `t-asyncroot` directive (to use alongside
|
||||||
`t-widget`) is there to help you. When this directive is met, a new rendering
|
`t-component`) is there to help you. When this directive is met, a new rendering
|
||||||
sub tree is created, such that the rendering of that component (and its
|
sub tree is created, such that the rendering of that component (and its
|
||||||
children) is not tied to the rendering of the rest of the interface. It can
|
children) is not tied to the rendering of the rest of the interface. It can
|
||||||
be used on an asynchronous component, to prevent it from delaying the
|
be used on an asynchronous component, to prevent it from delaying the
|
||||||
@@ -978,8 +1026,67 @@ Here are a few tips on how to work with asynchronous widgets:
|
|||||||
(triggered by state or props changes).
|
(triggered by state or props changes).
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<div t-name="ParentWidget">
|
<div t-name="ParentComponent">
|
||||||
<t t-widget="SyncChild"/>
|
<SyncChild />
|
||||||
<t t-widget="AsyncChild" t-asyncroot="1"/>
|
<AsyncChild t-asyncroot="1"/>
|
||||||
</div>
|
</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
By default, whenever an error occurs in the rendering of an Owl application, we
|
||||||
|
destroy the whole application. Otherwise, we cannot offer any guarantee on the
|
||||||
|
state of the resulting component tree. It might be hopelessly corrupted, but
|
||||||
|
without any user-visible state.
|
||||||
|
|
||||||
|
Clearly, it sometimes is a little bit extreme to destroy the application. This
|
||||||
|
is why we have a builtin mechanism to handle rendering errors (and errors coming
|
||||||
|
from lifecycle hooks): the `catchError` hook.
|
||||||
|
|
||||||
|
Whenever the `catchError` lifecycle hook is implemented, all errors coming from
|
||||||
|
sub components rendering and/or lifecycle method calls will be caught and given
|
||||||
|
to the `catchError` method. This allow us to properly handle the error, and to
|
||||||
|
not break the application.
|
||||||
|
|
||||||
|
For example, here is how we could implement an `ErrorBoundary` component:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div t-name="ErrorBoundary">
|
||||||
|
<t t-if="state.error">
|
||||||
|
Error handled
|
||||||
|
</t>
|
||||||
|
<t t-else="1">
|
||||||
|
<t t-slot="default" />
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
class ErrorBoundary extends Widget {
|
||||||
|
state = { error: false };
|
||||||
|
|
||||||
|
catchError() {
|
||||||
|
this.state.error = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Using the `ErrorBoundary` is then extremely simple:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<ErrorBoundary><SomeOtherComponent/></ErrorBoundary>
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that we need to be careful here: the fallback UI should not throw any
|
||||||
|
error, otherwise we risk going into an infinite loop.
|
||||||
|
|
||||||
|
Also, it may be useful to know that whenever an error is caught, it is then
|
||||||
|
broadcasted to the application by an event on the `qweb` instance. It may be
|
||||||
|
useful, for example, to log the error somewhere.
|
||||||
|
|
||||||
|
```js
|
||||||
|
env.qweb.on("error", null, function(error) {
|
||||||
|
// do something
|
||||||
|
// react to the error
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|||||||
+4
-4
@@ -58,16 +58,16 @@ To build an application (or a sub-part of an application), we need two things:
|
|||||||
need. In practice, it could context some user session information, some
|
need. In practice, it could context some user session information, some
|
||||||
configuration keys (for example, isMobile = true/false if we are in mobile mode).
|
configuration keys (for example, isMobile = true/false if we are in mobile mode).
|
||||||
|
|
||||||
- a description of the user interface: there should be a root widget, which can
|
- a description of the user interface: there should be a root component, which can
|
||||||
have sub widgets
|
have sub components
|
||||||
|
|
||||||
Here are a few steps that we may take to get started:
|
Here are a few steps that we may take to get started:
|
||||||
|
|
||||||
- get the templates
|
- get the templates
|
||||||
- create a qweb engine, with the templates
|
- create a qweb engine, with the templates
|
||||||
- create an environment
|
- create an environment
|
||||||
- create an instance of the root widget
|
- create an instance of the root component
|
||||||
- mount the root widget to a DOM element
|
- mount the root component to a DOM element
|
||||||
|
|
||||||
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`:
|
||||||
|
|
||||||
|
|||||||
+24
-10
@@ -44,7 +44,7 @@ with a few interesting points:
|
|||||||
|
|
||||||
- it compiles templates into functions that output a virtual DOM instead of a
|
- it compiles templates into functions that output a virtual DOM instead of a
|
||||||
string. This is necessary for the component system.
|
string. This is necessary for the component system.
|
||||||
- it has a few extra directives: `t-widget`, `t-on`, ...
|
- it has a few extra directives: `t-component`, `t-on`, ...
|
||||||
|
|
||||||
## Directives
|
## Directives
|
||||||
|
|
||||||
@@ -66,8 +66,8 @@ The component system in Owl requires additional directives, to express various
|
|||||||
needs. Here is a list of all Owl specific directives:
|
needs. Here is a list of all Owl specific directives:
|
||||||
|
|
||||||
| Name | Description |
|
| Name | Description |
|
||||||
| -------------------------------------------- | ----------------------------------------------------------------------------------- |
|
| ------------------------------------------- | ----------------------------------------------------------------------------------- |
|
||||||
| `t-widget`, `t-keepalive`, `t-asyncroot` | [Defining a sub component](component.md#composition) |
|
| `t-component`, `t-keepalive`, `t-asyncroot` | [Defining a sub component](component.md#composition) |
|
||||||
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#references) |
|
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#references) |
|
||||||
| `t-key` | [Defining a key (to help virtual dom reconciliation)](component.md#t-key-directive) |
|
| `t-key` | [Defining a key (to help virtual dom reconciliation)](component.md#t-key-directive) |
|
||||||
| `t-on-*` | [Event handling](component.md#event-handling) |
|
| `t-on-*` | [Event handling](component.md#event-handling) |
|
||||||
@@ -95,12 +95,14 @@ It's API is quite simple:
|
|||||||
const qweb = new owl.QWeb(TEMPLATES);
|
const qweb = new owl.QWeb(TEMPLATES);
|
||||||
```
|
```
|
||||||
|
|
||||||
- **`addTemplate(name, xmlStr)`**: add a specific template.
|
- **`addTemplate(name, xmlStr, allowDuplicate)`**: add a specific template.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
qweb.addTemplate("mytemplate", "<div>hello</div>");
|
qweb.addTemplate("mytemplate", "<div>hello</div>");
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If the optional `allowDuplicate` is set to `true`, then `QWeb` will simply return whenever a template is added for a second time. Otherwise, `QWeb` will crash.
|
||||||
|
|
||||||
- **`addTemplates(xmlStr)`**: add a list of templates (identified by `t-name`
|
- **`addTemplates(xmlStr)`**: add a list of templates (identified by `t-name`
|
||||||
attribute).
|
attribute).
|
||||||
|
|
||||||
@@ -108,7 +110,7 @@ It's API is quite simple:
|
|||||||
const TEMPLATES = `
|
const TEMPLATES = `
|
||||||
<templates>
|
<templates>
|
||||||
<div t-name="App" class="main">main</div>
|
<div t-name="App" class="main">main</div>
|
||||||
<div t-name="OtherWidget">other widget</div>
|
<div t-name="OtherComponent">other component</div>
|
||||||
</templates>`;
|
</templates>`;
|
||||||
qweb.addTemplates(TEMPLATES);
|
qweb.addTemplates(TEMPLATES);
|
||||||
```
|
```
|
||||||
@@ -117,12 +119,19 @@ It's API is quite simple:
|
|||||||
which is a virtual representation of the DOM (see [vdom doc](vdom.md)).
|
which is a virtual representation of the DOM (see [vdom doc](vdom.md)).
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const vnode = qweb.render("App", widget);
|
const vnode = qweb.render("App", component);
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`renderToString(name, context)`**: renders a template, but returns an html
|
||||||
|
string.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const str = qweb.renderToString("someTemplate", somecontext);
|
||||||
```
|
```
|
||||||
|
|
||||||
- **`register(name, Component)`**: static function to register an OWL Component
|
- **`register(name, Component)`**: static function to register an OWL Component
|
||||||
to QWeb's global registry. Globally registered Components can be used in
|
to QWeb's global registry. Globally registered Components can be used in
|
||||||
templates (see the `t-widget` directive). This is useful for commonly used
|
templates (see the `t-component` directive). This is useful for commonly used
|
||||||
components accross the application.
|
components accross the application.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
@@ -131,10 +140,15 @@ It's API is quite simple:
|
|||||||
|
|
||||||
...
|
...
|
||||||
|
|
||||||
class ParentWidget extends owl.Component { ... }
|
class ParentComponent extends owl.Component { ... }
|
||||||
qweb.addTemplate("ParentWidget", "<div><t t-widget='Dialog'/></div>");
|
qweb.addTemplate("ParentComponent", "<div><Dialog/></div>");
|
||||||
```
|
```
|
||||||
|
|
||||||
|
In some way, a `QWeb` instance is the core of an Owl application. It is the only
|
||||||
|
mandatory element of an [environment](component.md#environment). As such, it
|
||||||
|
has an extra responsability: it can act as an event bus for internal communication
|
||||||
|
between Owl classes. This is the reason why `QWeb` actually extends [EventBus](event_bus.md).
|
||||||
|
|
||||||
## Reference
|
## Reference
|
||||||
|
|
||||||
We define in this section the specification of how `QWeb` templates should be
|
We define in this section the specification of how `QWeb` templates should be
|
||||||
@@ -179,7 +193,7 @@ root nodes.
|
|||||||
|
|
||||||
QWeb expressions are strings that will be processed at compile time. Each variable in
|
QWeb expressions are strings that will be processed at compile time. Each variable in
|
||||||
the javascript expression will be replaced by a lookup in the context (so, the
|
the javascript expression will be replaced by a lookup in the context (so, the
|
||||||
widget). For example, `a + b.c(d)` will be converted into:
|
component). For example, `a + b.c(d)` will be converted into:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
context["a"] + context["b"].c(context["d"]);
|
context["a"] + context["b"].c(context["d"]);
|
||||||
|
|||||||
@@ -1,5 +1,28 @@
|
|||||||
# 🦉 OWL Documentation 🦉
|
# 🦉 OWL Documentation 🦉
|
||||||
|
|
||||||
|
## Owl Content
|
||||||
|
|
||||||
|
Owl is a javascript library that contains some core classes and function to help
|
||||||
|
build applications. Here is a complete representation of its content:
|
||||||
|
|
||||||
|
```
|
||||||
|
owl
|
||||||
|
Component
|
||||||
|
QWeb
|
||||||
|
core
|
||||||
|
EventBus
|
||||||
|
Observer
|
||||||
|
store
|
||||||
|
Store
|
||||||
|
ConnectedComponent
|
||||||
|
utils
|
||||||
|
whenReady
|
||||||
|
loadJS
|
||||||
|
loadTemplates
|
||||||
|
escape
|
||||||
|
debounce
|
||||||
|
```
|
||||||
|
|
||||||
## Reference
|
## Reference
|
||||||
|
|
||||||
- [Animations](animations.md)
|
- [Animations](animations.md)
|
||||||
|
|||||||
+68
-23
@@ -10,6 +10,8 @@
|
|||||||
- [Actions](#actions)
|
- [Actions](#actions)
|
||||||
- [Getters](#getters)
|
- [Getters](#getters)
|
||||||
- [Connecting a Component](#connecting-a-component)
|
- [Connecting a Component](#connecting-a-component)
|
||||||
|
- [Semantics](#semantics)
|
||||||
|
- [Good Practices](#good-practices)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -84,6 +86,20 @@ object, or modifying an array with the `arr[i] = newValue` syntax). See the
|
|||||||
Mutations are the only way to modify the state. Changing the state outside a
|
Mutations are the only way to modify the state. Changing the state outside a
|
||||||
mutation is not allowed (and should throw an error). Mutations are synchronous.
|
mutation is not allowed (and should throw an error). Mutations are synchronous.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const mutations = {
|
||||||
|
setLoginState({ state }, loginState) {
|
||||||
|
state.loginState = loginState;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Mutations are called with the `commit` method on the store, and can receive an arbitrary number of arguments.
|
||||||
|
|
||||||
|
```js
|
||||||
|
store.commit("setLoginState", "error");
|
||||||
|
```
|
||||||
|
|
||||||
### Actions
|
### Actions
|
||||||
|
|
||||||
Actions are used to coordinate state changes. It is also useful whenever some
|
Actions are used to coordinate state changes. It is also useful whenever some
|
||||||
@@ -92,10 +108,10 @@ in an action.
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
const actions = {
|
const actions = {
|
||||||
async login({ commit }) {
|
async login({ commit }, info) {
|
||||||
commit("setLoginState", "pending");
|
commit("setLoginState", "pending");
|
||||||
try {
|
try {
|
||||||
const loginInfo = await doSomeRPC("/login/", "someinfo");
|
const loginInfo = await doSomeRPC("/login/", info);
|
||||||
commit("setLoginState", loginInfo);
|
commit("setLoginState", loginInfo);
|
||||||
} catch {
|
} catch {
|
||||||
commit("setLoginState", "error");
|
commit("setLoginState", "error");
|
||||||
@@ -104,6 +120,16 @@ const actions = {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Actions are called with the `dispatch` method on the store, and can receive an
|
||||||
|
arbitrary number of arguments.
|
||||||
|
|
||||||
|
```js
|
||||||
|
store.dispatch("login", someInfo);
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that anything returned by an action will also be returned by the `dispatch`
|
||||||
|
call.
|
||||||
|
|
||||||
### Getters
|
### Getters
|
||||||
|
|
||||||
Usually, data contained in the store will be stored in a normalized way. For
|
Usually, data contained in the store will be stored in a normalized way. For
|
||||||
@@ -142,17 +168,16 @@ const getters = {
|
|||||||
const post = store.getters.getPost(id);
|
const post = store.getters.getPost(id);
|
||||||
```
|
```
|
||||||
|
|
||||||
Getters take *at most* one argument.
|
Getters take _at most_ one argument.
|
||||||
|
|
||||||
|
|
||||||
Note that getters are cached if they don't take any argument, or their argument
|
Note that getters are cached if they don't take any argument, or their argument
|
||||||
is a string or a number.
|
is a string or a number.
|
||||||
|
|
||||||
### Connecting a Component
|
### Connecting a Component
|
||||||
|
|
||||||
By default, an Owl `Component` is not connected to any store. The `connect`
|
At some point, we need a way to access the state in the store from a component.
|
||||||
function is there to create sub Components that are connected versions of
|
By default, an Owl `Component` is not connected to any store. To do that, we
|
||||||
Components.
|
need to create a component inheriting from `OwlComponent`:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const actions = {
|
const actions = {
|
||||||
@@ -170,19 +195,18 @@ const state = {
|
|||||||
};
|
};
|
||||||
const store = new owl.Store({ state, actions, mutations });
|
const store = new owl.Store({ state, actions, mutations });
|
||||||
|
|
||||||
class Counter extends owl.Component {
|
class Counter extends owl.ConnectedComponent {
|
||||||
increment() {
|
static mapStoreToProps(state) {
|
||||||
this.env.store.dispatch("increment");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function mapStoreToProps(state) {
|
|
||||||
return {
|
return {
|
||||||
value: state.counter
|
value: state.counter
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const ConnectedCounter = owl.connect(Counter, mapStoreToProps);
|
increment() {
|
||||||
|
this.env.store.dispatch("increment");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const counter = new ConnectedCounter({ store, qweb });
|
const counter = new Counter({ store, qweb });
|
||||||
```
|
```
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
@@ -191,19 +215,40 @@ const counter = new ConnectedCounter({ store, qweb });
|
|||||||
</button>
|
</button>
|
||||||
```
|
```
|
||||||
|
|
||||||
The arguments of `connect` are:
|
The `ConnectedComponent` class can be configured with the following fields:
|
||||||
|
|
||||||
- `Counter`: an owl `Component` to connect
|
|
||||||
- `mapStoreToProps`: a function that extracts the `props` of the Component
|
- `mapStoreToProps`: a function that extracts the `props` of the Component
|
||||||
from the `state` of the `Store` and returns them as a dict
|
from the `state` of the `Store` and returns them as a dict.
|
||||||
- `options`: dictionary of optional parameters that may contain
|
|
||||||
- `getStore`: a function that takes the `env` in arguments and returns an
|
- `getStore`: a function that takes the `env` in arguments and returns an
|
||||||
instance of `Store` to connect to (if not given, connects to `env.store`)
|
instance of `Store` to connect to (if not given, connects to `env.store`)
|
||||||
- `hashFunction`: the function to use to detect changes in the state (if not
|
- `hashFunction`: the function to use to detect changes in the state (if not
|
||||||
given, generates a function that uses revision numbers, incremented at
|
given, generates a function that uses revision numbers, incremented at
|
||||||
each state change)
|
each state change)
|
||||||
- `deep`: [only useful if no hashFunction is given] if false, only watch
|
- `deep` (boolean): [only useful if no hashFunction is given] if `false`, only watch
|
||||||
for top level state changes (true by default)
|
for top level state changes (`true` by default)
|
||||||
|
|
||||||
The `connect` function returns a sub class of the given `Component` which is
|
### Semantics
|
||||||
connected to the `store`.
|
|
||||||
|
The `Store` and the `ConnectedComponent` try to be smart and to optimize as much
|
||||||
|
as possible the rendering and update process. What is important to know is:
|
||||||
|
|
||||||
|
- components are always updated in the order of their creation (so, parent
|
||||||
|
before children)
|
||||||
|
- they are updated only if they are in the DOM
|
||||||
|
- if a parent is asynchronous, the system will wait for it to complete its
|
||||||
|
update before updating other components.
|
||||||
|
- in general, updates are not coordinated. This is not a problem for synchronous
|
||||||
|
components, but if there are many asynchronous components, this could lead to
|
||||||
|
a situation where some part of the UI is updated and other parts of the UI is
|
||||||
|
not updated.
|
||||||
|
|
||||||
|
### Good Practices
|
||||||
|
|
||||||
|
- avoid asynchronous components as much as possible. Asynchronous components
|
||||||
|
lead to situations where parts of the UI is not updated immediately.
|
||||||
|
- do not be afraid to connect many components, parent or children if needed. For
|
||||||
|
example, a `MessageList` component could get a list of ids in its `mapStoreToProps` and a `Message` component could get the data of its own
|
||||||
|
message
|
||||||
|
- since the `mapStoreToProps` function is called for each connected component,
|
||||||
|
for each state update, it is important to make sure that these functions are
|
||||||
|
as fast as possible.
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ owl.__info__.mode = "dev";
|
|||||||
Note that templates compiled with the `prod` settings will not be recompiled.
|
Note that templates compiled with the `prod` settings will not be recompiled.
|
||||||
So, changing this setting is best done at startup.
|
So, changing this setting is best done at startup.
|
||||||
|
|
||||||
|
An important job done by the `dev` mode is to validate props for each component
|
||||||
|
creation and update. Also, extra props will cause an error.
|
||||||
|
|
||||||
## Playground
|
## Playground
|
||||||
|
|
||||||
The playground is an important application designed to help learning and
|
The playground is an important application designed to help learning and
|
||||||
|
|||||||
+7
-3
@@ -1,12 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "owl",
|
"name": "owl",
|
||||||
"version": "0.15.0",
|
"version": "0.18.0",
|
||||||
"description": "Odoo Web Library (OWL)",
|
"description": "Odoo Web Library (OWL)",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:js": "tsc --target esnext --module es6 --outDir dist/owl",
|
"build:js": "tsc --target esnext --module es6 --outDir dist/owl",
|
||||||
"build:bundle": "rollup -c",
|
"build:bundle": "rollup -c",
|
||||||
"build": "npm run build:js && npm run build:bundle",
|
"build": "npm run build:js && npm run build:bundle",
|
||||||
|
"buildcommonjs": "npm run build:js && npm run build:bundle -- -f cjs",
|
||||||
"minify": "uglifyjs dist/owl.js -o dist/owl.min.js --compress --mangle",
|
"minify": "uglifyjs dist/owl.js -o dist/owl.min.js --compress --mangle",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
@@ -30,6 +31,7 @@
|
|||||||
"cpx": "^1.5.0",
|
"cpx": "^1.5.0",
|
||||||
"git-rev-sync": "^1.12.0",
|
"git-rev-sync": "^1.12.0",
|
||||||
"jest": "^23.6.0",
|
"jest": "^23.6.0",
|
||||||
|
"jest-environment-jsdom": "^24.7.1",
|
||||||
"live-server": "^1.2.1",
|
"live-server": "^1.2.1",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"rollup": "^1.6.0",
|
"rollup": "^1.6.0",
|
||||||
@@ -38,8 +40,7 @@
|
|||||||
"source-map-support": "^0.5.10",
|
"source-map-support": "^0.5.10",
|
||||||
"ts-jest": "^23.10.5",
|
"ts-jest": "^23.10.5",
|
||||||
"typescript": "^3.2.2",
|
"typescript": "^3.2.2",
|
||||||
"uglify-es": "^3.3.9",
|
"uglify-es": "^3.3.9"
|
||||||
"jest-environment-jsdom": "^24.7.1"
|
|
||||||
},
|
},
|
||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
"jest": {
|
"jest": {
|
||||||
@@ -60,5 +61,8 @@
|
|||||||
"json",
|
"json",
|
||||||
"node"
|
"node"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"printWidth": 100
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Observer } from "./observer";
|
import { Observer } from "../core/observer";
|
||||||
import { QWeb, CompiledTemplate } from "./qweb_core";
|
import { CompiledTemplate, QWeb } from "../qweb/index";
|
||||||
import { h, patch, VNode } from "./vdom";
|
import { h, patch, VNode } from "../vdom/index";
|
||||||
|
import "./directive";
|
||||||
|
import "./props_validation";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Owl Component System
|
* Owl Component System
|
||||||
@@ -28,6 +30,7 @@ import { h, patch, VNode } from "./vdom";
|
|||||||
*/
|
*/
|
||||||
export interface Env {
|
export interface Env {
|
||||||
qweb: QWeb;
|
qweb: QWeb;
|
||||||
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,17 +45,23 @@ export interface Meta<T extends Env, Props> {
|
|||||||
isDestroyed: boolean;
|
isDestroyed: boolean;
|
||||||
parent: Component<T, any, any> | null;
|
parent: Component<T, any, any> | null;
|
||||||
children: { [key: number]: Component<T, any, any> };
|
children: { [key: number]: Component<T, any, any> };
|
||||||
// children mapping: from templateID to widgetID
|
// children mapping: from templateID to componentID
|
||||||
// should it be a map number => Widget?
|
// should it be a map number => Component?
|
||||||
cmap: { [key: number]: number };
|
cmap: { [key: number]: number };
|
||||||
|
|
||||||
renderId: number;
|
renderId: number;
|
||||||
|
|
||||||
|
// 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;
|
renderProps: Props | null;
|
||||||
renderPromise: Promise<VNode> | null;
|
renderPromise: Promise<VNode> | null;
|
||||||
|
|
||||||
boundHandlers: { [key: number]: any };
|
boundHandlers: { [key: number]: any };
|
||||||
observer?: Observer;
|
observer?: Observer;
|
||||||
render?: CompiledTemplate;
|
render?: CompiledTemplate;
|
||||||
mountedHandlers: { [key: number]: Function };
|
mountedHandlers: { [key: number]: Function };
|
||||||
|
classObj?: { [key: string]: boolean };
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a component does not define explicitely a template
|
// If a component does not define explicitely a template
|
||||||
@@ -62,7 +71,7 @@ export interface Meta<T extends Env, Props> {
|
|||||||
const TEMPLATE_MAP: { [key: number]: { [name: string]: string } } = {};
|
const TEMPLATE_MAP: { [key: number]: { [name: string]: string } } = {};
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Widget
|
// Component
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
let nextId = 1;
|
let nextId = 1;
|
||||||
|
|
||||||
@@ -71,8 +80,8 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
template?: string;
|
template?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `el` is the root element of the widget. Note that it could be null:
|
* The `el` is the root element of the component. Note that it could be null:
|
||||||
* this is the case if the widget is not mounted yet, or is destroyed.
|
* this is the case if the component is not mounted yet, or is destroyed.
|
||||||
*/
|
*/
|
||||||
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;
|
||||||
@@ -97,34 +106,31 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
/**
|
/**
|
||||||
* Creates an instance of Component.
|
* Creates an instance of Component.
|
||||||
*
|
*
|
||||||
* The root widget of a component tree needs an environment:
|
* The root component of a component tree needs an environment:
|
||||||
*
|
*
|
||||||
* ```javascript
|
* ```javascript
|
||||||
* const root = new RootWidget(env, props);
|
* const root = new RootComponent(env, props);
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* Every other widget simply needs a reference to its parent:
|
* Every other component simply needs a reference to its parent:
|
||||||
*
|
*
|
||||||
* ```javascript
|
* ```javascript
|
||||||
* const child = new SomeWidget(parent, props);
|
* const child = new SomeComponent(parent, props);
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* Note that most of the time, only the root widget needs to be created by
|
* Note that most of the time, only the root component needs to be created by
|
||||||
* hand. Other widgets should be created automatically by the framework (with
|
* hand. Other components should be created automatically by the framework (with
|
||||||
* the t-widget 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, any> | T, props?: Props) {
|
||||||
const defaultProps = (<any>this.constructor).defaultProps;
|
const defaultProps = (<any>this.constructor).defaultProps;
|
||||||
if (defaultProps) {
|
if (defaultProps) {
|
||||||
props = this._applyDefaultProps(props, defaultProps);
|
props = this.__applyDefaultProps(props, defaultProps);
|
||||||
}
|
|
||||||
if (QWeb.dev) {
|
|
||||||
this._validateProps(props || {});
|
|
||||||
}
|
}
|
||||||
// is this a good idea?
|
// is this a good idea?
|
||||||
// Pro: if props is empty, we can create easily a widget
|
// Pro: if props is empty, we can create easily a component
|
||||||
// Con: this is not really safe
|
// Con: this is not really safe
|
||||||
// Pro: but creating widget (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, any> | null = null;
|
||||||
@@ -134,6 +140,24 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
parent.__owl__.children[id] = this;
|
parent.__owl__.children[id] = this;
|
||||||
} else {
|
} else {
|
||||||
this.env = parent;
|
this.env = parent;
|
||||||
|
if (QWeb.dev) {
|
||||||
|
// we only validate props for root widgets here. "Regular" widget
|
||||||
|
// props are validated by the t-component directive
|
||||||
|
QWeb.utils.validateProps(this.constructor, this.props);
|
||||||
|
}
|
||||||
|
this.env.qweb.on("update", this, () => {
|
||||||
|
if (this.__owl__.isMounted) {
|
||||||
|
this.render(true);
|
||||||
|
}
|
||||||
|
if (this.__owl__.isDestroyed) {
|
||||||
|
// this is unlikely to happen, but if a root widget is destroyed,
|
||||||
|
// we want to remove our subscription. The usual way to do that
|
||||||
|
// would be to perform some check in the destroy method, but since
|
||||||
|
// it is very performance sensitive, and since this is a rare event,
|
||||||
|
// we simply do it lazily
|
||||||
|
this.env.qweb.off("update", this);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
this.__owl__ = {
|
this.__owl__ = {
|
||||||
id: id,
|
id: id,
|
||||||
@@ -157,7 +181,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
*
|
*
|
||||||
* It will be called exactly once before the initial rendering. It is useful
|
* It will be called exactly once before the initial rendering. It is useful
|
||||||
* in some cases, for example, to load external assets (such as a JS library)
|
* in some cases, for example, to load external assets (such as a JS library)
|
||||||
* before the widget is rendered.
|
* before the component is rendered.
|
||||||
*
|
*
|
||||||
* Note that a slow willStart method will slow down the rendering of the user
|
* Note that a slow willStart method will slow down the rendering of the user
|
||||||
* interface. Therefore, some effort should be made to make this method as
|
* interface. Therefore, some effort should be made to make this method as
|
||||||
@@ -207,7 +231,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
* with the DOM (for example, through an external library) whenever the
|
* with the DOM (for example, through an external library) whenever the
|
||||||
* component was updated.
|
* component was updated.
|
||||||
*
|
*
|
||||||
* Updating the widget state in this hook is possible, but not encouraged.
|
* Updating the component state in this hook is possible, but not encouraged.
|
||||||
* One need to be careful, because updates here will cause rerender, which in
|
* 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.
|
||||||
@@ -227,6 +251,12 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
*/
|
*/
|
||||||
willUnmount() {}
|
willUnmount() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* catchError is a method called whenever some error happens in the rendering or
|
||||||
|
* lifecycle hooks of a child.
|
||||||
|
*/
|
||||||
|
catchError(error: Error): void {}
|
||||||
|
|
||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
// Public
|
// Public
|
||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
@@ -236,29 +266,38 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
*
|
*
|
||||||
* This should only be done if the component was created manually. Components
|
* This should only be done if the component was created manually. Components
|
||||||
* created declaratively in templates are managed by the Owl system.
|
* created declaratively in templates are managed by the Owl system.
|
||||||
|
*
|
||||||
|
* Note that a component can be mounted an unmounted several times
|
||||||
*/
|
*/
|
||||||
async mount(target: HTMLElement): Promise<void> {
|
async mount(target: HTMLElement): Promise<void> {
|
||||||
const vnode = await this._prepare();
|
if (this.__owl__.isMounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.__owl__.vnode ) {
|
||||||
|
// we use the fact that renderId === 1 as a way to determine that the
|
||||||
|
// component is mounted for the first time
|
||||||
|
const vnode = await this.__prepare();
|
||||||
if (this.__owl__.isDestroyed) {
|
if (this.__owl__.isDestroyed) {
|
||||||
// component was destroyed before we get here...
|
// component was destroyed before we get here...
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._patch(vnode);
|
this.__patch(vnode);
|
||||||
|
}
|
||||||
target.appendChild(this.el!);
|
target.appendChild(this.el!);
|
||||||
|
|
||||||
if (document.body.contains(target)) {
|
if (document.body.contains(target)) {
|
||||||
this._callMounted();
|
this.__callMounted();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
unmount() {
|
unmount() {
|
||||||
if (this.__owl__.isMounted) {
|
if (this.__owl__.isMounted) {
|
||||||
this._callWillUnmount();
|
this.__callWillUnmount();
|
||||||
this.el!.remove();
|
this.el!.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async render(force: boolean = false, patchQueue?: any[]): Promise<void> {
|
async render(force: boolean = false, patchQueue?: any[], scope?: any, vars?: any): Promise<void> {
|
||||||
const __owl__ = this.__owl__;
|
const __owl__ = this.__owl__;
|
||||||
if (!__owl__.isMounted) {
|
if (!__owl__.isMounted) {
|
||||||
return;
|
return;
|
||||||
@@ -267,14 +306,14 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
if (shouldPatch) {
|
if (shouldPatch) {
|
||||||
patchQueue = [];
|
patchQueue = [];
|
||||||
}
|
}
|
||||||
const renderVDom = this._render(force, patchQueue);
|
__owl__.renderId++
|
||||||
const renderId = __owl__.renderId;
|
const renderId = __owl__.renderId;
|
||||||
await renderVDom;
|
await this.__render(force, patchQueue, scope, vars);
|
||||||
|
|
||||||
if (shouldPatch && __owl__.isMounted && renderId === __owl__.renderId) {
|
if (shouldPatch && __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(<any[]>patchQueue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,14 +323,14 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
* - call the willUnmount hooks if necessary
|
* - call the willUnmount hooks if necessary
|
||||||
* - remove the dom node from the dom
|
* - remove the dom node from the dom
|
||||||
*
|
*
|
||||||
* This should only be called manually if you created the widget. Most widgets
|
* This should only be called manually if you created the component. Most
|
||||||
* will be automatically destroyed.
|
* components will be automatically destroyed.
|
||||||
*/
|
*/
|
||||||
destroy() {
|
destroy() {
|
||||||
const __owl__ = this.__owl__;
|
const __owl__ = this.__owl__;
|
||||||
if (!__owl__.isDestroyed) {
|
if (!__owl__.isDestroyed) {
|
||||||
const el = this.el;
|
const el = this.el;
|
||||||
this._destroy(__owl__.parent);
|
this.__destroy(__owl__.parent);
|
||||||
if (el) {
|
if (el) {
|
||||||
el.remove();
|
el.remove();
|
||||||
}
|
}
|
||||||
@@ -308,8 +347,8 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method is the correct way to update the environment of a widget. Doing
|
* This method is the correct way to update the environment of a component. Doing
|
||||||
* this will cause a full rerender of the widget and its children, so this is
|
* this will cause a full rerender of the component and its children, so this is
|
||||||
* an operation that should not be done frequently.
|
* an operation that should not be done frequently.
|
||||||
*
|
*
|
||||||
* A good usecase for updating the environment would be to update some mostly
|
* A good usecase for updating the environment would be to update some mostly
|
||||||
@@ -348,7 +387,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
// Private
|
// Private
|
||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
_destroy(parent: Component<any, any, any> | null) {
|
__destroy(parent: Component<any, any, any> | null) {
|
||||||
const __owl__ = this.__owl__;
|
const __owl__ = this.__owl__;
|
||||||
const isMounted = __owl__.isMounted;
|
const isMounted = __owl__.isMounted;
|
||||||
if (isMounted) {
|
if (isMounted) {
|
||||||
@@ -357,7 +396,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
}
|
}
|
||||||
const children = __owl__.children;
|
const children = __owl__.children;
|
||||||
for (let key in children) {
|
for (let key in children) {
|
||||||
children[key]._destroy(this);
|
children[key].__destroy(this);
|
||||||
}
|
}
|
||||||
if (parent) {
|
if (parent) {
|
||||||
let id = __owl__.id;
|
let id = __owl__.id;
|
||||||
@@ -368,13 +407,13 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
delete __owl__.vnode;
|
delete __owl__.vnode;
|
||||||
}
|
}
|
||||||
|
|
||||||
_callMounted() {
|
__callMounted() {
|
||||||
const __owl__ = this.__owl__;
|
const __owl__ = this.__owl__;
|
||||||
const children = __owl__.children;
|
const children = __owl__.children;
|
||||||
for (let id in children) {
|
for (let id in children) {
|
||||||
const comp = children[id];
|
const comp = children[id];
|
||||||
if (!comp.__owl__.isMounted && this.el!.contains(comp.el)) {
|
if (!comp.__owl__.isMounted && this.el!.contains(comp.el)) {
|
||||||
comp._callMounted();
|
comp.__callMounted();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
__owl__.isMounted = true;
|
__owl__.isMounted = true;
|
||||||
@@ -382,10 +421,14 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
for (let key in handlers) {
|
for (let key in handlers) {
|
||||||
handlers[key]();
|
handlers[key]();
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
this.mounted();
|
this.mounted();
|
||||||
|
} catch (e) {
|
||||||
|
errorHandler(e, this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_callWillUnmount() {
|
__callWillUnmount() {
|
||||||
this.willUnmount();
|
this.willUnmount();
|
||||||
const __owl__ = this.__owl__;
|
const __owl__ = this.__owl__;
|
||||||
__owl__.isMounted = false;
|
__owl__.isMounted = false;
|
||||||
@@ -393,46 +436,53 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
for (let id in children) {
|
for (let id in children) {
|
||||||
const comp = children[id];
|
const comp = children[id];
|
||||||
if (comp.__owl__.isMounted) {
|
if (comp.__owl__.isMounted) {
|
||||||
comp._callWillUnmount();
|
comp.__callWillUnmount();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _updateProps(
|
async __updateProps(
|
||||||
nextProps: Props,
|
nextProps: Props,
|
||||||
forceUpdate: boolean = false,
|
forceUpdate: boolean = false,
|
||||||
patchQueue?: any[]
|
patchQueue?: any[],
|
||||||
|
scope?: any,
|
||||||
|
vars?: any
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
|
const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
|
||||||
if (shouldUpdate) {
|
if (shouldUpdate) {
|
||||||
const defaultProps = (<any>this.constructor).defaultProps;
|
const defaultProps = (<any>this.constructor).defaultProps;
|
||||||
if (defaultProps) {
|
if (defaultProps) {
|
||||||
nextProps = this._applyDefaultProps(nextProps, defaultProps);
|
nextProps = this.__applyDefaultProps(nextProps, defaultProps);
|
||||||
}
|
|
||||||
if (QWeb.dev) {
|
|
||||||
this._validateProps(nextProps);
|
|
||||||
}
|
}
|
||||||
await this.willUpdateProps(nextProps);
|
await this.willUpdateProps(nextProps);
|
||||||
this.props = nextProps;
|
this.props = nextProps;
|
||||||
await this.render(forceUpdate, patchQueue);
|
await this.render(forceUpdate, patchQueue, scope, vars);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_patch(vnode) {
|
__patch(vnode) {
|
||||||
const __owl__ = this.__owl__;
|
const __owl__ = this.__owl__;
|
||||||
__owl__.renderPromise = null;
|
|
||||||
const target = __owl__.vnode || document.createElement(vnode.sel!);
|
const target = __owl__.vnode || document.createElement(vnode.sel!);
|
||||||
|
if (this.__owl__.classObj) {
|
||||||
|
(<any>vnode).data.class = Object.assign((<any>vnode).data.class || {}, this.__owl__.classObj);
|
||||||
|
}
|
||||||
__owl__.vnode = patch(target, vnode);
|
__owl__.vnode = patch(target, vnode);
|
||||||
}
|
}
|
||||||
_prepare(): Promise<VNode> {
|
|
||||||
|
__prepare(scope?: Object, vars?: any): Promise<VNode> {
|
||||||
const __owl__ = this.__owl__;
|
const __owl__ = this.__owl__;
|
||||||
__owl__.renderProps = this.props;
|
__owl__.renderProps = this.props;
|
||||||
__owl__.renderPromise = this._prepareAndRender();
|
__owl__.renderPromise = this.__prepareAndRender(scope, vars);
|
||||||
return __owl__.renderPromise;
|
return __owl__.renderPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _prepareAndRender(): Promise<VNode> {
|
async __prepareAndRender(scope?: Object, vars?: any): Promise<VNode> {
|
||||||
|
try {
|
||||||
await this.willStart();
|
await this.willStart();
|
||||||
|
} catch (e) {
|
||||||
|
errorHandler(e, this);
|
||||||
|
return Promise.resolve(h("div"));
|
||||||
|
}
|
||||||
const __owl__ = this.__owl__;
|
const __owl__ = this.__owl__;
|
||||||
if (__owl__.isDestroyed) {
|
if (__owl__.isDestroyed) {
|
||||||
return Promise.resolve(h("div"));
|
return Promise.resolve(h("div"));
|
||||||
@@ -450,17 +500,11 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
if (template) {
|
if (template) {
|
||||||
this.template = template;
|
this.template = template;
|
||||||
} else {
|
} else {
|
||||||
while (
|
while ((template = p.name) && !(template in qweb.templates) && p !== Component) {
|
||||||
(template = p.name) &&
|
|
||||||
!(template in qweb.templates) &&
|
|
||||||
p !== Component
|
|
||||||
) {
|
|
||||||
p = p.__proto__;
|
p = p.__proto__;
|
||||||
}
|
}
|
||||||
if (p === Component) {
|
if (p === Component) {
|
||||||
throw new Error(
|
throw new Error(`Could not find template for component "${this.constructor.name}"`);
|
||||||
`Could not find template for component "${this.constructor.name}"`
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
tmap[name] = template;
|
tmap[name] = template;
|
||||||
this.template = template;
|
this.template = template;
|
||||||
@@ -468,16 +512,17 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
__owl__.render = qweb.render.bind(qweb, this.template);
|
__owl__.render = qweb.render.bind(qweb, this.template);
|
||||||
this._observeState();
|
this.__observeState();
|
||||||
return this._render();
|
return this.__render(false, [], scope, vars);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _render(
|
__render(
|
||||||
force: boolean = false,
|
force: boolean = false,
|
||||||
patchQueue: any[] = []
|
patchQueue: any[] = [],
|
||||||
|
scope?: Object,
|
||||||
|
vars?: any
|
||||||
): Promise<VNode> {
|
): Promise<VNode> {
|
||||||
const __owl__ = this.__owl__;
|
const __owl__ = this.__owl__;
|
||||||
__owl__.renderId++;
|
|
||||||
const promises: Promise<void>[] = [];
|
const promises: Promise<void>[] = [];
|
||||||
const patch: any[] = [this];
|
const patch: any[] = [this];
|
||||||
if (__owl__.isMounted) {
|
if (__owl__.isMounted) {
|
||||||
@@ -486,45 +531,54 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
if (__owl__.observer) {
|
if (__owl__.observer) {
|
||||||
__owl__.observer.allowMutations = false;
|
__owl__.observer.allowMutations = false;
|
||||||
}
|
}
|
||||||
let vnode = __owl__.render!(this, {
|
let vnode;
|
||||||
|
try {
|
||||||
|
vnode = __owl__.render!(this, {
|
||||||
promises,
|
promises,
|
||||||
handlers: __owl__.boundHandlers,
|
handlers: __owl__.boundHandlers,
|
||||||
mountedHandlers: __owl__.mountedHandlers,
|
mountedHandlers: __owl__.mountedHandlers,
|
||||||
forceUpdate: force,
|
forceUpdate: force,
|
||||||
patchQueue
|
patchQueue,
|
||||||
|
scope,
|
||||||
|
vars
|
||||||
});
|
});
|
||||||
|
} catch (e) {
|
||||||
|
vnode = __owl__.vnode || h("div");
|
||||||
|
errorHandler(e, this);
|
||||||
|
}
|
||||||
patch.push(vnode);
|
patch.push(vnode);
|
||||||
if (__owl__.observer) {
|
if (__owl__.observer) {
|
||||||
__owl__.observer.allowMutations = true;
|
__owl__.observer.allowMutations = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// this part is critical for the patching process to be done correctly. The
|
// this part is critical for the patching process to be done correctly. The
|
||||||
// tricky part is that a child widget can be rerendered on its own, which
|
// tricky part is that a child component can be rerendered on its own, which
|
||||||
// will update its own vnode representation without the knowledge of the
|
// will update its own vnode representation without the knowledge of the
|
||||||
// parent widget. With this, we make sure that the parent widget will be
|
// parent component. With this, we make sure that the parent component will be
|
||||||
// able to patch itself properly after
|
// able to patch itself properly after
|
||||||
vnode.key = __owl__.id;
|
vnode.key = __owl__.id;
|
||||||
__owl__.renderProps = this.props;
|
return Promise.all(promises).then(() => vnode);
|
||||||
__owl__.renderPromise = Promise.all(promises).then(() => vnode);
|
|
||||||
return __owl__.renderPromise;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Only called by qweb t-widget directive
|
* Only called by qweb t-component directive
|
||||||
*/
|
*/
|
||||||
_mount(vnode: VNode, elm: HTMLElement): VNode {
|
__mount(vnode: VNode, elm: HTMLElement): VNode {
|
||||||
const __owl__ = this.__owl__;
|
const __owl__ = this.__owl__;
|
||||||
|
if (__owl__.classObj) {
|
||||||
|
(<any>vnode).data.class = Object.assign((<any>vnode).data.class || {}, __owl__.classObj);
|
||||||
|
}
|
||||||
__owl__.vnode = patch(elm, vnode);
|
__owl__.vnode = patch(elm, vnode);
|
||||||
if (__owl__.parent!.__owl__.isMounted && !__owl__.isMounted) {
|
if (__owl__.parent!.__owl__.isMounted && !__owl__.isMounted) {
|
||||||
this._callMounted();
|
this.__callMounted();
|
||||||
}
|
}
|
||||||
return __owl__.vnode;
|
return __owl__.vnode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Only called by qweb t-widget directive (when t-keepalive is set)
|
* Only called by qweb t-component directive (when t-keepalive is set)
|
||||||
*/
|
*/
|
||||||
_remount() {
|
__remount() {
|
||||||
const __owl__ = this.__owl__;
|
const __owl__ = this.__owl__;
|
||||||
if (!__owl__.isMounted) {
|
if (!__owl__.isMounted) {
|
||||||
__owl__.isMounted = true;
|
__owl__.isMounted = true;
|
||||||
@@ -532,11 +586,11 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_observeState() {
|
__observeState() {
|
||||||
if (this.state) {
|
if (this.state) {
|
||||||
const __owl__ = this.__owl__;
|
const __owl__ = this.__owl__;
|
||||||
__owl__.observer = new Observer();
|
__owl__.observer = new Observer();
|
||||||
__owl__.observer.observe(this.state);
|
this.state = __owl__.observer.observe(this.state);
|
||||||
__owl__.observer.notifyCB = this.render.bind(this);
|
__owl__.observer.notifyCB = this.render.bind(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -547,8 +601,8 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
* Note that this method does not modify in place the props, it returns a new
|
* Note that this method does not modify in place the props, it returns a new
|
||||||
* prop object
|
* prop object
|
||||||
*/
|
*/
|
||||||
_applyDefaultProps(props: Object | undefined, defaultProps: Object): Props {
|
__applyDefaultProps(props: Object | undefined, defaultProps: Object): Props {
|
||||||
props = props ? Object.create(props) : {};
|
props = props ? Object.assign({}, props) : {};
|
||||||
for (let propName in defaultProps) {
|
for (let propName in defaultProps) {
|
||||||
if (props![propName] === undefined) {
|
if (props![propName] === undefined) {
|
||||||
props![propName] = defaultProps[propName];
|
props![propName] = defaultProps[propName];
|
||||||
@@ -561,106 +615,54 @@ 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. A patch is a pair [c, vn], where c is a
|
||||||
* Component instance and vn a VNode.
|
* 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 inverse order
|
||||||
*/
|
*/
|
||||||
_applyPatchQueue(patchQueue: any[]) {
|
__applyPatchQueue(patchQueue: any[]) {
|
||||||
|
let component = this;
|
||||||
|
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];
|
const patch = patchQueue[i];
|
||||||
|
component = patch[0];
|
||||||
patch.push(patch[0].willPatch());
|
patch.push(patch[0].willPatch());
|
||||||
}
|
}
|
||||||
for (let i = 0; i < patchLen; i++) {
|
for (let i = 0; i < patchLen; i++) {
|
||||||
const patch = patchQueue[i];
|
const patch = patchQueue[i];
|
||||||
patch[0]._patch(patch[1]);
|
patch[0].__patch(patch[1]);
|
||||||
}
|
}
|
||||||
for (let i = patchLen - 1; i >= 0; i--) {
|
for (let i = patchLen - 1; i >= 0; i--) {
|
||||||
const patch = patchQueue[i];
|
const patch = patchQueue[i];
|
||||||
|
component = patch[0];
|
||||||
patch[0].patched(patch[2]);
|
patch[0].patched(patch[2]);
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
errorHandler(e, component);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
//------------------------------------------------------------------------------
|
||||||
* Validate the component props (or next props) against the (static) props
|
// Error handling
|
||||||
* description. This is potentially an expensive operation: it may needs to
|
//------------------------------------------------------------------------------
|
||||||
* visit recursively the props and all the children to check if they are valid.
|
|
||||||
* This is why it is only done in 'dev' mode.
|
function errorHandler(error, component) {
|
||||||
*/
|
let canCatch = false;
|
||||||
_validateProps(props: Object) {
|
let qweb = component.env.qweb;
|
||||||
const propsDef = (<any>this.constructor).props;
|
let root = component;
|
||||||
if (propsDef instanceof Array) {
|
while (component && !(canCatch = component.catchError !== Component.prototype.catchError)) {
|
||||||
// list of strings (prop names)
|
root = component;
|
||||||
for (let i = 0, l = propsDef.length; i < l; i++) {
|
component = component.__owl__.parent;
|
||||||
if (!(propsDef[i] in props)) {
|
|
||||||
throw new Error(
|
|
||||||
`Missing props '${propsDef[i]}' (widget '${this.constructor.name}')`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
console.error(error);
|
||||||
} else if (propsDef) {
|
// we trigger error on QWeb so it can be logged/handled
|
||||||
// propsDef is an object now
|
qweb.trigger("error", error);
|
||||||
for (let propName in propsDef) {
|
|
||||||
if (!(propName in props)) {
|
if (canCatch) {
|
||||||
if (propsDef[propName] && !propsDef[propName].optional) {
|
setTimeout(() => {
|
||||||
throw new Error(
|
component.catchError(error);
|
||||||
`Missing props '${propName}' (widget '${this.constructor.name}')`
|
});
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
break;
|
root.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let isValid = isValidProp(props[propName], propsDef[propName]);
|
|
||||||
if (!isValid) {
|
|
||||||
throw new Error(
|
|
||||||
`Props '${propName}' of invalid type in widget '${
|
|
||||||
this.constructor.name
|
|
||||||
}'`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Prop validation helper
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if an invidual prop value matches its (static) prop definition
|
|
||||||
*/
|
|
||||||
function isValidProp(prop, propDef): boolean {
|
|
||||||
if (typeof propDef === "function") {
|
|
||||||
// Check if a value is constructed by some Constructor. Note that there is a
|
|
||||||
// slight abuse of language: we want to consider primitive values as well.
|
|
||||||
//
|
|
||||||
// So, even though 1 is not an instance of Number, we want to consider that
|
|
||||||
// it is valid.
|
|
||||||
if (typeof prop === "object") {
|
|
||||||
return prop instanceof propDef;
|
|
||||||
}
|
|
||||||
return typeof prop === propDef.name.toLowerCase();
|
|
||||||
} else if (propDef instanceof Array) {
|
|
||||||
// If this code is executed, this means that we want to check if a prop
|
|
||||||
// matches at least one of its descriptor.
|
|
||||||
let result = false;
|
|
||||||
for (let i = 0, iLen = propDef.length; i < iLen; i++) {
|
|
||||||
result = result || isValidProp(prop, propDef[i]);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
// propsDef is an object
|
|
||||||
let result = isValidProp(prop, propDef.type);
|
|
||||||
if (propDef.type === Array) {
|
|
||||||
for (let i = 0, iLen = prop.length; i < iLen; i++) {
|
|
||||||
result = result && isValidProp(prop[i], propDef.element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (propDef.type === Object) {
|
|
||||||
const shape = propDef.shape;
|
|
||||||
for (let key in shape) {
|
|
||||||
result = result && isValidProp(prop[key], shape[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,494 @@
|
|||||||
|
import { QWeb } from "../qweb/index";
|
||||||
|
import { MODS_CODE } from "../qweb/extensions";
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// t-component
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const T_COMPONENT_MODS_CODE = Object.assign({}, MODS_CODE, {
|
||||||
|
self: "if (e.target !== vn.elm) {return}"
|
||||||
|
});
|
||||||
|
|
||||||
|
QWeb.utils.defineProxy = function defineProxy(target, source) {
|
||||||
|
for (let k in source) {
|
||||||
|
Object.defineProperty(target, k, {
|
||||||
|
get() {
|
||||||
|
return source[k];
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
source[k] = val;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The t-component directive is certainly a complicated and hard to maintain piece
|
||||||
|
* of code. To help you, fellow developer, if you have to maintain it, I offer
|
||||||
|
* you this advice: Good luck...
|
||||||
|
*
|
||||||
|
* Since it is not 'direct' code, but rather code that generates other code, it
|
||||||
|
* is not easy to understand. To help you, here is a detailed and commented
|
||||||
|
* explanation of the code generated by the t-component directive for the following
|
||||||
|
* situation:
|
||||||
|
* ```xml
|
||||||
|
* <Child
|
||||||
|
* t-key="'somestring'"
|
||||||
|
* flag="state.flag"
|
||||||
|
* t-transition="fade"/>
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* // we assign utils on top of the function because it will be useful for
|
||||||
|
* // each components
|
||||||
|
* let utils = this.utils;
|
||||||
|
*
|
||||||
|
* // this is the virtual node representing the parent div
|
||||||
|
* let c1 = [], p1 = { key: 1 };
|
||||||
|
* var vn1 = h("div", p1, c1);
|
||||||
|
*
|
||||||
|
* // t-component directive: we start by evaluating the expression given by t-key:
|
||||||
|
* let key5 = "somestring";
|
||||||
|
*
|
||||||
|
* // def3 is the promise that will contain later either the new component
|
||||||
|
* // creation, or the props update...
|
||||||
|
* let def3;
|
||||||
|
*
|
||||||
|
* // this is kind of tricky: we need here to find if the component was already
|
||||||
|
* // created by a previous rendering. This is done by checking the internal
|
||||||
|
* // `cmap` (children map) of the parent component: it maps keys to component ids,
|
||||||
|
* // and, then, if there is an id, we look into the children list to get the
|
||||||
|
* // instance
|
||||||
|
* let w4 =
|
||||||
|
* key5 in context.__owl__.cmap
|
||||||
|
* ? context.__owl__.children[context.__owl__.cmap[key5]]
|
||||||
|
* : false;
|
||||||
|
*
|
||||||
|
* // We keep the index of the position of the component in the closure. We push
|
||||||
|
* // null to reserve the slot, and will replace it later by the component vnode,
|
||||||
|
* // when it will be ready (do not forget that preparing/rendering a component is
|
||||||
|
* // asynchronous)
|
||||||
|
* let _2_index = c1.length;
|
||||||
|
* c1.push(null);
|
||||||
|
*
|
||||||
|
* // we evaluate here the props given to the component. It is done here to be
|
||||||
|
* // able to easily reference it later, and also, it might be an expensive
|
||||||
|
* // computation, so it is certainly better to do it only once
|
||||||
|
* let props4 = { flag: context["state"].flag };
|
||||||
|
*
|
||||||
|
* // If we have a component, currently rendering, but not ready yet, we do not want
|
||||||
|
* // to wait for it to be ready if we can avoid it
|
||||||
|
* if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
||||||
|
* // we check if the props are the same. In that case, we can simply reuse
|
||||||
|
* // the previous rendering and skip all useless work
|
||||||
|
* if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
||||||
|
* def3 = w4.__owl__.renderPromise;
|
||||||
|
* } else {
|
||||||
|
* // if the props are not the same, we destroy the component and starts anew.
|
||||||
|
* // this will be faster than waiting for its rendering, then updating it
|
||||||
|
* w4.destroy();
|
||||||
|
* w4 = false;
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* if (!w4) {
|
||||||
|
* // in this situation, we need to create a new component. First step is
|
||||||
|
* // to get a reference to the class, then create an instance with
|
||||||
|
* // current context as parent, and the props.
|
||||||
|
* let W4 = context.component && context.components[componentKey4] || QWeb.component[componentKey4];
|
||||||
|
|
||||||
|
* if (!W4) {
|
||||||
|
* throw new Error("Cannot find the definition of component 'child'");
|
||||||
|
* }
|
||||||
|
* w4 = new W4(owner, props4);
|
||||||
|
*
|
||||||
|
* // Whenever we rerender the parent component, we need to be sure that we
|
||||||
|
* // are able to find the component instance. To do that, we register it to
|
||||||
|
* // the parent cmap (children map). Note that the 'template' key is
|
||||||
|
* // used here, since this is what identify the component from the template
|
||||||
|
* // perspective.
|
||||||
|
* context.__owl__.cmap[key5] = w4.__owl__.id;
|
||||||
|
*
|
||||||
|
* // __prepare is called, to basically call willStart, then render the
|
||||||
|
* // component
|
||||||
|
* def3 = w4.__prepare();
|
||||||
|
*
|
||||||
|
* def3 = def3.then(vnode => {
|
||||||
|
* // we create here a virtual node for the parent (NOT the component). This
|
||||||
|
* // means that the vdom of the parent will be stopped here, and from
|
||||||
|
* // the parent's perspective, it simply is a vnode with no children.
|
||||||
|
* // However, it shares the same dom element with the component root
|
||||||
|
* // vnode.
|
||||||
|
* let pvnode = h(vnode.sel, { key: key5 });
|
||||||
|
*
|
||||||
|
* // we add hooks to the parent vnode so we can interact with the new
|
||||||
|
* // component at the proper time
|
||||||
|
* pvnode.data.hook = {
|
||||||
|
* insert(vn) {
|
||||||
|
* // the __mount method will patch the component vdom into the elm vn.elm,
|
||||||
|
* // then call the mounted hooks. However, suprisingly, the snabbdom
|
||||||
|
* // patch method actually replace the elm by a new elm, so we need
|
||||||
|
* // to synchronise the pvnode elm with the resulting elm
|
||||||
|
* let nvn = w4.__mount(vnode, vn.elm);
|
||||||
|
* pvnode.elm = nvn.elm;
|
||||||
|
* // what follows is only present if there are animations on the component
|
||||||
|
* utils.transitionInsert(vn, "fade");
|
||||||
|
* },
|
||||||
|
* remove() {
|
||||||
|
* // override with empty function to prevent from removing the node
|
||||||
|
* // directly. It will be removed when destroy is called anyway, which
|
||||||
|
* // delays the removal if there are animations.
|
||||||
|
* },
|
||||||
|
* destroy() {
|
||||||
|
* // if there are animations, we delay the call to destroy on the
|
||||||
|
* // component, if not, we call it directly.
|
||||||
|
* let finalize = () => {
|
||||||
|
* w4.destroy();
|
||||||
|
* };
|
||||||
|
* utils.transitionRemove(vn, "fade", finalize);
|
||||||
|
* }
|
||||||
|
* };
|
||||||
|
* // the pvnode is inserted at the correct position in the div's children
|
||||||
|
* c1[_2_index] = pvnode;
|
||||||
|
*
|
||||||
|
* // we keep here a reference to the parent vnode (representing the
|
||||||
|
* // component, so we can reuse it later whenever we update the component
|
||||||
|
* w4.__owl__.pvnode = pvnode;
|
||||||
|
* });
|
||||||
|
* } else {
|
||||||
|
* // this is the 'update' path of the directive.
|
||||||
|
* // the call to __updateProps is the actual component update
|
||||||
|
* // Note that we only update the props if we cannot reuse the previous
|
||||||
|
* // rendering work (in the case it was rendered with the same props)
|
||||||
|
* def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
||||||
|
* def3 = def3.then(() => {
|
||||||
|
* // if component was destroyed in the meantime, we do nothing (so, this
|
||||||
|
* // means that the parent's element children list will have a null in
|
||||||
|
* // the component's position, which will cause the pvnode to be removed
|
||||||
|
* // when it is patched.
|
||||||
|
* if (w4.__owl__.isDestroyed) {
|
||||||
|
* return;
|
||||||
|
* }
|
||||||
|
* // like above, we register the pvnode to the children list, so it
|
||||||
|
* // will not be patched out of the dom.
|
||||||
|
* let pvnode = w4.__owl__.pvnode;
|
||||||
|
* c1[_2_index] = pvnode;
|
||||||
|
* });
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* // we register the deferred here so the parent can coordinate its patch operation
|
||||||
|
* // with all the children.
|
||||||
|
* extra.promises.push(def3);
|
||||||
|
* return vn1;
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
QWeb.addDirective({
|
||||||
|
name: "component",
|
||||||
|
extraNames: ["props", "keepalive", "asyncroot"],
|
||||||
|
priority: 100,
|
||||||
|
atNodeEncounter({ ctx, value, node, qweb }): boolean {
|
||||||
|
ctx.addLine("//COMPONENT");
|
||||||
|
ctx.rootContext.shouldDefineOwner = true;
|
||||||
|
ctx.rootContext.shouldDefineQWeb = true;
|
||||||
|
ctx.rootContext.shouldDefineParent = true;
|
||||||
|
ctx.rootContext.shouldDefineUtils = true;
|
||||||
|
let keepAlive = node.getAttribute("t-keepalive") ? true : false;
|
||||||
|
let async = node.getAttribute("t-asyncroot") ? true : false;
|
||||||
|
|
||||||
|
// t-on- events and t-transition
|
||||||
|
const events: [string, string[], string, string][] = [];
|
||||||
|
let transition: string = "";
|
||||||
|
const attributes = (<Element>node).attributes;
|
||||||
|
const props: { [key: string]: string } = {};
|
||||||
|
for (let i = 0; i < attributes.length; i++) {
|
||||||
|
const name = attributes[i].name;
|
||||||
|
const value = attributes[i].textContent!;
|
||||||
|
if (name.startsWith("t-on-")) {
|
||||||
|
const [eventName, ...mods] = name.slice(5).split(".");
|
||||||
|
let extraArgs;
|
||||||
|
let handlerName = value.replace(/\(.*\)/, function(args) {
|
||||||
|
extraArgs = args.slice(1, -1);
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
events.push([eventName, mods, handlerName, extraArgs]);
|
||||||
|
} else if (name === "t-transition") {
|
||||||
|
transition = value;
|
||||||
|
} else if (!name.startsWith("t-")) {
|
||||||
|
if (name !== "class" && name !== "style") {
|
||||||
|
// this is a prop!
|
||||||
|
props[name] = ctx.formatExpression(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = node.getAttribute("t-key");
|
||||||
|
if (key) {
|
||||||
|
key = ctx.formatExpression(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// computing the props string representing the props object
|
||||||
|
let propStr = Object.keys(props)
|
||||||
|
.map(k => k + ":" + props[k])
|
||||||
|
.join(",");
|
||||||
|
let dummyID = ctx.generateID();
|
||||||
|
let defID = ctx.generateID();
|
||||||
|
let componentID = ctx.generateID();
|
||||||
|
let keyID = key && ctx.generateID();
|
||||||
|
if (key) {
|
||||||
|
// we bind a variable to the key (could be a complex expression, so we
|
||||||
|
// want to evaluate it only once)
|
||||||
|
ctx.addLine(`let key${keyID} = ${key};`);
|
||||||
|
}
|
||||||
|
ctx.addLine(`let def${defID};`);
|
||||||
|
let templateID = key
|
||||||
|
? `key${keyID}`
|
||||||
|
: ctx.inLoop
|
||||||
|
? `String(-${componentID} - i)`
|
||||||
|
: String(componentID);
|
||||||
|
if (ctx.allowMultipleRoots) {
|
||||||
|
// necessary to prevent collisions
|
||||||
|
if (!key && ctx.inLoop) {
|
||||||
|
let id = ctx.generateID();
|
||||||
|
ctx.addLine(`let template${id} = "_slot_" + String(-${componentID} - i)`);
|
||||||
|
templateID = `template${id}`;
|
||||||
|
} else {
|
||||||
|
templateID = `"_slot_${templateID}"`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let ref = node.getAttribute("t-ref");
|
||||||
|
let refExpr = "";
|
||||||
|
let refKey: string = "";
|
||||||
|
if (ref) {
|
||||||
|
refKey = `ref${ctx.generateID()}`;
|
||||||
|
ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`);
|
||||||
|
refExpr = `context.refs[${refKey}] = w${componentID};`;
|
||||||
|
}
|
||||||
|
let transitionsInsertCode = "";
|
||||||
|
if (transition) {
|
||||||
|
transitionsInsertCode = `utils.transitionInsert(vn, '${transition}');`;
|
||||||
|
}
|
||||||
|
let finalizeComponentCode = `w${componentID}.${keepAlive ? "unmount" : "destroy"}();`;
|
||||||
|
if (ref && !keepAlive) {
|
||||||
|
finalizeComponentCode += `delete context.refs[${refKey}];`;
|
||||||
|
}
|
||||||
|
if (transition) {
|
||||||
|
finalizeComponentCode = `let finalize = () => {
|
||||||
|
${finalizeComponentCode}
|
||||||
|
};
|
||||||
|
utils.transitionRemove(vn, '${transition}', finalize);`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let createHook = "";
|
||||||
|
let classAttr = node.getAttribute("class");
|
||||||
|
let tattClass = node.getAttribute("t-att-class");
|
||||||
|
let styleAttr = node.getAttribute("style");
|
||||||
|
let tattStyle = node.getAttribute("t-att-style");
|
||||||
|
if (tattStyle) {
|
||||||
|
const attVar = `_${ctx.generateID()}`;
|
||||||
|
ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattStyle)};`);
|
||||||
|
tattStyle = attVar;
|
||||||
|
}
|
||||||
|
let classObj = "";
|
||||||
|
if (classAttr || tattClass || styleAttr || tattStyle || events.length) {
|
||||||
|
if (classAttr) {
|
||||||
|
let classDef = classAttr
|
||||||
|
.trim()
|
||||||
|
.split(/\s+/)
|
||||||
|
.map(a => `'${a}':true`)
|
||||||
|
.join(",");
|
||||||
|
classObj = `_${ctx.generateID()}`;
|
||||||
|
ctx.addLine(`let ${classObj} = {${classDef}};`);
|
||||||
|
}
|
||||||
|
if (tattClass) {
|
||||||
|
let tattExpr = ctx.formatExpression(tattClass);
|
||||||
|
if (tattExpr[0] !== "{" || tattExpr[tattExpr.length - 1] !== "}") {
|
||||||
|
tattExpr = `utils.toObj(${tattExpr})`;
|
||||||
|
}
|
||||||
|
if (classAttr) {
|
||||||
|
ctx.addLine(`Object.assign(${classObj}, ${tattExpr})`);
|
||||||
|
} else {
|
||||||
|
classObj = `_${ctx.generateID()}`;
|
||||||
|
ctx.addLine(`let ${classObj} = ${tattExpr};`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let eventsCode = events
|
||||||
|
.map(function([eventName, mods, handlerName, extraArgs]) {
|
||||||
|
let params = "owner";
|
||||||
|
if (extraArgs) {
|
||||||
|
if (ctx.inLoop) {
|
||||||
|
let argId = ctx.generateID();
|
||||||
|
// we need to evaluate the arguments now, because the handler will
|
||||||
|
// be set asynchronously later when the widget is ready, and the
|
||||||
|
// context might be different.
|
||||||
|
ctx.addLine(`let arg${argId} = ${ctx.formatExpression(extraArgs)};`);
|
||||||
|
params = `owner, arg${argId}`;
|
||||||
|
} else {
|
||||||
|
params = `owner, ${ctx.formatExpression(extraArgs)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let handler;
|
||||||
|
if (mods.length > 0) {
|
||||||
|
handler = `function (e) {`;
|
||||||
|
handler += mods
|
||||||
|
.map(function(mod) {
|
||||||
|
return T_COMPONENT_MODS_CODE[mod];
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
handler += `owner['${handlerName}'].call(${params}, e);}`;
|
||||||
|
} else {
|
||||||
|
handler = `owner['${handlerName}'].bind(${params})`;
|
||||||
|
}
|
||||||
|
return `vn.elm.addEventListener('${eventName}', ${handler});`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false);
|
||||||
|
const styleCode = styleExpr ? `vn.elm.style = ${styleExpr};` : "";
|
||||||
|
createHook = `vnode.data.hook = {create(_, vn){${styleCode}${eventsCode}}};`;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.addLine(
|
||||||
|
`let w${componentID} = ${templateID} in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[${templateID}]] : false;`
|
||||||
|
);
|
||||||
|
if (ctx.parentNode) {
|
||||||
|
ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`);
|
||||||
|
}
|
||||||
|
let shouldProxy = false;
|
||||||
|
if (async) {
|
||||||
|
ctx.addLine(`const patchQueue${componentID} = [];`);
|
||||||
|
ctx.addLine(
|
||||||
|
`c${ctx.parentNode}.push(w${componentID} && w${componentID}.__owl__.pvnode || null);`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (ctx.parentNode) {
|
||||||
|
ctx.addLine(`c${ctx.parentNode}.push(null);`);
|
||||||
|
} else {
|
||||||
|
let id = ctx.generateID();
|
||||||
|
ctx.rootContext.rootNode = id;
|
||||||
|
shouldProxy = true;
|
||||||
|
ctx.rootContext.shouldDefineResult = true;
|
||||||
|
ctx.addLine(`let vn${id} = {};`);
|
||||||
|
ctx.addLine(`result = vn${id};`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.addLine(`let props${componentID} = {${propStr}};`);
|
||||||
|
ctx.addIf(
|
||||||
|
`w${componentID} && w${componentID}.__owl__.renderPromise && !w${componentID}.__owl__.vnode`
|
||||||
|
);
|
||||||
|
ctx.addIf(`utils.shallowEqual(props${componentID}, w${componentID}.__owl__.renderProps)`);
|
||||||
|
ctx.addLine(`def${defID} = w${componentID}.__owl__.renderPromise;`);
|
||||||
|
ctx.addElse();
|
||||||
|
ctx.addLine(`w${componentID}.destroy();`);
|
||||||
|
ctx.addLine(`w${componentID} = false;`);
|
||||||
|
ctx.closeIf();
|
||||||
|
ctx.closeIf();
|
||||||
|
|
||||||
|
ctx.addIf(`!w${componentID}`);
|
||||||
|
// new component
|
||||||
|
ctx.addLine(`let componentKey${componentID} = ${ctx.interpolate(value)};`);
|
||||||
|
ctx.addLine(
|
||||||
|
`let W${componentID} = context.components && context.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}];`
|
||||||
|
);
|
||||||
|
// maybe only do this in dev mode...
|
||||||
|
ctx.addLine(
|
||||||
|
`if (!W${componentID}) {throw new Error('Cannot find the definition of component "' + componentKey${componentID} + '"')}`
|
||||||
|
);
|
||||||
|
if (QWeb.dev) {
|
||||||
|
ctx.addLine(`utils.validateProps(W${componentID}, props${componentID})`);
|
||||||
|
}
|
||||||
|
ctx.addLine(`w${componentID} = new W${componentID}(parent, props${componentID});`);
|
||||||
|
ctx.addLine(`parent.__owl__.cmap[${templateID}] = w${componentID}.__owl__.id;`);
|
||||||
|
|
||||||
|
// SLOTS
|
||||||
|
const varDefs: string[] = [];
|
||||||
|
const hasSlots = node.childNodes.length;
|
||||||
|
if (hasSlots) {
|
||||||
|
ctx.rootContext.shouldTrackScope = true;
|
||||||
|
for (let v of Object.values(ctx.variables)) {
|
||||||
|
if (v["id"]) {
|
||||||
|
varDefs.push(v["id"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clone = <Element>node.cloneNode(true);
|
||||||
|
const slotNodes = clone.querySelectorAll("[t-set]");
|
||||||
|
const slotId = qweb.nextSlotId++;
|
||||||
|
ctx.addLine(`w${componentID}.__owl__.slotId = ${slotId};`);
|
||||||
|
if (slotNodes.length) {
|
||||||
|
for (let i = 0, length = slotNodes.length; i < length; i++) {
|
||||||
|
const slotNode = slotNodes[i];
|
||||||
|
slotNode.parentElement!.removeChild(slotNode);
|
||||||
|
const key = slotNode.getAttribute("t-set")!;
|
||||||
|
slotNode.removeAttribute("t-set");
|
||||||
|
const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx);
|
||||||
|
qweb.slots[`${slotId}_${key}`] = slotFn.bind(qweb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (clone.childNodes.length) {
|
||||||
|
const t = clone.ownerDocument!.createElement("t");
|
||||||
|
for (let child of Object.values(clone.childNodes)) {
|
||||||
|
t.appendChild(child);
|
||||||
|
}
|
||||||
|
const slotFn = qweb._compile(`slot_default_template`, t, ctx);
|
||||||
|
qweb.slots[`${slotId}_default`] = slotFn.bind(qweb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let scopeVars = "";
|
||||||
|
if (hasSlots) {
|
||||||
|
scopeVars += ctx.scopeVars.length ? `Object.assign({}, scope)` : varDefs.length ? `{}` : "";
|
||||||
|
if (varDefs.length) {
|
||||||
|
scopeVars += `, {${varDefs.join(",")}}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.addLine(`def${defID} = w${componentID}.__prepare(${scopeVars});`);
|
||||||
|
// hack: specify empty remove hook to prevent the node from being removed from the DOM
|
||||||
|
let registerCode = `c${ctx.parentNode}[_${dummyID}_index]=pvnode;`;
|
||||||
|
if (shouldProxy) {
|
||||||
|
registerCode = `utils.defineProxy(vn${ctx.rootNode}, pvnode);`;
|
||||||
|
}
|
||||||
|
ctx.addLine(
|
||||||
|
`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${componentID}.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeComponentCode}}}});${registerCode}w${componentID}.__owl__.pvnode = pvnode;});`
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.addElse();
|
||||||
|
// need to update component
|
||||||
|
const patchQueueCode = async ? `patchQueue${componentID}` : "extra.patchQueue";
|
||||||
|
if (QWeb.dev) {
|
||||||
|
ctx.addLine(`utils.validateProps(w${componentID}.constructor, props${componentID})`);
|
||||||
|
}
|
||||||
|
ctx.addLine(
|
||||||
|
`def${defID} = def${defID} || w${componentID}.__updateProps(props${componentID}, extra.forceUpdate, ${patchQueueCode}${scopeVars &&
|
||||||
|
", " + scopeVars});`
|
||||||
|
);
|
||||||
|
let keepAliveCode = "";
|
||||||
|
if (keepAlive) {
|
||||||
|
keepAliveCode = `pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w${componentID}.el,vn.elm);vn.elm=w${componentID}.el;w${componentID}.__remount();};`;
|
||||||
|
}
|
||||||
|
ctx.addLine(
|
||||||
|
`def${defID} = def${defID}.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};${
|
||||||
|
tattStyle ? `w${componentID}.el.style=${tattStyle};` : ""
|
||||||
|
}let pvnode=w${componentID}.__owl__.pvnode;${keepAliveCode}${registerCode}});`
|
||||||
|
);
|
||||||
|
ctx.closeIf();
|
||||||
|
|
||||||
|
if (classObj) {
|
||||||
|
ctx.addLine(`w${componentID}.__owl__.classObj=${classObj};`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (async) {
|
||||||
|
ctx.addLine(
|
||||||
|
`def${defID}.then(w${componentID}.__applyPatchQueue.bind(w${componentID}, patchQueue${componentID}));`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ctx.addLine(`extra.promises.push(def${defID});`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.hasAttribute("t-if") || node.hasAttribute("t-else") || node.hasAttribute("t-elif")) {
|
||||||
|
ctx.closeIf();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { QWeb } from "../qweb/index";
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Prop validation helper
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the component props (or next props) against the (static) props
|
||||||
|
* description. This is potentially an expensive operation: it may needs to
|
||||||
|
* visit recursively the props and all the children to check if they are valid.
|
||||||
|
* This is why it is only done in 'dev' mode.
|
||||||
|
*/
|
||||||
|
|
||||||
|
QWeb.utils.validateProps = function(Widget, props: Object) {
|
||||||
|
const propsDef = (<any>Widget).props;
|
||||||
|
if (propsDef instanceof Array) {
|
||||||
|
// list of strings (prop names)
|
||||||
|
for (let i = 0, l = propsDef.length; i < l; i++) {
|
||||||
|
const propName = propsDef[i];
|
||||||
|
if (propName[propName.length - 1] === "?") {
|
||||||
|
// optional prop
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!props[propName]) {
|
||||||
|
throw new Error(`Missing props '${propsDef[i]}' (component '${Widget.name}')`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let key in props) {
|
||||||
|
if (!propsDef.includes(key) && !propsDef.includes(key + "?")) {
|
||||||
|
throw new Error(`Unknown prop '${key}' given to component '${Widget.name}'`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (propsDef) {
|
||||||
|
// propsDef is an object now
|
||||||
|
for (let propName in propsDef) {
|
||||||
|
if (props[propName] === undefined) {
|
||||||
|
if (propsDef[propName] && !propsDef[propName].optional) {
|
||||||
|
throw new Error(`Missing props '${propName}' (component '${Widget.name}')`);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let isValid = isValidProp(props[propName], propsDef[propName]);
|
||||||
|
if (!isValid) {
|
||||||
|
throw new Error(`Props '${propName}' of invalid type in component '${Widget.name}'`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let propName in props) {
|
||||||
|
if (!(propName in propsDef)) {
|
||||||
|
throw new Error(`Unknown prop '${propName}' given to component '${Widget.name}'`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an invidual prop value matches its (static) prop definition
|
||||||
|
*/
|
||||||
|
function isValidProp(prop, propDef): boolean {
|
||||||
|
if (propDef === true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (typeof propDef === "function") {
|
||||||
|
// Check if a value is constructed by some Constructor. Note that there is a
|
||||||
|
// slight abuse of language: we want to consider primitive values as well.
|
||||||
|
//
|
||||||
|
// So, even though 1 is not an instance of Number, we want to consider that
|
||||||
|
// it is valid.
|
||||||
|
if (typeof prop === "object") {
|
||||||
|
return prop instanceof propDef;
|
||||||
|
}
|
||||||
|
return typeof prop === propDef.name.toLowerCase();
|
||||||
|
} else if (propDef instanceof Array) {
|
||||||
|
// If this code is executed, this means that we want to check if a prop
|
||||||
|
// matches at least one of its descriptor.
|
||||||
|
let result = false;
|
||||||
|
for (let i = 0, iLen = propDef.length; i < iLen; i++) {
|
||||||
|
result = result || isValidProp(prop, propDef[i]);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
// propsDef is an object
|
||||||
|
let result = isValidProp(prop, propDef.type);
|
||||||
|
if (propDef.type === Array) {
|
||||||
|
for (let i = 0, iLen = prop.length; i < iLen; i++) {
|
||||||
|
result = result && isValidProp(prop[i], propDef.element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (propDef.type === Object) {
|
||||||
|
const shape = propDef.shape;
|
||||||
|
for (let key in shape) {
|
||||||
|
result = result && isValidProp(prop[key], shape[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -29,7 +29,7 @@ export class EventBus {
|
|||||||
* Add a listener for the 'eventType' events.
|
* Add a listener for the 'eventType' events.
|
||||||
*
|
*
|
||||||
* Note that the 'owner' of this event can be anything, but will more likely
|
* Note that the 'owner' of this event can be anything, but will more likely
|
||||||
* be a widget or a class. The idea is that the callback will be called with
|
* be a component or a class. The idea is that the callback will be called with
|
||||||
* the proper owner bound.
|
* the proper owner bound.
|
||||||
*
|
*
|
||||||
* Also, the owner should be kind of unique. This will be used to remove the
|
* Also, the owner should be kind of unique. This will be used to remove the
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* Owl Observer
|
||||||
|
*
|
||||||
|
* This code contains the logic that allows Owl to observe and react to state
|
||||||
|
* changes.
|
||||||
|
*
|
||||||
|
* This is a Observer class that can observe any JS values. The way it works
|
||||||
|
* can be summarized thusly:
|
||||||
|
* - primitive values are not observed at all
|
||||||
|
* - Objects and arrays are observed by replacing them with a Proxy
|
||||||
|
* - each object/array metadata are tracked in a weakmap, and keep a revision
|
||||||
|
* number
|
||||||
|
*
|
||||||
|
* Note that this code is loosely inspired by Vue.
|
||||||
|
*/
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Observer
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
export class Observer {
|
||||||
|
rev: number = 1;
|
||||||
|
allowMutations: boolean = true;
|
||||||
|
dirty: boolean = false;
|
||||||
|
weakMap: WeakMap<any, any> = new WeakMap();
|
||||||
|
|
||||||
|
notifyCB() {}
|
||||||
|
async notifyChange() {
|
||||||
|
|
||||||
|
this.dirty = true;
|
||||||
|
await Promise.resolve();
|
||||||
|
if (this.dirty) {
|
||||||
|
this.dirty = false;
|
||||||
|
this.notifyCB();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
observe(value: any, parent?: any): any {
|
||||||
|
if (value === null || typeof value !== "object" || value instanceof Date) {
|
||||||
|
// fun fact: typeof null === 'object'
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
let metadata = this.weakMap.get(value) || this._observe(value, parent);
|
||||||
|
return metadata.proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
revNumber(value): number {
|
||||||
|
const metadata = this.weakMap.get(value);
|
||||||
|
return metadata ? metadata.rev : 0;
|
||||||
|
}
|
||||||
|
deepRevNumber(value): number {
|
||||||
|
const metadata = this.weakMap.get(value);
|
||||||
|
return metadata ? metadata.deepRev : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_observe(value, parent) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
const proxy = new Proxy(value, {
|
||||||
|
get(target, k) {
|
||||||
|
const targetValue = target[k];
|
||||||
|
return self.observe(targetValue, value);
|
||||||
|
},
|
||||||
|
set(target, key: string, newVal): boolean {
|
||||||
|
const value = target[key];
|
||||||
|
if (newVal !== value) {
|
||||||
|
if (!self.allowMutations) {
|
||||||
|
throw new Error(
|
||||||
|
`Observed state cannot be changed here! (key: "${key}", val: "${newVal}")`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self._updateRevNumber(target);
|
||||||
|
target[key] = newVal;
|
||||||
|
self.notifyChange();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
deleteProperty(target, key) {
|
||||||
|
if (key in target) {
|
||||||
|
delete target[key];
|
||||||
|
self._updateRevNumber(target);
|
||||||
|
self.notifyChange();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const metadata = {
|
||||||
|
value,
|
||||||
|
proxy,
|
||||||
|
rev: this.rev,
|
||||||
|
deepRev: this.rev,
|
||||||
|
parent
|
||||||
|
};
|
||||||
|
|
||||||
|
this.weakMap.set(value, metadata);
|
||||||
|
this.weakMap.set(metadata.proxy, metadata);
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateRevNumber(target: any) {
|
||||||
|
this.rev++;
|
||||||
|
let metadata = this.weakMap.get(target);
|
||||||
|
metadata.rev!++;
|
||||||
|
let parent = target;
|
||||||
|
do {
|
||||||
|
metadata = this.weakMap.get(parent);
|
||||||
|
metadata.deepRev++;
|
||||||
|
} while ((parent = metadata.parent) && parent !== target);
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
-14
@@ -4,19 +4,21 @@
|
|||||||
*
|
*
|
||||||
* Note that dynamic values, such as a date or a commit hash are added by rollup
|
* Note that dynamic values, such as a date or a commit hash are added by rollup
|
||||||
*/
|
*/
|
||||||
export { Component } from "./component";
|
import { EventBus } from "./core/event_bus";
|
||||||
export { EventBus } from "./event_bus";
|
import { Observer } from "./core/observer";
|
||||||
export { Observer } from "./observer";
|
import { QWeb } from "./qweb/index";
|
||||||
|
import { ConnectedComponent } from "./store/connected_component";
|
||||||
// we need to import manually the extra directives so they can register
|
import { Store } from "./store/store";
|
||||||
// themselves in QWeb, otherwise these files will not even be loaded.
|
|
||||||
import "./qweb_directives";
|
|
||||||
import "./qweb_extensions";
|
|
||||||
import { QWeb } from "./qweb_core";
|
|
||||||
export { QWeb };
|
|
||||||
|
|
||||||
export { connect, Store } from "./store";
|
|
||||||
import * as _utils from "./utils";
|
import * as _utils from "./utils";
|
||||||
|
import { Link } from "./router/Link";
|
||||||
|
import { Router } from "./router/Router";
|
||||||
|
|
||||||
|
export { Component } from "./component/component";
|
||||||
|
export { QWeb };
|
||||||
|
export const core = { EventBus, Observer };
|
||||||
|
export const router = { Router, Link };
|
||||||
|
export const store = { Store, ConnectedComponent };
|
||||||
|
export const utils = _utils;
|
||||||
|
|
||||||
export const __info__ = {};
|
export const __info__ = {};
|
||||||
|
|
||||||
@@ -32,8 +34,7 @@ Object.defineProperty(__info__, "mode", {
|
|||||||
`Owl is running in 'dev' mode. This is not suitable for production use. See ${url} for more information.`
|
`Owl is running in 'dev' mode. This is not suitable for production use. See ${url} for more information.`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log(`Owl is now running in 'prod' mode.`)
|
console.log(`Owl is now running in 'prod' mode.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
export const utils = _utils;
|
|
||||||
|
|||||||
-211
@@ -1,211 +0,0 @@
|
|||||||
/**
|
|
||||||
* Owl Observer
|
|
||||||
*
|
|
||||||
* This code contains the logic that allows Owl to observe and react to state
|
|
||||||
* changes.
|
|
||||||
*
|
|
||||||
* This is a Observer class that can observe any JS values. The way it works
|
|
||||||
* can be summarized thusly:
|
|
||||||
* - primitive values are not observed at all
|
|
||||||
* - Objects are observed by replacing all their keys with getters/setters
|
|
||||||
* (recursively)
|
|
||||||
* - Arrays are observed by replacing their prototype with a customized version,
|
|
||||||
* which wrap some methods to allow the tracking of each state change.
|
|
||||||
*
|
|
||||||
* Note that this code is inspired by Vue.
|
|
||||||
*/
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Modified Array prototype
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// we define here a new modified Array prototype, which basically override all
|
|
||||||
// Array methods that change some state to be able to track their changes
|
|
||||||
const methodsToPatch = [
|
|
||||||
"push",
|
|
||||||
"pop",
|
|
||||||
"shift",
|
|
||||||
"unshift",
|
|
||||||
"splice",
|
|
||||||
"sort",
|
|
||||||
"reverse"
|
|
||||||
];
|
|
||||||
const methodLen = methodsToPatch.length;
|
|
||||||
|
|
||||||
const ArrayProto = Array.prototype;
|
|
||||||
const ModifiedArrayProto = Object.create(ArrayProto);
|
|
||||||
|
|
||||||
for (let i = 0; i < methodLen; i++) {
|
|
||||||
const method = methodsToPatch[i];
|
|
||||||
const initialMethod = ArrayProto[method];
|
|
||||||
ModifiedArrayProto[method] = function(...args) {
|
|
||||||
if (!this.__observer__.allowMutations) {
|
|
||||||
throw new Error(`Array cannot be changed here")`);
|
|
||||||
}
|
|
||||||
this.__observer__.rev++;
|
|
||||||
this.__observer__.notifyChange();
|
|
||||||
this.__owl__.rev++;
|
|
||||||
let parent = this;
|
|
||||||
do {
|
|
||||||
parent.__owl__.deepRev++;
|
|
||||||
} while ((parent = parent.__owl__.parent));
|
|
||||||
let inserted;
|
|
||||||
switch (method) {
|
|
||||||
case "push":
|
|
||||||
case "unshift":
|
|
||||||
inserted = args;
|
|
||||||
break;
|
|
||||||
case "splice":
|
|
||||||
inserted = args.slice(2);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (inserted) {
|
|
||||||
for (let i = 0, iLen = inserted.length; i < iLen; i++) {
|
|
||||||
this.__observer__.observe(inserted[i], this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return initialMethod.call(this, ...args);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Observer
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
export class Observer {
|
|
||||||
rev: number = 1;
|
|
||||||
allowMutations: boolean = true;
|
|
||||||
dirty: boolean = false;
|
|
||||||
|
|
||||||
static set(target: any, key: number | string, value: any) {
|
|
||||||
if (!target.__owl__) {
|
|
||||||
throw Error(
|
|
||||||
"`Observer.set()` can only be called with observed Objects or Arrays"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
target.__owl__.observer.set(target, key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
static delete(target: any, key: number | string) {
|
|
||||||
if (!target.__owl__) {
|
|
||||||
throw Error(
|
|
||||||
"`Observer.delete()` can only be called with observed Objects"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
target.__owl__.observer.delete(target, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyCB() {}
|
|
||||||
notifyChange() {
|
|
||||||
this.dirty = true;
|
|
||||||
Promise.resolve().then(() => {
|
|
||||||
if (this.dirty) {
|
|
||||||
this.dirty = false;
|
|
||||||
this.notifyCB();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
observe(value: any, parent?: any) {
|
|
||||||
if (value === null) {
|
|
||||||
// fun fact: typeof null === 'object'
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (typeof value !== "object") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ("__owl__" in value) {
|
|
||||||
// already observed
|
|
||||||
value.__owl__.parent = parent;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
this._observeArr(value, parent);
|
|
||||||
} else {
|
|
||||||
this._observeObj(value, parent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
set(target: any, key: number | string, value: any) {
|
|
||||||
let alreadyDefined =
|
|
||||||
key in target &&
|
|
||||||
Object.getOwnPropertyDescriptor(target, key)!.configurable === false;
|
|
||||||
if (alreadyDefined) {
|
|
||||||
target[key] = value;
|
|
||||||
} else {
|
|
||||||
this._addProp(target, key, value);
|
|
||||||
this._updateRevNumber(target);
|
|
||||||
}
|
|
||||||
this.notifyChange();
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(target: any, key: number | string) {
|
|
||||||
delete target[key];
|
|
||||||
this._updateRevNumber(target);
|
|
||||||
this.notifyChange();
|
|
||||||
}
|
|
||||||
|
|
||||||
_observeObj<T extends { __owl__?: any }>(obj: T, parent?: any) {
|
|
||||||
obj.__owl__ = {
|
|
||||||
rev: this.rev,
|
|
||||||
deepRev: this.rev,
|
|
||||||
parent,
|
|
||||||
observer: this
|
|
||||||
};
|
|
||||||
Object.defineProperty(obj, "__owl__", { enumerable: false });
|
|
||||||
for (let key in obj) {
|
|
||||||
this._addProp(obj, key, obj[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_observeArr(arr: Array<any>, parent?: any) {
|
|
||||||
(<any>arr).__owl__ = {
|
|
||||||
rev: this.rev,
|
|
||||||
deepRev: this.rev,
|
|
||||||
parent,
|
|
||||||
observer: this
|
|
||||||
};
|
|
||||||
Object.defineProperty(arr, "__owl__", { enumerable: false });
|
|
||||||
(<any>arr).__proto__ = Object.create(ModifiedArrayProto);
|
|
||||||
(<any>arr).__proto__.__observer__ = this;
|
|
||||||
for (let i = 0, iLen = arr.length; i < iLen; i++) {
|
|
||||||
this.observe(arr[i], arr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_addProp<T extends { __owl__?: any }>(
|
|
||||||
obj: T,
|
|
||||||
key: string | number,
|
|
||||||
value: any
|
|
||||||
) {
|
|
||||||
var self = this;
|
|
||||||
Object.defineProperty(obj, key, {
|
|
||||||
configurable: true,
|
|
||||||
enumerable: true,
|
|
||||||
get() {
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
set(newVal) {
|
|
||||||
if (newVal !== value) {
|
|
||||||
if (!self.allowMutations) {
|
|
||||||
throw new Error(
|
|
||||||
`Observed state cannot be changed here! (key: "${key}", val: "${newVal}")`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
self._updateRevNumber(obj);
|
|
||||||
value = newVal;
|
|
||||||
self.observe(newVal, obj);
|
|
||||||
self.notifyChange();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.observe(value, obj);
|
|
||||||
}
|
|
||||||
_updateRevNumber(target: any) {
|
|
||||||
this.rev++;
|
|
||||||
target.__owl__.rev!++;
|
|
||||||
let parent = target;
|
|
||||||
do {
|
|
||||||
parent.__owl__.deepRev++;
|
|
||||||
} while ((parent = parent.__owl__.parent) && parent !== target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Context, QWeb, UTILS } from "./qweb_core";
|
import { Context } from "./context";
|
||||||
import { QWebExprVar } from "./qweb_expressions";
|
import { QWebExprVar } from "./expression_parser";
|
||||||
|
import { QWeb } from "./qweb";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Owl QWeb Directives
|
* Owl QWeb Directives
|
||||||
@@ -18,7 +19,7 @@ import { QWebExprVar } from "./qweb_expressions";
|
|||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// t-esc and t-raw
|
// t-esc and t-raw
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
(<any>UTILS).getFragment = function(str: string): DocumentFragment {
|
QWeb.utils.getFragment = function(str: string): DocumentFragment {
|
||||||
const temp = document.createElement("template");
|
const temp = document.createElement("template");
|
||||||
temp.innerHTML = str;
|
temp.innerHTML = str;
|
||||||
return temp.content;
|
return temp.content;
|
||||||
@@ -57,12 +58,11 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Context) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let fragID = ctx.generateID();
|
let fragID = ctx.generateID();
|
||||||
ctx.addLine(`var frag${fragID} = this.utils.getFragment(${exprID})`);
|
ctx.rootContext.shouldDefineUtils = true;
|
||||||
|
ctx.addLine(`var frag${fragID} = utils.getFragment(${exprID})`);
|
||||||
let tempNodeID = ctx.generateID();
|
let tempNodeID = ctx.generateID();
|
||||||
ctx.addLine(`var p${tempNodeID} = {hook: {`);
|
ctx.addLine(`var p${tempNodeID} = {hook: {`);
|
||||||
ctx.addLine(
|
ctx.addLine(` insert: n => n.elm.parentNode.replaceChild(frag${fragID}, n.elm),`);
|
||||||
` insert: n => n.elm.parentNode.replaceChild(frag${fragID}, n.elm),`
|
|
||||||
);
|
|
||||||
ctx.addLine(`}};`);
|
ctx.addLine(`}};`);
|
||||||
ctx.addLine(`var vn${tempNodeID} = h('div', p${tempNodeID})`);
|
ctx.addLine(`var vn${tempNodeID} = h('div', p${tempNodeID})`);
|
||||||
ctx.addLine(`c${ctx.parentNode}.push(vn${tempNodeID});`);
|
ctx.addLine(`c${ctx.parentNode}.push(vn${tempNodeID});`);
|
||||||
@@ -116,9 +116,7 @@ QWeb.addDirective({
|
|||||||
if (value) {
|
if (value) {
|
||||||
const formattedValue = ctx.formatExpression(value);
|
const formattedValue = ctx.formatExpression(value);
|
||||||
if (ctx.variables.hasOwnProperty(variable)) {
|
if (ctx.variables.hasOwnProperty(variable)) {
|
||||||
ctx.addLine(
|
ctx.addLine(`${(<QWebExprVar>ctx.variables[variable]).id} = ${formattedValue}`);
|
||||||
`${(<QWebExprVar>ctx.variables[variable]).id} = ${formattedValue}`
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
const varName = `_${ctx.generateID()}`;
|
const varName = `_${ctx.generateID()}`;
|
||||||
ctx.addLine(`var ${varName} = ${formattedValue};`);
|
ctx.addLine(`var ${varName} = ${formattedValue};`);
|
||||||
@@ -220,9 +218,7 @@ QWeb.addDirective({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// compile sub template
|
// compile sub template
|
||||||
const subCtx = ctx
|
const subCtx = ctx.subContext("caller", nodeCopy).subContext("variables", Object.create(vars));
|
||||||
.subContext("caller", nodeCopy)
|
|
||||||
.subContext("variables", Object.create(vars));
|
|
||||||
|
|
||||||
qweb._compileNode(nodeTemplate.elem, subCtx);
|
qweb._compileNode(nodeTemplate.elem, subCtx);
|
||||||
|
|
||||||
@@ -232,6 +228,10 @@ QWeb.addDirective({
|
|||||||
ctx.addLine("}");
|
ctx.addLine("}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (node.hasAttribute("t-if") || node.hasAttribute("t-else") || node.hasAttribute("t-elif")) {
|
||||||
|
ctx.closeIf();
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -250,9 +250,7 @@ QWeb.addDirective({
|
|||||||
const name = node.getAttribute("t-as")!;
|
const name = node.getAttribute("t-as")!;
|
||||||
let arrayID = ctx.generateID();
|
let arrayID = ctx.generateID();
|
||||||
ctx.addLine(`var _${arrayID} = ${ctx.formatExpression(elems)};`);
|
ctx.addLine(`var _${arrayID} = ${ctx.formatExpression(elems)};`);
|
||||||
ctx.addLine(
|
ctx.addLine(`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`);
|
||||||
`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`
|
|
||||||
);
|
|
||||||
let keysID = ctx.generateID();
|
let keysID = ctx.generateID();
|
||||||
let valuesID = ctx.generateID();
|
let valuesID = ctx.generateID();
|
||||||
ctx.addLine(`var _${keysID} = _${valuesID} = _${arrayID};`);
|
ctx.addLine(`var _${keysID} = _${valuesID} = _${arrayID};`);
|
||||||
@@ -263,16 +261,15 @@ QWeb.addDirective({
|
|||||||
ctx.addLine(`var _length${keysID} = _${keysID}.length;`);
|
ctx.addLine(`var _length${keysID} = _${keysID}.length;`);
|
||||||
ctx.addLine(`for (let i = 0; i < _length${keysID}; i++) {`);
|
ctx.addLine(`for (let i = 0; i < _length${keysID}; i++) {`);
|
||||||
ctx.indent();
|
ctx.indent();
|
||||||
ctx.addLine(`context.${name}_first = i === 0;`);
|
ctx.addToScope(name + "_first", "i === 0");
|
||||||
ctx.addLine(`context.${name}_last = i === _length${keysID} - 1;`);
|
ctx.addToScope(name + "_last", `i === _length${keysID} - 1`);
|
||||||
ctx.addLine(`context.${name}_index = i;`);
|
ctx.addToScope(name + "_index", "i");
|
||||||
ctx.addLine(`context.${name} = _${keysID}[i];`);
|
ctx.addToScope(name, `_${keysID}[i]`);
|
||||||
ctx.addLine(`context.${name}_value = _${valuesID}[i];`);
|
ctx.addToScope(name + "_value", `_${valuesID}[i]`);
|
||||||
const nodeCopy = <Element>node.cloneNode(true);
|
const nodeCopy = <Element>node.cloneNode(true);
|
||||||
let shouldWarn =
|
let shouldWarn = nodeCopy.tagName !== "t" && !nodeCopy.hasAttribute("t-key");
|
||||||
nodeCopy.tagName !== "t" && !nodeCopy.hasAttribute("t-key");
|
|
||||||
if (!shouldWarn && node.tagName === "t") {
|
if (!shouldWarn && node.tagName === "t") {
|
||||||
if (node.hasAttribute("t-widget") && !node.hasAttribute("t-key")) {
|
if (node.hasAttribute("t-component") && !node.hasAttribute("t-key")) {
|
||||||
shouldWarn = true;
|
shouldWarn = true;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@@ -304,7 +301,7 @@ QWeb.addDirective({
|
|||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
QWeb.addDirective({
|
QWeb.addDirective({
|
||||||
name: "debug",
|
name: "debug",
|
||||||
priority: 99,
|
priority: 1,
|
||||||
atNodeEncounter({ ctx }) {
|
atNodeEncounter({ ctx }) {
|
||||||
ctx.addLine("debugger;");
|
ctx.addLine("debugger;");
|
||||||
}
|
}
|
||||||
@@ -315,7 +312,7 @@ QWeb.addDirective({
|
|||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
QWeb.addDirective({
|
QWeb.addDirective({
|
||||||
name: "log",
|
name: "log",
|
||||||
priority: 99,
|
priority: 1,
|
||||||
atNodeEncounter({ ctx, value }) {
|
atNodeEncounter({ ctx, value }) {
|
||||||
const expr = ctx.formatExpression(value);
|
const expr = ctx.formatExpression(value);
|
||||||
ctx.addLine(`console.log(${expr})`);
|
ctx.addLine(`console.log(${expr})`);
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
import { compileExpr, QWebVar } from "./expression_parser";
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Compilation Context
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export class Context {
|
||||||
|
nextID: number = 1;
|
||||||
|
code: string[] = [];
|
||||||
|
variables: { [key: string]: QWebVar } = {};
|
||||||
|
escaping: boolean = false;
|
||||||
|
parentNode: number | null = null;
|
||||||
|
parentTextNode: number | null = null;
|
||||||
|
rootNode: number | null = null;
|
||||||
|
indentLevel: number = 0;
|
||||||
|
rootContext: Context;
|
||||||
|
caller: Element | undefined;
|
||||||
|
shouldDefineOwner: boolean = false;
|
||||||
|
shouldDefineParent: boolean = false;
|
||||||
|
shouldDefineQWeb: boolean = false;
|
||||||
|
shouldDefineUtils: boolean = false;
|
||||||
|
shouldDefineResult: boolean = false;
|
||||||
|
shouldProtectContext: boolean = false;
|
||||||
|
shouldTrackScope: boolean = false;
|
||||||
|
inLoop: boolean = false;
|
||||||
|
inPreTag: boolean = false;
|
||||||
|
templateName: string;
|
||||||
|
allowMultipleRoots: boolean = false;
|
||||||
|
hasParentWidget: boolean = false;
|
||||||
|
scopeVars: any[] = [];
|
||||||
|
|
||||||
|
constructor(name?: string) {
|
||||||
|
this.rootContext = this;
|
||||||
|
this.templateName = name || "noname";
|
||||||
|
this.addLine("var h = this.h;");
|
||||||
|
}
|
||||||
|
|
||||||
|
generateID(): number {
|
||||||
|
const id = this.rootContext.nextID++;
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
generateCode(): string[] {
|
||||||
|
const shouldTrackScope = this.shouldTrackScope && this.scopeVars.length;
|
||||||
|
if (shouldTrackScope) {
|
||||||
|
// add some vars to scope if needed
|
||||||
|
for (let scopeVar of this.scopeVars.reverse()) {
|
||||||
|
let { index, key, indent } = scopeVar;
|
||||||
|
const prefix = new Array(indent + 2).join(" ");
|
||||||
|
this.code.splice(index + 1, 0, prefix + `scope.${key} = context.${key};`);
|
||||||
|
}
|
||||||
|
this.code.unshift(" const scope = Object.create(null);");
|
||||||
|
}
|
||||||
|
if (this.shouldProtectContext) {
|
||||||
|
this.code.unshift(" context = Object.create(context);");
|
||||||
|
}
|
||||||
|
if (this.shouldDefineResult) {
|
||||||
|
this.code.unshift(" let result;");
|
||||||
|
}
|
||||||
|
if (this.shouldDefineOwner) {
|
||||||
|
// this is necessary to prevent some directives (t-forach for ex) to
|
||||||
|
// pollute the rendering context by adding some keys in it.
|
||||||
|
this.code.unshift(" let owner = context;");
|
||||||
|
}
|
||||||
|
if (this.shouldDefineParent) {
|
||||||
|
if (this.hasParentWidget) {
|
||||||
|
this.code.unshift(" let parent = extra.parent;");
|
||||||
|
} else {
|
||||||
|
this.code.unshift(" let parent = context;");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.shouldDefineQWeb) {
|
||||||
|
this.code.unshift(" let QWeb = this.constructor;");
|
||||||
|
}
|
||||||
|
if (this.shouldDefineUtils) {
|
||||||
|
this.code.unshift(" let utils = this.constructor.utils;");
|
||||||
|
}
|
||||||
|
return this.code;
|
||||||
|
}
|
||||||
|
|
||||||
|
withParent(node: number): Context {
|
||||||
|
if (
|
||||||
|
!this.allowMultipleRoots &&
|
||||||
|
this === this.rootContext &&
|
||||||
|
(this.parentNode || this.parentTextNode)
|
||||||
|
) {
|
||||||
|
throw new Error("A template should not have more than one root node");
|
||||||
|
}
|
||||||
|
if (!this.rootContext.rootNode) {
|
||||||
|
this.rootContext.rootNode = node;
|
||||||
|
}
|
||||||
|
return this.subContext("parentNode", node);
|
||||||
|
}
|
||||||
|
|
||||||
|
subContext(key: keyof Context, value: any): Context {
|
||||||
|
const newContext = Object.create(this);
|
||||||
|
newContext[key] = value;
|
||||||
|
return newContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
indent() {
|
||||||
|
this.indentLevel++;
|
||||||
|
}
|
||||||
|
|
||||||
|
dedent() {
|
||||||
|
this.indentLevel--;
|
||||||
|
}
|
||||||
|
|
||||||
|
addLine(line: string): number {
|
||||||
|
const prefix = new Array(this.indentLevel + 2).join(" ");
|
||||||
|
this.code.push(prefix + line);
|
||||||
|
return this.code.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
addToScope(key: string, expr: string) {
|
||||||
|
const index = this.addLine(`context.${key} = ${expr};`);
|
||||||
|
this.rootContext.scopeVars.push({ index, key, indent: this.indentLevel });
|
||||||
|
}
|
||||||
|
|
||||||
|
addIf(condition: string) {
|
||||||
|
this.addLine(`if (${condition}) {`);
|
||||||
|
this.indent();
|
||||||
|
}
|
||||||
|
|
||||||
|
addElse() {
|
||||||
|
this.dedent();
|
||||||
|
this.addLine("} else {");
|
||||||
|
this.indent();
|
||||||
|
}
|
||||||
|
|
||||||
|
closeIf() {
|
||||||
|
this.dedent();
|
||||||
|
this.addLine("}");
|
||||||
|
}
|
||||||
|
|
||||||
|
getValue(val: any): any {
|
||||||
|
return val in this.variables ? this.getValue(this.variables[val]) : val;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare an expression for being consumed at render time. Its main job
|
||||||
|
* is to
|
||||||
|
* - replace unknown variables by a lookup in the context
|
||||||
|
* - replace already defined variables by their internal name
|
||||||
|
*/
|
||||||
|
formatExpression(expr: string): string {
|
||||||
|
return compileExpr(expr, this.variables);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform string interpolation on the given string. Note that if the whole
|
||||||
|
* string is an expression, it simply returns it (formatted and enclosed in
|
||||||
|
* parentheses).
|
||||||
|
* For instance:
|
||||||
|
* 'Hello {{x}}!' -> `Hello ${x}`
|
||||||
|
* '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b')
|
||||||
|
*/
|
||||||
|
interpolate(s: string): string {
|
||||||
|
let matches = s.match(/\{\{.*?\}\}/g);
|
||||||
|
if (matches && matches[0].length === s.length) {
|
||||||
|
return `(${this.formatExpression(s.slice(2, -2))})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let r = s.replace(/\{\{.*?\}\}/g, s => "${" + this.formatExpression(s.slice(2, -2)) + "}");
|
||||||
|
return "`" + r + "`";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -82,7 +82,7 @@ const STATIC_TOKEN_MAP: { [key: string]: TKind } = {
|
|||||||
")": "RIGHT_PAREN"
|
")": "RIGHT_PAREN"
|
||||||
};
|
};
|
||||||
|
|
||||||
const OPERATORS = ".,===,==,+,!,||,&&,>=,>,<=,<,?,-,*,/,%".split(',');
|
const OPERATORS = ".,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%".split(",");
|
||||||
|
|
||||||
type Tokenizer = (expr: string) => Token | false;
|
type Tokenizer = (expr: string) => Token | false;
|
||||||
|
|
||||||
@@ -231,10 +231,7 @@ export function tokenize(expr: string): Token[] {
|
|||||||
* - or if the previous token is a left brace or a comma, and the next token is
|
* - or if the previous token is a left brace or a comma, and the next token is
|
||||||
* a colon (in that case, this is an object key: `{a: b}`)
|
* a colon (in that case, this is an object key: `{a: b}`)
|
||||||
*/
|
*/
|
||||||
export function compileExpr(
|
export function compileExpr(expr: string, vars: { [key: string]: QWebVar }): string {
|
||||||
expr: string,
|
|
||||||
vars: { [key: string]: QWebVar }
|
|
||||||
): string {
|
|
||||||
const tokens = tokenize(expr);
|
const tokens = tokenize(expr);
|
||||||
let result = "";
|
let result = "";
|
||||||
for (let i = 0; i < tokens.length; i++) {
|
for (let i = 0; i < tokens.length; i++) {
|
||||||
@@ -246,10 +243,7 @@ export function compileExpr(
|
|||||||
if (prevToken) {
|
if (prevToken) {
|
||||||
if (prevToken.type === "OPERATOR" && prevToken.value === ".") {
|
if (prevToken.type === "OPERATOR" && prevToken.value === ".") {
|
||||||
isVar = false;
|
isVar = false;
|
||||||
} else if (
|
} else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") {
|
||||||
prevToken.type === "LEFT_BRACE" ||
|
|
||||||
prevToken.type === "COMMA"
|
|
||||||
) {
|
|
||||||
let nextToken = tokens[i + 1];
|
let nextToken = tokens[i + 1];
|
||||||
if (nextToken && nextToken.type === "COLON") {
|
if (nextToken && nextToken.type === "COLON") {
|
||||||
isVar = false;
|
isVar = false;
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
import { VNode } from "../vdom/index";
|
||||||
|
import { QWeb } from "./qweb";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Owl QWeb Extensions
|
||||||
|
*
|
||||||
|
* This file contains the implementation of non standard QWeb directives, added
|
||||||
|
* by Owl and that will only work on Owl projects:
|
||||||
|
*
|
||||||
|
* - t-on
|
||||||
|
* - t-ref
|
||||||
|
* - t-transition
|
||||||
|
* - t-mounted
|
||||||
|
* - t-slot
|
||||||
|
* - t-model
|
||||||
|
*/
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// t-on
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// these are pieces of code that will be injected into the event handler if
|
||||||
|
// modifiers are specified
|
||||||
|
export const MODS_CODE = {
|
||||||
|
prevent: "e.preventDefault();",
|
||||||
|
self: "if (e.target !== this.elm) {return}",
|
||||||
|
stop: "e.stopPropagation();"
|
||||||
|
};
|
||||||
|
|
||||||
|
QWeb.addDirective({
|
||||||
|
name: "on",
|
||||||
|
priority: 90,
|
||||||
|
atNodeCreation({ ctx, fullName, value, nodeID }) {
|
||||||
|
ctx.rootContext.shouldDefineOwner = true;
|
||||||
|
const [eventName, ...mods] = fullName.slice(5).split(".");
|
||||||
|
if (!eventName) {
|
||||||
|
throw new Error("Missing event name with t-on directive");
|
||||||
|
}
|
||||||
|
let extraArgs;
|
||||||
|
let handlerName = value.replace(/\(.*\)/, function(args) {
|
||||||
|
extraArgs = args.slice(1, -1);
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
ctx.addIf(`!context['${handlerName}']`);
|
||||||
|
ctx.addLine(
|
||||||
|
`throw new Error('Missing handler \\'' + '${handlerName}' + \`\\' when evaluating template '${ctx.templateName.replace(
|
||||||
|
/`/g,
|
||||||
|
"'"
|
||||||
|
)}'\`)`
|
||||||
|
);
|
||||||
|
ctx.closeIf();
|
||||||
|
let params = extraArgs ? `owner, ${ctx.formatExpression(extraArgs)}` : "owner";
|
||||||
|
let handler;
|
||||||
|
if (mods.length > 0) {
|
||||||
|
handler = `function (e) {`;
|
||||||
|
handler += mods
|
||||||
|
.map(function(mod) {
|
||||||
|
return MODS_CODE[mod];
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
handler += `context['${handlerName}'].call(${params}, e);}`;
|
||||||
|
} else {
|
||||||
|
handler = `context['${handlerName}'].bind(${params})`;
|
||||||
|
}
|
||||||
|
if (extraArgs) {
|
||||||
|
ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
|
||||||
|
} else {
|
||||||
|
ctx.addLine(
|
||||||
|
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`
|
||||||
|
);
|
||||||
|
ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// t-ref
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
QWeb.addDirective({
|
||||||
|
name: "ref",
|
||||||
|
priority: 95,
|
||||||
|
atNodeCreation({ ctx, value, addNodeHook }) {
|
||||||
|
const refKey = `ref${ctx.generateID()}`;
|
||||||
|
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
|
||||||
|
addNodeHook("create", `context.refs[${refKey}] = n.elm;`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// t-transition
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
QWeb.utils.nextFrame = function(cb: () => void) {
|
||||||
|
requestAnimationFrame(() => requestAnimationFrame(cb));
|
||||||
|
};
|
||||||
|
|
||||||
|
QWeb.utils.transitionInsert = function(vn: VNode, name: string) {
|
||||||
|
const elm = <HTMLElement>vn.elm;
|
||||||
|
// remove potential duplicated vnode that is currently being removed, to
|
||||||
|
// prevent from having twice the same node in the DOM during an animation
|
||||||
|
const dup = elm.parentElement && elm.parentElement!.querySelector(`*[data-owl-key='${vn.key}']`);
|
||||||
|
if (dup) {
|
||||||
|
dup.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
elm.classList.add(name + "-enter");
|
||||||
|
elm.classList.add(name + "-enter-active");
|
||||||
|
const finalize = () => {
|
||||||
|
elm.classList.remove(name + "-enter-active");
|
||||||
|
elm.classList.remove(name + "-enter-to");
|
||||||
|
};
|
||||||
|
this.nextFrame(() => {
|
||||||
|
elm.classList.remove(name + "-enter");
|
||||||
|
elm.classList.add(name + "-enter-to");
|
||||||
|
whenTransitionEnd(elm, finalize);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
QWeb.utils.transitionRemove = function(vn: VNode, name: string, rm: () => void) {
|
||||||
|
const elm = <HTMLElement>vn.elm;
|
||||||
|
elm.setAttribute("data-owl-key", vn.key!);
|
||||||
|
|
||||||
|
elm.classList.add(name + "-leave");
|
||||||
|
elm.classList.add(name + "-leave-active");
|
||||||
|
const finalize = () => {
|
||||||
|
elm.classList.remove(name + "-leave-active");
|
||||||
|
elm.classList.remove(name + "-leave-to");
|
||||||
|
rm();
|
||||||
|
};
|
||||||
|
this.nextFrame(() => {
|
||||||
|
elm.classList.remove(name + "-leave");
|
||||||
|
elm.classList.add(name + "-leave-to");
|
||||||
|
whenTransitionEnd(elm, finalize);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function getTimeout(delays: Array<string>, durations: Array<string>): number {
|
||||||
|
/* istanbul ignore next */
|
||||||
|
while (delays.length < durations.length) {
|
||||||
|
delays = delays.concat(delays);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.max.apply(
|
||||||
|
null,
|
||||||
|
durations.map((d, i) => {
|
||||||
|
return toMs(d) + toMs(delays[i]);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Old versions of Chromium (below 61.0.3163.100) formats floating pointer numbers
|
||||||
|
// in a locale-dependent way, using a comma instead of a dot.
|
||||||
|
// If comma is not replaced with a dot, the input will be rounded down (i.e. acting
|
||||||
|
// as a floor function) causing unexpected behaviors
|
||||||
|
function toMs(s: string): number {
|
||||||
|
return Number(s.slice(0, -1).replace(",", ".")) * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
function whenTransitionEnd(elm: HTMLElement, cb) {
|
||||||
|
const styles = window.getComputedStyle(elm);
|
||||||
|
const delays: Array<string> = (styles.transitionDelay || "").split(", ");
|
||||||
|
const durations: Array<string> = (styles.transitionDuration || "").split(", ");
|
||||||
|
const timeout: number = getTimeout(delays, durations);
|
||||||
|
if (timeout > 0) {
|
||||||
|
elm.addEventListener("transitionend", cb, { once: true });
|
||||||
|
} else {
|
||||||
|
cb();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QWeb.addDirective({
|
||||||
|
name: "transition",
|
||||||
|
priority: 96,
|
||||||
|
atNodeCreation({ ctx, value, addNodeHook }) {
|
||||||
|
ctx.rootContext.shouldDefineUtils = true;
|
||||||
|
let name = value;
|
||||||
|
const hooks = {
|
||||||
|
insert: `utils.transitionInsert(vn, '${name}');`,
|
||||||
|
remove: `utils.transitionRemove(vn, '${name}', rm);`
|
||||||
|
};
|
||||||
|
for (let hookName in hooks) {
|
||||||
|
addNodeHook(hookName, hooks[hookName]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// t-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
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
QWeb.addDirective({
|
||||||
|
name: "slot",
|
||||||
|
priority: 80,
|
||||||
|
atNodeEncounter({ ctx, value }): boolean {
|
||||||
|
const slotKey = ctx.generateID();
|
||||||
|
ctx.rootContext.shouldDefineOwner = true;
|
||||||
|
ctx.addLine(`const slot${slotKey} = this.slots[context.__owl__.slotId + '_' + '${value}'];`);
|
||||||
|
ctx.addIf(`slot${slotKey}`);
|
||||||
|
ctx.addLine(
|
||||||
|
`slot${slotKey}(context.__owl__.parent, Object.assign({}, extra, {parentNode: c${
|
||||||
|
ctx.parentNode
|
||||||
|
}, vars: extra.vars, parent: owner}));`
|
||||||
|
);
|
||||||
|
ctx.closeIf();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// t-model
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
QWeb.utils.toNumber = function(val: string): number | string {
|
||||||
|
const n = parseFloat(val);
|
||||||
|
return isNaN(n) ? val : n;
|
||||||
|
};
|
||||||
|
|
||||||
|
QWeb.addDirective({
|
||||||
|
name: "model",
|
||||||
|
priority: 42,
|
||||||
|
atNodeCreation({ ctx, nodeID, value, node, fullName }) {
|
||||||
|
const type = node.getAttribute("type");
|
||||||
|
let handler;
|
||||||
|
let event = fullName.includes(".lazy") ? "change" : "input";
|
||||||
|
if (node.tagName === "select") {
|
||||||
|
ctx.addLine(`p${nodeID}.props = {value: context.state['${value}']};`);
|
||||||
|
event = "change";
|
||||||
|
handler = `(ev) => {context.state['${value}'] = ev.target.value}`;
|
||||||
|
} else if (type === "checkbox") {
|
||||||
|
ctx.addLine(`p${nodeID}.props = {checked: context.state['${value}']};`);
|
||||||
|
handler = `(ev) => {context.state['${value}'] = ev.target.checked}`;
|
||||||
|
} else if (type === "radio") {
|
||||||
|
const nodeValue = node.getAttribute("value")!;
|
||||||
|
ctx.addLine(`p${nodeID}.props = {checked:context.state['${value}'] === '${nodeValue}'};`);
|
||||||
|
handler = `(ev) => {context.state['${value}'] = ev.target.value}`;
|
||||||
|
event = "click";
|
||||||
|
} else {
|
||||||
|
ctx.addLine(`p${nodeID}.props = {value: context.state['${value}']};`);
|
||||||
|
const trimCode = fullName.includes(".trim") ? ".trim()" : "";
|
||||||
|
let valueCode = `ev.target.value${trimCode}`;
|
||||||
|
if (fullName.includes(".number")) {
|
||||||
|
ctx.rootContext.shouldDefineUtils = true;
|
||||||
|
valueCode = `utils.toNumber(${valueCode})`;
|
||||||
|
}
|
||||||
|
handler = `(ev) => {context.state['${value}'] = ${valueCode}}`;
|
||||||
|
}
|
||||||
|
ctx.addLine(
|
||||||
|
`extra.handlers['${event}' + ${nodeID}] = extra.handlers['${event}' + ${nodeID}] || (${handler});`
|
||||||
|
);
|
||||||
|
ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers['${event}' + ${nodeID}];`);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import "./base_directives";
|
||||||
|
import "./extensions";
|
||||||
|
|
||||||
|
export { CompiledTemplate, QWeb } from "./qweb";
|
||||||
+179
-217
@@ -1,5 +1,6 @@
|
|||||||
import { VNode, h } from "./vdom";
|
import { EventBus } from "../core/event_bus";
|
||||||
import { QWebVar, compileExpr } from "./qweb_expressions";
|
import { h, patch, VNode } from "../vdom/index";
|
||||||
|
import { Context } from "./context";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Owl QWeb Engine
|
* Owl QWeb Engine
|
||||||
@@ -58,27 +59,11 @@ export interface Directive {
|
|||||||
// Const/global stuff/helpers
|
// Const/global stuff/helpers
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
const DISABLED_TAGS = [
|
const DISABLED_TAGS = ["input", "textarea", "button", "select", "option", "optgroup"];
|
||||||
"input",
|
|
||||||
"textarea",
|
|
||||||
"button",
|
|
||||||
"select",
|
|
||||||
"option",
|
|
||||||
"optgroup"
|
|
||||||
];
|
|
||||||
|
|
||||||
const lineBreakRE = /[\r\n]/;
|
const lineBreakRE = /[\r\n]/;
|
||||||
const whitespaceRE = /\s+/g;
|
const whitespaceRE = /\s+/g;
|
||||||
|
|
||||||
const DIRECTIVE_NAMES = {
|
|
||||||
name: 1,
|
|
||||||
att: 1,
|
|
||||||
attf: 1,
|
|
||||||
key: 1
|
|
||||||
};
|
|
||||||
|
|
||||||
const DIRECTIVES: Directive[] = [];
|
|
||||||
|
|
||||||
const NODE_HOOKS_PARAMS = {
|
const NODE_HOOKS_PARAMS = {
|
||||||
create: "(_, n)",
|
create: "(_, n)",
|
||||||
insert: "vn",
|
insert: "vn",
|
||||||
@@ -86,22 +71,26 @@ const NODE_HOOKS_PARAMS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface Utils {
|
interface Utils {
|
||||||
h: typeof h;
|
toObj(expr: any): Object;
|
||||||
objectToAttrString(obj: Object): string;
|
|
||||||
shallowEqual(p1: Object, p2: Object): boolean;
|
shallowEqual(p1: Object, p2: Object): boolean;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UTILS: Utils = {
|
const UTILS: Utils = {
|
||||||
h: h,
|
toObj(expr) {
|
||||||
objectToAttrString(obj: Object): string {
|
if (typeof expr === "string") {
|
||||||
let classes: string[] = [];
|
expr = expr.trim();
|
||||||
for (let k in obj) {
|
if (!expr) {
|
||||||
if (obj[k]) {
|
return {};
|
||||||
classes.push(k);
|
|
||||||
}
|
}
|
||||||
|
let words = expr.split(/\s+/);
|
||||||
|
let result = {};
|
||||||
|
for (let i = 0; i < words.length; i++) {
|
||||||
|
result[words[i]] = true;
|
||||||
}
|
}
|
||||||
return classes.join(" ");
|
return result;
|
||||||
|
}
|
||||||
|
return expr;
|
||||||
},
|
},
|
||||||
shallowEqual(p1, p2) {
|
shallowEqual(p1, p2) {
|
||||||
for (let k in p1) {
|
for (let k in p1) {
|
||||||
@@ -117,7 +106,27 @@ function parseXML(xml: string): Document {
|
|||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
const doc = parser.parseFromString(xml, "text/xml");
|
const doc = parser.parseFromString(xml, "text/xml");
|
||||||
if (doc.getElementsByTagName("parsererror").length) {
|
if (doc.getElementsByTagName("parsererror").length) {
|
||||||
throw new Error("Invalid XML in template");
|
let msg = "Invalid XML in template.";
|
||||||
|
const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
|
||||||
|
if (parsererrorText) {
|
||||||
|
msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
|
||||||
|
const re = /\d+/g;
|
||||||
|
const firstMatch = re.exec(parsererrorText);
|
||||||
|
if (firstMatch) {
|
||||||
|
const lineNumber = Number(firstMatch[0]);
|
||||||
|
const line = xml.split("\n")[lineNumber - 1];
|
||||||
|
const secondMatch = re.exec(parsererrorText);
|
||||||
|
if (line && secondMatch) {
|
||||||
|
const columnIndex = Number(secondMatch[0]) - 1;
|
||||||
|
if (line[columnIndex]) {
|
||||||
|
msg +=
|
||||||
|
`\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
|
||||||
|
`${line}\n${"-".repeat(columnIndex - 1)}^`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(msg);
|
||||||
}
|
}
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
@@ -127,11 +136,20 @@ let nextID = 1;
|
|||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// QWeb rendering engine
|
// QWeb rendering engine
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
export class QWeb {
|
export class QWeb extends EventBus {
|
||||||
templates: { [name: string]: Template } = {};
|
templates: { [name: string]: Template } = {};
|
||||||
utils = UTILS;
|
static utils = UTILS;
|
||||||
static widgets = Object.create(null);
|
static components = Object.create(null);
|
||||||
|
|
||||||
|
static DIRECTIVE_NAMES: { [key: string]: 1 } = {
|
||||||
|
name: 1,
|
||||||
|
att: 1,
|
||||||
|
attf: 1,
|
||||||
|
key: 1
|
||||||
|
};
|
||||||
|
static DIRECTIVES: Directive[] = [];
|
||||||
|
|
||||||
|
h = h;
|
||||||
// dev mode enables better error messages or more costly validations
|
// dev mode enables better error messages or more costly validations
|
||||||
static dev: boolean = false;
|
static dev: boolean = false;
|
||||||
|
|
||||||
@@ -140,38 +158,47 @@ export class QWeb {
|
|||||||
// able to map a qweb instance to a template name.
|
// able to map a qweb instance to a template name.
|
||||||
id = nextID++;
|
id = nextID++;
|
||||||
|
|
||||||
// slots contains sub templates defined with t-set inside t-widget nodes, and
|
// slots contains sub templates defined with t-set inside t-component nodes, and
|
||||||
// are meant to be used by the t-slot directive.
|
// are meant to be used by the t-slot directive.
|
||||||
slots = {};
|
slots = {};
|
||||||
nextSlotId = 1;
|
nextSlotId = 1;
|
||||||
|
|
||||||
|
isUpdating: boolean = false;
|
||||||
|
|
||||||
constructor(data?: string) {
|
constructor(data?: string) {
|
||||||
|
super();
|
||||||
if (data) {
|
if (data) {
|
||||||
this.addTemplates(data);
|
this.addTemplates(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static addDirective(directive: Directive) {
|
static addDirective(directive: Directive) {
|
||||||
DIRECTIVES.push(directive);
|
if (directive.name in QWeb.DIRECTIVE_NAMES) {
|
||||||
DIRECTIVE_NAMES[directive.name] = 1;
|
throw new Error(`Directive "${directive.name} already registered`);
|
||||||
DIRECTIVES.sort((d1, d2) => d1.priority - d2.priority);
|
}
|
||||||
|
QWeb.DIRECTIVES.push(directive);
|
||||||
|
QWeb.DIRECTIVE_NAMES[directive.name] = 1;
|
||||||
|
QWeb.DIRECTIVES.sort((d1, d2) => d1.priority - d2.priority);
|
||||||
if (directive.extraNames) {
|
if (directive.extraNames) {
|
||||||
directive.extraNames.forEach(n => (DIRECTIVE_NAMES[n] = 1));
|
directive.extraNames.forEach(n => (QWeb.DIRECTIVE_NAMES[n] = 1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static register(name: string, Component: any) {
|
static register(name: string, Component: any) {
|
||||||
if (QWeb.widgets[name]) {
|
if (QWeb.components[name]) {
|
||||||
throw new Error(`Component '${name}' has already been registered`);
|
throw new Error(`Component '${name}' has already been registered`);
|
||||||
}
|
}
|
||||||
QWeb.widgets[name] = Component;
|
QWeb.components[name] = Component;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a template to the internal template map. Note that it is not
|
* Add a template to the internal template map. Note that it is not
|
||||||
* immediately compiled.
|
* immediately compiled.
|
||||||
*/
|
*/
|
||||||
addTemplate(name: string, xmlString: string) {
|
addTemplate(name: string, xmlString: string, allowDuplicate?: boolean) {
|
||||||
|
if (allowDuplicate && name in this.templates) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const doc = parseXML(xmlString);
|
const doc = parseXML(xmlString);
|
||||||
if (!doc.firstChild) {
|
if (!doc.firstChild) {
|
||||||
throw new Error("Invalid template (should not be empty)");
|
throw new Error("Invalid template (should not be empty)");
|
||||||
@@ -234,9 +261,7 @@ export class QWeb {
|
|||||||
return a + b;
|
return a + b;
|
||||||
}) > 1
|
}) > 1
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error("Only one conditional branching directive is allowed per node");
|
||||||
"Only one conditional branching directive is allowed per node"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
// All text nodes between branch nodes are removed
|
// All text nodes between branch nodes are removed
|
||||||
let textNode;
|
let textNode;
|
||||||
@@ -266,53 +291,93 @@ export class QWeb {
|
|||||||
return template.fn.call(this, context, extra);
|
return template.fn.call(this, context, extra);
|
||||||
}
|
}
|
||||||
|
|
||||||
_compile(name: string, elem: Element): CompiledTemplate {
|
/**
|
||||||
|
* Render a template to a html string.
|
||||||
|
*
|
||||||
|
* Note that this is more limited than the `render` method: it is not suitable
|
||||||
|
* to render a full component tree, since this is an asynchronous operation.
|
||||||
|
* This method can only render templates without components.
|
||||||
|
*/
|
||||||
|
renderToString(name: string, context: EvalContext = {}): string {
|
||||||
|
const vnode = this.render(name, context);
|
||||||
|
if (vnode.sel === undefined) {
|
||||||
|
return vnode.text!;
|
||||||
|
}
|
||||||
|
const node = document.createElement(vnode.sel);
|
||||||
|
const result = patch(node, vnode);
|
||||||
|
return (<HTMLElement>result.elm).outerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force all widgets connected to this QWeb instance to rerender themselves.
|
||||||
|
*
|
||||||
|
* This method is mostly useful for external code that want to modify the
|
||||||
|
* application in some cases. For example, a router plugin.
|
||||||
|
*/
|
||||||
|
forceUpdate() {
|
||||||
|
this.isUpdating = true;
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
if (this.isUpdating) {
|
||||||
|
this.isUpdating = false;
|
||||||
|
this.trigger("update");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_compile(name: string, elem: Element, parentContext?: Context): CompiledTemplate {
|
||||||
const isDebug = elem.attributes.hasOwnProperty("t-debug");
|
const isDebug = elem.attributes.hasOwnProperty("t-debug");
|
||||||
const ctx = new Context(name);
|
const ctx = new Context(name);
|
||||||
|
if (parentContext) {
|
||||||
|
ctx.variables = Object.create(parentContext.variables);
|
||||||
|
ctx.nextID = parentContext.parentNode! + 1;
|
||||||
|
ctx.parentNode = parentContext.parentNode!;
|
||||||
|
ctx.allowMultipleRoots = true;
|
||||||
|
ctx.hasParentWidget = true;
|
||||||
|
ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`);
|
||||||
|
|
||||||
|
for (let v in parentContext.variables) {
|
||||||
|
let variable = <any>parentContext.variables[v];
|
||||||
|
if (variable.id) {
|
||||||
|
ctx.addLine(`let ${variable.id} = extra.vars.${variable.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (parentContext) {
|
||||||
|
ctx.addLine(" Object.assign(context, extra.scope);");
|
||||||
|
}
|
||||||
this._compileNode(elem, ctx);
|
this._compileNode(elem, ctx);
|
||||||
|
|
||||||
if (ctx.shouldProtectContext) {
|
if (!parentContext) {
|
||||||
ctx.code.unshift(" context = Object.create(context);");
|
if (ctx.shouldDefineResult) {
|
||||||
}
|
ctx.addLine(`return result;`);
|
||||||
if (ctx.shouldDefineOwner) {
|
} else {
|
||||||
// this is necessary to prevent some directives (t-forach for ex) to
|
|
||||||
// pollute the rendering context by adding some keys in it.
|
|
||||||
ctx.code.unshift(" let owner = context;");
|
|
||||||
}
|
|
||||||
if (ctx.shouldDefineQWeb) {
|
|
||||||
ctx.code.unshift(" let QWeb = this.constructor;");
|
|
||||||
}
|
|
||||||
if (ctx.shouldDefineUtils) {
|
|
||||||
ctx.code.unshift(" let utils = this.utils;");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ctx.rootNode) {
|
if (!ctx.rootNode) {
|
||||||
throw new Error("A template should have one root node");
|
throw new Error(`A template should have one root node (${ctx.templateName})`);
|
||||||
}
|
}
|
||||||
ctx.addLine(`return vn${ctx.rootNode};`);
|
ctx.addLine(`return vn${ctx.rootNode};`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let code = ctx.generateCode();
|
||||||
|
|
||||||
let template;
|
let template;
|
||||||
try {
|
try {
|
||||||
template = new Function(
|
template = new Function("context", "extra", code.join("\n")) as CompiledTemplate;
|
||||||
"context",
|
|
||||||
"extra",
|
|
||||||
ctx.code.join("\n")
|
|
||||||
) as CompiledTemplate;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const templateName = ctx.templateName.replace(/`/g, "'");
|
const templateName = ctx.templateName.replace(/`/g, "'");
|
||||||
console.groupCollapsed(`Invalid Code generated by ${templateName}`);
|
console.groupCollapsed(`Invalid Code generated by ${templateName}`);
|
||||||
console.warn(ctx.code.join("\n"));
|
console.warn(code.join("\n"));
|
||||||
console.groupEnd();
|
console.groupEnd();
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Invalid generated code while compiling template '${templateName}': ${
|
`Invalid generated code while compiling template '${templateName}': ${e.message}`
|
||||||
e.message
|
|
||||||
}`
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (isDebug) {
|
if (isDebug) {
|
||||||
console.log(
|
const tpl = this.templates[name];
|
||||||
`Template: ${this.templates[name].elem.outerHTML}\nCompiled code:\n` +
|
if (tpl) {
|
||||||
template.toString()
|
const msg = `Template: ${tpl.elem.outerHTML}\nCompiled code:\n${template.toString()}`;
|
||||||
);
|
console.log(msg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return template;
|
return template;
|
||||||
}
|
}
|
||||||
@@ -346,6 +411,12 @@ export class QWeb {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const firstLetter = node.tagName[0];
|
||||||
|
if (firstLetter === firstLetter.toUpperCase()) {
|
||||||
|
// this is a component, we modify in place the xml document to change
|
||||||
|
// <SomeComponent ... /> to <t t-component="SomeComponent" ... />
|
||||||
|
node.setAttribute("t-component", node.tagName);
|
||||||
|
}
|
||||||
const attributes = (<Element>node).attributes;
|
const attributes = (<Element>node).attributes;
|
||||||
|
|
||||||
const validDirectives: {
|
const validDirectives: {
|
||||||
@@ -362,16 +433,16 @@ export class QWeb {
|
|||||||
let attrName = attributes[i].name;
|
let attrName = attributes[i].name;
|
||||||
if (attrName.startsWith("t-")) {
|
if (attrName.startsWith("t-")) {
|
||||||
let dName = attrName.slice(2).split(/-|\./)[0];
|
let dName = attrName.slice(2).split(/-|\./)[0];
|
||||||
if (!(dName in DIRECTIVE_NAMES)) {
|
if (!(dName in QWeb.DIRECTIVE_NAMES)) {
|
||||||
throw new Error(`Unknown QWeb directive: '${attrName}'`);
|
throw new Error(`Unknown QWeb directive: '${attrName}'`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const DIR_N = DIRECTIVES.length;
|
const DIR_N = QWeb.DIRECTIVES.length;
|
||||||
const ATTR_N = attributes.length;
|
const ATTR_N = attributes.length;
|
||||||
for (let i = 0; i < DIR_N; i++) {
|
for (let i = 0; i < DIR_N; i++) {
|
||||||
let directive = DIRECTIVES[i];
|
let directive = QWeb.DIRECTIVES[i];
|
||||||
let fullName;
|
let fullName;
|
||||||
let value;
|
let value;
|
||||||
for (let j = 0; j < ATTR_N; j++) {
|
for (let j = 0; j < ATTR_N; j++) {
|
||||||
@@ -384,7 +455,7 @@ export class QWeb {
|
|||||||
fullName = name;
|
fullName = name;
|
||||||
value = attributes[j].textContent;
|
value = attributes[j].textContent;
|
||||||
validDirectives.push({ directive, value, fullName });
|
validDirectives.push({ directive, value, fullName });
|
||||||
if (directive.name === "on" || directive.name === 'model') {
|
if (directive.name === "on" || directive.name === "model") {
|
||||||
withHandlers = true;
|
withHandlers = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -453,11 +524,7 @@ export class QWeb {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_compileGenericNode(
|
_compileGenericNode(node: ChildNode, ctx: Context, withHandlers: boolean = true): number {
|
||||||
node: ChildNode,
|
|
||||||
ctx: Context,
|
|
||||||
withHandlers: boolean = true
|
|
||||||
): number {
|
|
||||||
// nodeType 1 is generic tag
|
// nodeType 1 is generic tag
|
||||||
if (node.nodeType !== 1) {
|
if (node.nodeType !== 1) {
|
||||||
throw new Error("unsupported node type");
|
throw new Error("unsupported node type");
|
||||||
@@ -481,27 +548,31 @@ export class QWeb {
|
|||||||
if (key === "disabled" && DISABLED_TAGS.indexOf(node.nodeName) > -1) {
|
if (key === "disabled" && DISABLED_TAGS.indexOf(node.nodeName) > -1) {
|
||||||
isProp = true;
|
isProp = true;
|
||||||
}
|
}
|
||||||
if (
|
if ((key === "readonly" && node.nodeName === "input") || node.nodeName === "textarea") {
|
||||||
(key === "readonly" && node.nodeName === "input") ||
|
|
||||||
node.nodeName === "textarea"
|
|
||||||
) {
|
|
||||||
isProp = true;
|
isProp = true;
|
||||||
}
|
}
|
||||||
if (isProp) {
|
if (isProp) {
|
||||||
props.push(`${key}: _${val}`);
|
props.push(`${key}: _${val}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let classObj = "";
|
||||||
|
|
||||||
for (let i = 0; i < attributes.length; i++) {
|
for (let i = 0; i < attributes.length; i++) {
|
||||||
let name = attributes[i].name;
|
let name = attributes[i].name;
|
||||||
const value = attributes[i].textContent!;
|
const value = attributes[i].textContent!;
|
||||||
|
|
||||||
// regular attributes
|
// regular attributes
|
||||||
if (
|
if (!name.startsWith("t-") && !(<Element>node).getAttribute("t-attf-" + name)) {
|
||||||
!name.startsWith("t-") &&
|
|
||||||
!(<Element>node).getAttribute("t-attf-" + name)
|
|
||||||
) {
|
|
||||||
const attID = ctx.generateID();
|
const attID = ctx.generateID();
|
||||||
|
if (name === "class") {
|
||||||
|
let classDef = value
|
||||||
|
.trim()
|
||||||
|
.split(/\s+/)
|
||||||
|
.map(a => `'${a}':true`)
|
||||||
|
.join(",");
|
||||||
|
classObj = `_${ctx.generateID()}`;
|
||||||
|
ctx.addLine(`let ${classObj} = {${classDef}};`);
|
||||||
|
} else {
|
||||||
ctx.addLine(`var _${attID} = '${value}';`);
|
ctx.addLine(`var _${attID} = '${value}';`);
|
||||||
if (!name.match(/^[a-zA-Z]+$/)) {
|
if (!name.match(/^[a-zA-Z]+$/)) {
|
||||||
// attribute contains 'non letters' => we want to quote it
|
// attribute contains 'non letters' => we want to quote it
|
||||||
@@ -510,18 +581,24 @@ export class QWeb {
|
|||||||
attrs.push(`${name}: _${attID}`);
|
attrs.push(`${name}: _${attID}`);
|
||||||
handleBooleanProps(name, attID);
|
handleBooleanProps(name, attID);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// dynamic attributes
|
// dynamic attributes
|
||||||
if (name.startsWith("t-att-")) {
|
if (name.startsWith("t-att-")) {
|
||||||
let attName = name.slice(6);
|
let attName = name.slice(6);
|
||||||
const v = ctx.getValue(value);
|
const v = ctx.getValue(value);
|
||||||
let formattedValue = v.id || ctx.formatExpression(v);
|
let formattedValue = v.id || ctx.formatExpression(v);
|
||||||
if (
|
|
||||||
formattedValue[0] === "{" &&
|
if (attName === "class") {
|
||||||
formattedValue[formattedValue.length - 1] === "}"
|
ctx.rootContext.shouldDefineUtils = true;
|
||||||
) {
|
formattedValue = `utils.toObj(${formattedValue})`;
|
||||||
formattedValue = `this.utils.objectToAttrString(${formattedValue})`;
|
if (classObj) {
|
||||||
|
ctx.addLine(`Object.assign(${classObj}, ${formattedValue})`);
|
||||||
|
} else {
|
||||||
|
classObj = `_${ctx.generateID()}`;
|
||||||
|
ctx.addLine(`let ${classObj} = ${formattedValue};`);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
const attID = ctx.generateID();
|
const attID = ctx.generateID();
|
||||||
if (!attName.match(/^[a-zA-Z]+$/)) {
|
if (!attName.match(/^[a-zA-Z]+$/)) {
|
||||||
// attribute contains 'non letters' => we want to quote it
|
// attribute contains 'non letters' => we want to quote it
|
||||||
@@ -534,15 +611,14 @@ export class QWeb {
|
|||||||
const attValueID = ctx.generateID();
|
const attValueID = ctx.generateID();
|
||||||
ctx.addLine(`var _${attValueID} = ${formattedValue};`);
|
ctx.addLine(`var _${attValueID} = ${formattedValue};`);
|
||||||
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
|
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
|
||||||
const attrIndex = attrs.findIndex(att =>
|
const attrIndex = attrs.findIndex(att => att.startsWith(attName + ":"));
|
||||||
att.startsWith(attName + ":")
|
|
||||||
);
|
|
||||||
attrs.splice(attrIndex, 1);
|
attrs.splice(attrIndex, 1);
|
||||||
}
|
}
|
||||||
ctx.addLine(`var _${attID} = ${formattedValue};`);
|
ctx.addLine(`var _${attID} = ${formattedValue};`);
|
||||||
attrs.push(`${attName}: _${attID}`);
|
attrs.push(`${attName}: _${attID}`);
|
||||||
handleBooleanProps(attName, attID);
|
handleBooleanProps(attName, attID);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (name.startsWith("t-attf-")) {
|
if (name.startsWith("t-attf-")) {
|
||||||
let attName = name.slice(7);
|
let attName = name.slice(7);
|
||||||
@@ -582,6 +658,9 @@ export class QWeb {
|
|||||||
if (props.length > 0) {
|
if (props.length > 0) {
|
||||||
parts.push(`props:{${props.join(",")}}`);
|
parts.push(`props:{${props.join(",")}}`);
|
||||||
}
|
}
|
||||||
|
if (classObj) {
|
||||||
|
parts.push(`class:${classObj}`);
|
||||||
|
}
|
||||||
if (withHandlers) {
|
if (withHandlers) {
|
||||||
parts.push(`on:{}`);
|
parts.push(`on:{}`);
|
||||||
}
|
}
|
||||||
@@ -598,9 +677,7 @@ export class QWeb {
|
|||||||
ctx.addLine(`}`);
|
ctx.addLine(`}`);
|
||||||
ctx.closeIf();
|
ctx.closeIf();
|
||||||
}
|
}
|
||||||
ctx.addLine(
|
ctx.addLine(`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`);
|
||||||
`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`
|
|
||||||
);
|
|
||||||
if (ctx.parentNode) {
|
if (ctx.parentNode) {
|
||||||
ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`);
|
ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`);
|
||||||
}
|
}
|
||||||
@@ -616,118 +693,3 @@ export class QWeb {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Compilation Context
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
export class Context {
|
|
||||||
nextID: number = 1;
|
|
||||||
code: string[] = [];
|
|
||||||
variables: { [key: string]: QWebVar } = {};
|
|
||||||
escaping: boolean = false;
|
|
||||||
parentNode: number | null = null;
|
|
||||||
parentTextNode: number | null = null;
|
|
||||||
rootNode: number | null = null;
|
|
||||||
indentLevel: number = 0;
|
|
||||||
rootContext: Context;
|
|
||||||
caller: Element | undefined;
|
|
||||||
shouldDefineOwner: boolean = false;
|
|
||||||
shouldDefineQWeb: boolean = false;
|
|
||||||
shouldDefineUtils: boolean = false;
|
|
||||||
shouldProtectContext: boolean = false;
|
|
||||||
inLoop: boolean = false;
|
|
||||||
inPreTag: boolean = false;
|
|
||||||
templateName: string;
|
|
||||||
|
|
||||||
constructor(name?: string) {
|
|
||||||
this.rootContext = this;
|
|
||||||
this.templateName = name || "noname";
|
|
||||||
this.addLine("var h = this.utils.h;");
|
|
||||||
}
|
|
||||||
|
|
||||||
generateID(): number {
|
|
||||||
const id = this.rootContext.nextID++;
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
withParent(node: number): Context {
|
|
||||||
if (this === this.rootContext && (this.parentNode || this.parentTextNode)) {
|
|
||||||
throw new Error("A template should not have more than one root node");
|
|
||||||
}
|
|
||||||
if (!this.rootContext.rootNode) {
|
|
||||||
this.rootContext.rootNode = node;
|
|
||||||
}
|
|
||||||
return this.subContext("parentNode", node);
|
|
||||||
}
|
|
||||||
|
|
||||||
subContext(key: keyof Context, value: any): Context {
|
|
||||||
const newContext = Object.create(this);
|
|
||||||
newContext[key] = value;
|
|
||||||
return newContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
indent() {
|
|
||||||
this.indentLevel++;
|
|
||||||
}
|
|
||||||
|
|
||||||
dedent() {
|
|
||||||
this.indentLevel--;
|
|
||||||
}
|
|
||||||
|
|
||||||
addLine(line: string) {
|
|
||||||
const prefix = new Array(this.indentLevel + 2).join(" ");
|
|
||||||
this.code.push(prefix + line);
|
|
||||||
}
|
|
||||||
|
|
||||||
addIf(condition: string) {
|
|
||||||
this.addLine(`if (${condition}) {`);
|
|
||||||
this.indent();
|
|
||||||
}
|
|
||||||
|
|
||||||
addElse() {
|
|
||||||
this.dedent();
|
|
||||||
this.addLine("} else {");
|
|
||||||
this.indent();
|
|
||||||
}
|
|
||||||
|
|
||||||
closeIf() {
|
|
||||||
this.dedent();
|
|
||||||
this.addLine("}");
|
|
||||||
}
|
|
||||||
|
|
||||||
getValue(val: any): any {
|
|
||||||
return val in this.variables ? this.getValue(this.variables[val]) : val;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prepare an expression for being consumed at render time. Its main job
|
|
||||||
* is to
|
|
||||||
* - replace unknown variables by a lookup in the context
|
|
||||||
* - replace already defined variables by their internal name
|
|
||||||
*/
|
|
||||||
formatExpression(expr: string): string {
|
|
||||||
return compileExpr(expr, this.variables);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform string interpolation on the given string. Note that if the whole
|
|
||||||
* string is an expression, it simply returns it (formatted and enclosed in
|
|
||||||
* parentheses).
|
|
||||||
* For instance:
|
|
||||||
* 'Hello {{x}}!' -> `Hello ${x}`
|
|
||||||
* '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b')
|
|
||||||
*/
|
|
||||||
interpolate(s: string): string {
|
|
||||||
let matches = s.match(/\{\{.*?\}\}/g);
|
|
||||||
if (matches && matches[0].length === s.length) {
|
|
||||||
return `(${this.formatExpression(s.slice(2, -2))})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let r = s.replace(
|
|
||||||
/\{\{.*?\}\}/g,
|
|
||||||
s => "${" + this.formatExpression(s.slice(2, -2)) + "}"
|
|
||||||
);
|
|
||||||
return "`" + r + "`";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,722 +0,0 @@
|
|||||||
import { QWeb, UTILS } from "./qweb_core";
|
|
||||||
import { VNode } from "./vdom";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Owl QWeb Extensions
|
|
||||||
*
|
|
||||||
* This file contains the implementation of non standard QWeb directives, added
|
|
||||||
* by Owl and that will only work on Owl projects:
|
|
||||||
*
|
|
||||||
* - t-on
|
|
||||||
* - t-ref
|
|
||||||
* - t-transition
|
|
||||||
* - t-widget/t-keepalive
|
|
||||||
* - t-mounted
|
|
||||||
* - t-slot
|
|
||||||
* - t-model
|
|
||||||
*/
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// t-on
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// these are pieces of code that will be injected into the event handler if
|
|
||||||
// modifiers are specified
|
|
||||||
const MODS_CODE = {
|
|
||||||
prevent: "e.preventDefault();",
|
|
||||||
self: "if (e.target !== this.elm) {return}",
|
|
||||||
stop: "e.stopPropagation();"
|
|
||||||
};
|
|
||||||
|
|
||||||
QWeb.addDirective({
|
|
||||||
name: "on",
|
|
||||||
priority: 90,
|
|
||||||
atNodeCreation({ ctx, fullName, value, nodeID }) {
|
|
||||||
ctx.rootContext.shouldDefineOwner = true;
|
|
||||||
const [eventName, ...mods] = fullName.slice(5).split(".");
|
|
||||||
if (!eventName) {
|
|
||||||
throw new Error("Missing event name with t-on directive");
|
|
||||||
}
|
|
||||||
let extraArgs;
|
|
||||||
let handlerName = value.replace(/\(.*\)/, function(args) {
|
|
||||||
extraArgs = args.slice(1, -1);
|
|
||||||
return "";
|
|
||||||
});
|
|
||||||
ctx.addIf(`!context['${handlerName}']`);
|
|
||||||
ctx.addLine(
|
|
||||||
`throw new Error('Missing handler \\'' + '${handlerName}' + \`\\' when evaluating template '${ctx.templateName.replace(
|
|
||||||
/`/g,
|
|
||||||
"'"
|
|
||||||
)}'\`)`
|
|
||||||
);
|
|
||||||
ctx.closeIf();
|
|
||||||
let params = extraArgs
|
|
||||||
? `owner, ${ctx.formatExpression(extraArgs)}`
|
|
||||||
: "owner";
|
|
||||||
let handler;
|
|
||||||
if (mods.length > 0) {
|
|
||||||
handler = `function (e) {`;
|
|
||||||
handler += mods
|
|
||||||
.map(function(mod) {
|
|
||||||
return MODS_CODE[mod];
|
|
||||||
})
|
|
||||||
.join("");
|
|
||||||
handler += `context['${handlerName}'].call(${params}, e);}`;
|
|
||||||
} else {
|
|
||||||
handler = `context['${handlerName}'].bind(${params})`;
|
|
||||||
}
|
|
||||||
if (extraArgs) {
|
|
||||||
ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
|
|
||||||
} else {
|
|
||||||
ctx.addLine(
|
|
||||||
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`
|
|
||||||
);
|
|
||||||
ctx.addLine(
|
|
||||||
`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// t-ref
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
QWeb.addDirective({
|
|
||||||
name: "ref",
|
|
||||||
priority: 95,
|
|
||||||
atNodeCreation({ ctx, value, addNodeHook }) {
|
|
||||||
const refKey = `ref${ctx.generateID()}`;
|
|
||||||
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
|
|
||||||
addNodeHook("create", `context.refs[${refKey}] = n.elm;`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// t-transition
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
UTILS.nextFrame = function(cb: () => void) {
|
|
||||||
requestAnimationFrame(() => requestAnimationFrame(cb));
|
|
||||||
};
|
|
||||||
|
|
||||||
UTILS.transitionInsert = function(vn: VNode, name: string) {
|
|
||||||
const elm = <HTMLElement>vn.elm;
|
|
||||||
// remove potential duplicated vnode that is currently being removed, to
|
|
||||||
// prevent from having twice the same node in the DOM during an animation
|
|
||||||
const dup =
|
|
||||||
elm.parentElement &&
|
|
||||||
elm.parentElement!.querySelector(`*[data-owl-key='${vn.key}']`);
|
|
||||||
if (dup) {
|
|
||||||
dup.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
elm.classList.add(name + "-enter");
|
|
||||||
elm.classList.add(name + "-enter-active");
|
|
||||||
const finalize = () => {
|
|
||||||
elm.classList.remove(name + "-enter-active");
|
|
||||||
elm.classList.remove(name + "-enter-to");
|
|
||||||
};
|
|
||||||
this.nextFrame(() => {
|
|
||||||
elm.classList.remove(name + "-enter");
|
|
||||||
elm.classList.add(name + "-enter-to");
|
|
||||||
whenTransitionEnd(elm, finalize);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
UTILS.transitionRemove = function(vn: VNode, name: string, rm: () => void) {
|
|
||||||
const elm = <HTMLElement>vn.elm;
|
|
||||||
elm.setAttribute("data-owl-key", vn.key!);
|
|
||||||
|
|
||||||
elm.classList.add(name + "-leave");
|
|
||||||
elm.classList.add(name + "-leave-active");
|
|
||||||
const finalize = () => {
|
|
||||||
elm.classList.remove(name + "-leave-active");
|
|
||||||
elm.classList.remove(name + "-leave-to");
|
|
||||||
rm();
|
|
||||||
};
|
|
||||||
this.nextFrame(() => {
|
|
||||||
elm.classList.remove(name + "-leave");
|
|
||||||
elm.classList.add(name + "-leave-to");
|
|
||||||
whenTransitionEnd(elm, finalize);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
function getTimeout(delays: Array<string>, durations: Array<string>): number {
|
|
||||||
/* istanbul ignore next */
|
|
||||||
while (delays.length < durations.length) {
|
|
||||||
delays = delays.concat(delays);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Math.max.apply(
|
|
||||||
null,
|
|
||||||
durations.map((d, i) => {
|
|
||||||
return toMs(d) + toMs(delays[i]);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Old versions of Chromium (below 61.0.3163.100) formats floating pointer numbers
|
|
||||||
// in a locale-dependent way, using a comma instead of a dot.
|
|
||||||
// If comma is not replaced with a dot, the input will be rounded down (i.e. acting
|
|
||||||
// as a floor function) causing unexpected behaviors
|
|
||||||
function toMs(s: string): number {
|
|
||||||
return Number(s.slice(0, -1).replace(",", ".")) * 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
function whenTransitionEnd(elm: HTMLElement, cb) {
|
|
||||||
const styles = window.getComputedStyle(elm);
|
|
||||||
const delays: Array<string> = (styles.transitionDelay || "").split(", ");
|
|
||||||
const durations: Array<string> = (styles.transitionDuration || "").split(
|
|
||||||
", "
|
|
||||||
);
|
|
||||||
const timeout: number = getTimeout(delays, durations);
|
|
||||||
if (timeout > 0) {
|
|
||||||
elm.addEventListener("transitionend", cb, { once: true });
|
|
||||||
} else {
|
|
||||||
cb();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QWeb.addDirective({
|
|
||||||
name: "transition",
|
|
||||||
priority: 96,
|
|
||||||
atNodeCreation({ value, addNodeHook }) {
|
|
||||||
let name = value;
|
|
||||||
const hooks = {
|
|
||||||
insert: `this.utils.transitionInsert(vn, '${name}');`,
|
|
||||||
remove: `this.utils.transitionRemove(vn, '${name}', rm);`
|
|
||||||
};
|
|
||||||
for (let hookName in hooks) {
|
|
||||||
addNodeHook(hookName, hooks[hookName]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// t-widget
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const T_WIDGET_MODS_CODE = Object.assign({}, MODS_CODE, {
|
|
||||||
self: "if (e.target !== vn.elm) {return}"
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The t-widget directive is certainly a complicated and hard to maintain piece
|
|
||||||
* of code. To help you, fellow developer, if you have to maintain it, I offer
|
|
||||||
* you this advice: Good luck...
|
|
||||||
*
|
|
||||||
* Since it is not 'direct' code, but rather code that generates other code, it
|
|
||||||
* is not easy to understand. To help you, here is a detailed and commented
|
|
||||||
* explanation of the code generated by the t-widget directive for the following
|
|
||||||
* situation:
|
|
||||||
* ```xml
|
|
||||||
* <t t-widget="child"
|
|
||||||
* t-key="'somestring'"
|
|
||||||
* flag="state.flag"
|
|
||||||
* t-transition="fade"/>
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* // we assign utils on top of the function because it will be useful for
|
|
||||||
* // each widgets
|
|
||||||
* let utils = this.utils;
|
|
||||||
*
|
|
||||||
* // this is the virtual node representing the parent div
|
|
||||||
* let c1 = [], p1 = { key: 1 };
|
|
||||||
* var vn1 = h("div", p1, c1);
|
|
||||||
*
|
|
||||||
* // t-widget directive: we start by evaluating the expression given by t-key:
|
|
||||||
* let key5 = "somestring";
|
|
||||||
*
|
|
||||||
* // def3 is the promise that will contain later either the new widget
|
|
||||||
* // creation, or the props update...
|
|
||||||
* let def3;
|
|
||||||
*
|
|
||||||
* // this is kind of tricky: we need here to find if the widget was already
|
|
||||||
* // created by a previous rendering. This is done by checking the internal
|
|
||||||
* // `cmap` (children map) of the parent widget: it maps keys to widget ids,
|
|
||||||
* // and, then, if there is an id, we look into the children list to get the
|
|
||||||
* // instance
|
|
||||||
* let w4 =
|
|
||||||
* key5 in context.__owl__.cmap
|
|
||||||
* ? context.__owl__.children[context.__owl__.cmap[key5]]
|
|
||||||
* : false;
|
|
||||||
*
|
|
||||||
* // We keep the index of the position of the widget in the closure. We push
|
|
||||||
* // null to reserve the slot, and will replace it later by the widget vnode,
|
|
||||||
* // when it will be ready (do not forget that preparing/rendering a widget is
|
|
||||||
* // asynchronous)
|
|
||||||
* let _2_index = c1.length;
|
|
||||||
* c1.push(null);
|
|
||||||
*
|
|
||||||
* // we evaluate here the props given to the component. It is done here to be
|
|
||||||
* // able to easily reference it later, and also, it might be an expensive
|
|
||||||
* // computation, so it is certainly better to do it only once
|
|
||||||
* let props4 = { flag: context["state"].flag };
|
|
||||||
*
|
|
||||||
* // If we have a widget, currently rendering, but not ready yet, we do not want
|
|
||||||
* // to wait for it to be ready if we can avoid it
|
|
||||||
* if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
|
||||||
* // we check if the props are the same. In that case, we can simply reuse
|
|
||||||
* // the previous rendering and skip all useless work
|
|
||||||
* if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
|
||||||
* def3 = w4.__owl__.renderPromise;
|
|
||||||
* } else {
|
|
||||||
* // if the props are not the same, we destroy the widget and starts anew.
|
|
||||||
* // this will be faster than waiting for its rendering, then updating it
|
|
||||||
* w4.destroy();
|
|
||||||
* w4 = false;
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* if (!w4) {
|
|
||||||
* // in this situation, we need to create a new widget. First step is
|
|
||||||
* // to get a reference to the class, then create an instance with
|
|
||||||
* // current context as parent, and the props.
|
|
||||||
* let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
|
|
||||||
|
|
||||||
* if (!W4) {
|
|
||||||
* throw new Error("Cannot find the definition of widget 'child'");
|
|
||||||
* }
|
|
||||||
* w4 = new W4(owner, props4);
|
|
||||||
*
|
|
||||||
* // Whenever we rerender the parent widget, we need to be sure that we
|
|
||||||
* // are able to find the widget instance. To do that, we register it to
|
|
||||||
* // the parent cmap (children map). Note that the 'template' key is
|
|
||||||
* // used here, since this is what identify the widget from the template
|
|
||||||
* // perspective.
|
|
||||||
* context.__owl__.cmap[key5] = w4.__owl__.id;
|
|
||||||
*
|
|
||||||
* // _prepare is called, to basically call willStart, then render the
|
|
||||||
* // widget
|
|
||||||
* def3 = w4._prepare();
|
|
||||||
*
|
|
||||||
* def3 = def3.then(vnode => {
|
|
||||||
* // we create here a virtual node for the parent (NOT the widget). This
|
|
||||||
* // means that the vdom of the parent will be stopped here, and from
|
|
||||||
* // the parent's perspective, it simply is a vnode with no children.
|
|
||||||
* // However, it shares the same dom element with the component root
|
|
||||||
* // vnode.
|
|
||||||
* let pvnode = h(vnode.sel, { key: key5 });
|
|
||||||
*
|
|
||||||
* // we add hooks to the parent vnode so we can interact with the new
|
|
||||||
* // widget at the proper time
|
|
||||||
* pvnode.data.hook = {
|
|
||||||
* insert(vn) {
|
|
||||||
* // the _mount method will patch the widget vdom into the elm vn.elm,
|
|
||||||
* // then call the mounted hooks. However, suprisingly, the snabbdom
|
|
||||||
* // patch method actually replace the elm by a new elm, so we need
|
|
||||||
* // to synchronise the pvnode elm with the resulting elm
|
|
||||||
* let nvn = w4._mount(vnode, vn.elm);
|
|
||||||
* pvnode.elm = nvn.elm;
|
|
||||||
* // what follows is only present if there are animations on the widget
|
|
||||||
* utils.transitionInsert(vn, "fade");
|
|
||||||
* },
|
|
||||||
* remove() {
|
|
||||||
* // override with empty function to prevent from removing the node
|
|
||||||
* // directly. It will be removed when destroy is called anyway, which
|
|
||||||
* // delays the removal if there are animations.
|
|
||||||
* },
|
|
||||||
* destroy() {
|
|
||||||
* // if there are animations, we delay the call to destroy on the
|
|
||||||
* // widget, if not, we call it directly.
|
|
||||||
* let finalize = () => {
|
|
||||||
* w4.destroy();
|
|
||||||
* };
|
|
||||||
* utils.transitionRemove(vn, "fade", finalize);
|
|
||||||
* }
|
|
||||||
* };
|
|
||||||
* // the pvnode is inserted at the correct position in the div's children
|
|
||||||
* c1[_2_index] = pvnode;
|
|
||||||
*
|
|
||||||
* // we keep here a reference to the parent vnode (representing the
|
|
||||||
* // widget, so we can reuse it later whenever we update the widget
|
|
||||||
* w4.__owl__.pvnode = pvnode;
|
|
||||||
* });
|
|
||||||
* } else {
|
|
||||||
* // this is the 'update' path of the directive.
|
|
||||||
* // the call to _updateProps is the actual widget update
|
|
||||||
* // Note that we only update the props if we cannot reuse the previous
|
|
||||||
* // rendering work (in the case it was rendered with the same props)
|
|
||||||
* def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
|
||||||
* def3 = def3.then(() => {
|
|
||||||
* // if widget was destroyed in the meantime, we do nothing (so, this
|
|
||||||
* // means that the parent's element children list will have a null in
|
|
||||||
* // the widget's position, which will cause the pvnode to be removed
|
|
||||||
* // when it is patched.
|
|
||||||
* if (w4.__owl__.isDestroyed) {
|
|
||||||
* return;
|
|
||||||
* }
|
|
||||||
* // like above, we register the pvnode to the children list, so it
|
|
||||||
* // will not be patched out of the dom.
|
|
||||||
* let pvnode = w4.__owl__.pvnode;
|
|
||||||
* c1[_2_index] = pvnode;
|
|
||||||
* });
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* // we register the deferred here so the parent can coordinate its patch operation
|
|
||||||
* // with all the children.
|
|
||||||
* extra.promises.push(def3);
|
|
||||||
* return vn1;
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
|
|
||||||
QWeb.addDirective({
|
|
||||||
name: "widget",
|
|
||||||
extraNames: ["props", "keepalive", "asyncroot"],
|
|
||||||
priority: 100,
|
|
||||||
atNodeEncounter({ ctx, value, node, qweb }): boolean {
|
|
||||||
ctx.addLine("//WIDGET");
|
|
||||||
ctx.rootContext.shouldDefineOwner = true;
|
|
||||||
ctx.rootContext.shouldDefineQWeb = true;
|
|
||||||
ctx.rootContext.shouldDefineUtils = true;
|
|
||||||
let keepAlive = node.getAttribute("t-keepalive") ? true : false;
|
|
||||||
let async = node.getAttribute("t-asyncroot") ? true : false;
|
|
||||||
|
|
||||||
// t-on- events and t-transition
|
|
||||||
const events: [string, string[], string, string][] = [];
|
|
||||||
let transition: string = "";
|
|
||||||
const attributes = (<Element>node).attributes;
|
|
||||||
const props: { [key: string]: string } = {};
|
|
||||||
for (let i = 0; i < attributes.length; i++) {
|
|
||||||
const name = attributes[i].name;
|
|
||||||
const value = attributes[i].textContent!;
|
|
||||||
if (name.startsWith("t-on-")) {
|
|
||||||
const [eventName, ...mods] = name.slice(5).split(".");
|
|
||||||
let extraArgs;
|
|
||||||
let handlerName = value.replace(/\(.*\)/, function(args) {
|
|
||||||
extraArgs = args.slice(1, -1);
|
|
||||||
return "";
|
|
||||||
});
|
|
||||||
events.push([eventName, mods, handlerName, extraArgs]);
|
|
||||||
} else if (name === "t-transition") {
|
|
||||||
transition = value;
|
|
||||||
} else if (!name.startsWith("t-")) {
|
|
||||||
if (name !== "class" && name !== "style") {
|
|
||||||
// this is a prop!
|
|
||||||
props[name] = ctx.formatExpression(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let key = node.getAttribute("t-key");
|
|
||||||
if (key) {
|
|
||||||
key = ctx.formatExpression(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
// computing the props string representing the props object
|
|
||||||
let propStr = Object.keys(props)
|
|
||||||
.map(k => k + ":" + props[k])
|
|
||||||
.join(",");
|
|
||||||
let dummyID = ctx.generateID();
|
|
||||||
let defID = ctx.generateID();
|
|
||||||
let widgetID = ctx.generateID();
|
|
||||||
let keyID = key && ctx.generateID();
|
|
||||||
if (key) {
|
|
||||||
// we bind a variable to the key (could be a complex expression, so we
|
|
||||||
// want to evaluate it only once)
|
|
||||||
ctx.addLine(`let key${keyID} = ${key};`);
|
|
||||||
}
|
|
||||||
ctx.addLine(`let def${defID};`);
|
|
||||||
let templateID = key
|
|
||||||
? `key${keyID}`
|
|
||||||
: ctx.inLoop
|
|
||||||
? `String(-${widgetID} - i)`
|
|
||||||
: String(widgetID);
|
|
||||||
|
|
||||||
let ref = node.getAttribute("t-ref");
|
|
||||||
let refExpr = "";
|
|
||||||
let refKey: string = "";
|
|
||||||
if (ref) {
|
|
||||||
refKey = `ref${ctx.generateID()}`;
|
|
||||||
ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`);
|
|
||||||
refExpr = `context.refs[${refKey}] = w${widgetID};`;
|
|
||||||
}
|
|
||||||
let transitionsInsertCode = "";
|
|
||||||
if (transition) {
|
|
||||||
transitionsInsertCode = `utils.transitionInsert(vn, '${transition}');`;
|
|
||||||
}
|
|
||||||
let finalizeWidgetCode = `w${widgetID}.${
|
|
||||||
keepAlive ? "unmount" : "destroy"
|
|
||||||
}();`;
|
|
||||||
if (ref && !keepAlive) {
|
|
||||||
finalizeWidgetCode += `delete context.refs[${refKey}];`;
|
|
||||||
}
|
|
||||||
if (transition) {
|
|
||||||
finalizeWidgetCode = `let finalize = () => {
|
|
||||||
${finalizeWidgetCode}
|
|
||||||
};
|
|
||||||
utils.transitionRemove(vn, '${transition}', finalize);`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let createHook = "";
|
|
||||||
let classAttr = node.getAttribute("class");
|
|
||||||
let tattClass = node.getAttribute("t-att-class");
|
|
||||||
let styleAttr = node.getAttribute("style");
|
|
||||||
let tattStyle = node.getAttribute("t-att-style");
|
|
||||||
if (tattStyle) {
|
|
||||||
const attVar = `_${ctx.generateID()}`;
|
|
||||||
ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattStyle)};`);
|
|
||||||
tattStyle = attVar;
|
|
||||||
}
|
|
||||||
let updateClassCode = "";
|
|
||||||
if (classAttr || tattClass || styleAttr || tattStyle || events.length) {
|
|
||||||
let classCode = "";
|
|
||||||
if (classAttr) {
|
|
||||||
classCode =
|
|
||||||
classAttr
|
|
||||||
.split(" ")
|
|
||||||
.map(c => `vn.elm.classList.add('${c}')`)
|
|
||||||
.join(";") + ";";
|
|
||||||
}
|
|
||||||
if (tattClass) {
|
|
||||||
const attVar = `_${ctx.generateID()}`;
|
|
||||||
ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattClass)};`);
|
|
||||||
classCode = `for (let k in ${attVar}) {
|
|
||||||
if (${attVar}[k]) {
|
|
||||||
vn.elm.classList.add(k);
|
|
||||||
}
|
|
||||||
}`;
|
|
||||||
updateClassCode = `let cl=w${widgetID}.el.classList;for (let k in ${attVar}) {if (${attVar}[k]) {cl.add(k)} else {cl.remove(k)}}`;
|
|
||||||
}
|
|
||||||
let eventsCode = events
|
|
||||||
.map(function([eventName, mods, handlerName, extraArgs]) {
|
|
||||||
let params = extraArgs
|
|
||||||
? `owner, ${ctx.formatExpression(extraArgs)}`
|
|
||||||
: "owner";
|
|
||||||
let handler;
|
|
||||||
if (mods.length > 0) {
|
|
||||||
handler = `function (e) {`;
|
|
||||||
handler += mods
|
|
||||||
.map(function(mod) {
|
|
||||||
return T_WIDGET_MODS_CODE[mod];
|
|
||||||
})
|
|
||||||
.join("");
|
|
||||||
handler += `owner['${handlerName}'].call(${params}, e);}`;
|
|
||||||
} else {
|
|
||||||
handler = `owner['${handlerName}'].bind(${params})`;
|
|
||||||
}
|
|
||||||
return `vn.elm.addEventListener('${eventName}', ${handler});`;
|
|
||||||
})
|
|
||||||
.join("");
|
|
||||||
const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false);
|
|
||||||
const styleCode = styleExpr ? `vn.elm.style = ${styleExpr};` : "";
|
|
||||||
createHook = `vnode.data.hook = {create(_, vn){${classCode}${styleCode}${eventsCode}}};`;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.addLine(
|
|
||||||
`let w${widgetID} = ${templateID} in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[${templateID}]] : false;`
|
|
||||||
);
|
|
||||||
ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`);
|
|
||||||
if (async) {
|
|
||||||
ctx.addLine(`const patchQueue${widgetID} = [];`);
|
|
||||||
ctx.addLine(
|
|
||||||
`c${
|
|
||||||
ctx.parentNode
|
|
||||||
}.push(w${widgetID} && w${widgetID}.__owl__.pvnode || null);`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
ctx.addLine(`c${ctx.parentNode}.push(null);`);
|
|
||||||
}
|
|
||||||
ctx.addLine(`let props${widgetID} = {${propStr}};`);
|
|
||||||
ctx.addIf(
|
|
||||||
`w${widgetID} && w${widgetID}.__owl__.renderPromise && !w${widgetID}.__owl__.vnode`
|
|
||||||
);
|
|
||||||
ctx.addIf(
|
|
||||||
`utils.shallowEqual(props${widgetID}, w${widgetID}.__owl__.renderProps)`
|
|
||||||
);
|
|
||||||
ctx.addLine(`def${defID} = w${widgetID}.__owl__.renderPromise;`);
|
|
||||||
ctx.addElse();
|
|
||||||
ctx.addLine(`w${widgetID}.destroy();`);
|
|
||||||
ctx.addLine(`w${widgetID} = false;`);
|
|
||||||
ctx.closeIf();
|
|
||||||
ctx.closeIf();
|
|
||||||
|
|
||||||
ctx.addIf(`!w${widgetID}`);
|
|
||||||
// new widget
|
|
||||||
ctx.addLine(`let widgetKey${widgetID} = ${ctx.interpolate(value)};`);
|
|
||||||
ctx.addLine(
|
|
||||||
`let W${widgetID} = context.widgets && context.widgets[widgetKey${widgetID}] || QWeb.widgets[widgetKey${widgetID}];`
|
|
||||||
);
|
|
||||||
|
|
||||||
// maybe only do this in dev mode...
|
|
||||||
ctx.addLine(
|
|
||||||
`if (!W${widgetID}) {throw new Error('Cannot find the definition of widget "' + widgetKey${widgetID} + '"')}`
|
|
||||||
);
|
|
||||||
ctx.addLine(`w${widgetID} = new W${widgetID}(owner, props${widgetID});`);
|
|
||||||
ctx.addLine(
|
|
||||||
`context.__owl__.cmap[${templateID}] = w${widgetID}.__owl__.id;`
|
|
||||||
);
|
|
||||||
|
|
||||||
// SLOTS
|
|
||||||
if (node.childElementCount) {
|
|
||||||
const clone = <Element>node.cloneNode(true);
|
|
||||||
const slotNodes = clone.querySelectorAll("[t-set]");
|
|
||||||
const slotId = qweb.nextSlotId++;
|
|
||||||
ctx.addLine(`w${widgetID}.__owl__.slotId = ${slotId};`);
|
|
||||||
if (slotNodes.length) {
|
|
||||||
for (let i = 0, length = slotNodes.length; i < length; i++) {
|
|
||||||
const slotNode = slotNodes[i];
|
|
||||||
slotNode.parentElement!.removeChild(slotNode);
|
|
||||||
const key = slotNode.getAttribute("t-set")!;
|
|
||||||
slotNode.removeAttribute("t-set");
|
|
||||||
const slotFn = qweb._compile(`slot_${key}_template`, slotNode);
|
|
||||||
qweb.slots[`${slotId}_${key}`] = slotFn.bind(qweb);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (clone.childElementCount) {
|
|
||||||
const content = clone.children[0];
|
|
||||||
const slotFn = qweb._compile(`slot_default_template`, content);
|
|
||||||
qweb.slots[`${slotId}_default`] = slotFn.bind(qweb);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.addLine(`def${defID} = w${widgetID}._prepare();`);
|
|
||||||
// hack: specify empty remove hook to prevent the node from being removed from the DOM
|
|
||||||
ctx.addLine(
|
|
||||||
`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${widgetID}._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeWidgetCode}}}});c${
|
|
||||||
ctx.parentNode
|
|
||||||
}[_${dummyID}_index]=pvnode;w${widgetID}.__owl__.pvnode = pvnode;});`
|
|
||||||
);
|
|
||||||
|
|
||||||
ctx.addElse();
|
|
||||||
// need to update widget
|
|
||||||
const patchQueueCode = async ? `patchQueue${widgetID}` : "extra.patchQueue";
|
|
||||||
ctx.addLine(
|
|
||||||
`def${defID} = def${defID} || w${widgetID}._updateProps(props${widgetID}, extra.forceUpdate, ${patchQueueCode});`
|
|
||||||
);
|
|
||||||
let keepAliveCode = "";
|
|
||||||
if (keepAlive) {
|
|
||||||
keepAliveCode = `pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w${widgetID}.el,vn.elm);vn.elm=w${widgetID}.el;w${widgetID}._remount();};`;
|
|
||||||
}
|
|
||||||
ctx.addLine(
|
|
||||||
`def${defID} = def${defID}.then(()=>{if (w${widgetID}.__owl__.isDestroyed) {return};${
|
|
||||||
tattStyle ? `w${widgetID}.el.style=${tattStyle};` : ""
|
|
||||||
}${updateClassCode}let pvnode=w${widgetID}.__owl__.pvnode;${keepAliveCode}c${
|
|
||||||
ctx.parentNode
|
|
||||||
}[_${dummyID}_index]=pvnode;});`
|
|
||||||
);
|
|
||||||
ctx.closeIf();
|
|
||||||
|
|
||||||
if (async) {
|
|
||||||
ctx.addLine(
|
|
||||||
`def${defID}.then(w${widgetID}._applyPatchQueue.bind(w${widgetID}, patchQueue${widgetID}));`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
ctx.addLine(`extra.promises.push(def${defID});`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
node.hasAttribute("t-if") ||
|
|
||||||
node.hasAttribute("t-else") ||
|
|
||||||
node.hasAttribute("t-elif")
|
|
||||||
) {
|
|
||||||
ctx.closeIf();
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// 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
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
QWeb.addDirective({
|
|
||||||
name: "slot",
|
|
||||||
priority: 80,
|
|
||||||
atNodeEncounter({ ctx, value }): boolean {
|
|
||||||
const slotKey = ctx.generateID();
|
|
||||||
ctx.addLine(
|
|
||||||
`const slot${slotKey} = this.slots[context.__owl__.slotId + '_' + '${value}'];`
|
|
||||||
);
|
|
||||||
ctx.addLine(
|
|
||||||
`c${ctx.parentNode}.push(slot${slotKey}(context.__owl__.parent, extra));`
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// t-model
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
UTILS.toNumber = function(val: string): number | string {
|
|
||||||
const n = parseFloat(val);
|
|
||||||
return isNaN(n) ? val : n;
|
|
||||||
};
|
|
||||||
|
|
||||||
QWeb.addDirective({
|
|
||||||
name: "model",
|
|
||||||
priority: 42,
|
|
||||||
atNodeCreation({ ctx, nodeID, value, node, fullName }) {
|
|
||||||
const type = node.getAttribute("type");
|
|
||||||
let handler;
|
|
||||||
let event = fullName.includes(".lazy") ? "change" : "input";
|
|
||||||
if (node.tagName === "select") {
|
|
||||||
ctx.addLine(`p${nodeID}.props = {value: context.state['${value}']};`);
|
|
||||||
event = "change";
|
|
||||||
handler = `(ev) => {context.state['${value}'] = ev.target.value}`;
|
|
||||||
} else if (type === "checkbox") {
|
|
||||||
ctx.addLine(`p${nodeID}.props = {checked: context.state['${value}']};`);
|
|
||||||
handler = `(ev) => {context.state['${value}'] = ev.target.checked}`;
|
|
||||||
} else if (type === "radio") {
|
|
||||||
const nodeValue = node.getAttribute("value")!;
|
|
||||||
ctx.addLine(
|
|
||||||
`p${nodeID}.props = {checked:context.state['${value}'] === '${nodeValue}'};`
|
|
||||||
);
|
|
||||||
handler = `(ev) => {context.state['${value}'] = ev.target.value}`;
|
|
||||||
event = "click";
|
|
||||||
} else {
|
|
||||||
ctx.addLine(`p${nodeID}.props = {value: context.state['${value}']};`);
|
|
||||||
const trimCode = fullName.includes(".trim") ? ".trim()" : "";
|
|
||||||
let valueCode = `ev.target.value${trimCode}`;
|
|
||||||
if (fullName.includes(".number")) {
|
|
||||||
ctx.rootContext.shouldDefineUtils = true;
|
|
||||||
valueCode = `utils.toNumber(${valueCode})`;
|
|
||||||
}
|
|
||||||
handler = `(ev) => {context.state['${value}'] = ${valueCode}}`;
|
|
||||||
}
|
|
||||||
ctx.addLine(
|
|
||||||
`extra.handlers['${event}' + ${nodeID}] = extra.handlers['${event}' + ${nodeID}] || (${handler});`
|
|
||||||
);
|
|
||||||
ctx.addLine(
|
|
||||||
`p${nodeID}.on['${event}'] = extra.handlers['${event}' + ${nodeID}];`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { Component } from "../component/component";
|
||||||
|
import { Destination, RouterEnv } from "./Router";
|
||||||
|
|
||||||
|
export const LINK_TEMPLATE_NAME = "__owl__-router-link";
|
||||||
|
export const LINK_TEMPLATE = `
|
||||||
|
<a t-att-class="{'router-link-active': isActive }"
|
||||||
|
t-att-href="href"
|
||||||
|
t-on-click="navigate">
|
||||||
|
<t t-slot="default"/>
|
||||||
|
</a>`;
|
||||||
|
|
||||||
|
type Props = Destination;
|
||||||
|
|
||||||
|
export class Link<Env extends RouterEnv> extends Component<Env, Props, {}> {
|
||||||
|
template = LINK_TEMPLATE_NAME;
|
||||||
|
href: string = this.env.router.destToPath(this.props);
|
||||||
|
|
||||||
|
async willUpdateProps(nextProps) {
|
||||||
|
this.href = this.env.router.destToPath(nextProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
get isActive() {
|
||||||
|
if (this.env.router.mode === "hash") {
|
||||||
|
return (<any>document.location).hash === this.href;
|
||||||
|
}
|
||||||
|
return (<any>document.location).pathname === this.href;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate(ev) {
|
||||||
|
// don't redirect with control keys
|
||||||
|
if (ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// don't redirect on right click
|
||||||
|
if (ev.button !== undefined && ev.button !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// don't redirect if `target="_blank"`
|
||||||
|
if (ev.currentTarget && ev.currentTarget.getAttribute) {
|
||||||
|
const target = ev.currentTarget.getAttribute("target");
|
||||||
|
if (/\b_blank\b/i.test(target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ev.preventDefault();
|
||||||
|
this.env.router.navigate(this.props);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
import { Env } from "../component/component";
|
||||||
|
import { QWeb } from "../qweb/index";
|
||||||
|
import { makeDirective } from "./directive";
|
||||||
|
import { LINK_TEMPLATE, LINK_TEMPLATE_NAME } from "./Link";
|
||||||
|
|
||||||
|
type NavigationGuard = (info: {
|
||||||
|
env: Env;
|
||||||
|
to: Route | null;
|
||||||
|
from: Route | null;
|
||||||
|
}) => boolean | Destination;
|
||||||
|
|
||||||
|
export interface Route {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
component?: any;
|
||||||
|
redirect?: Destination;
|
||||||
|
params: string[];
|
||||||
|
beforeRouteEnter?: NavigationGuard;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RouteParams = { [key: string]: string | number };
|
||||||
|
|
||||||
|
export interface RouterEnv extends Env {
|
||||||
|
router: Router;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Destination {
|
||||||
|
path?: string;
|
||||||
|
to?: string;
|
||||||
|
params?: RouteParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PositiveMatchResult {
|
||||||
|
type: "match";
|
||||||
|
route: Route;
|
||||||
|
params: RouteParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NegativeMatchResult {
|
||||||
|
type: "nomatch";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CancelledMatch {
|
||||||
|
type: "cancelled";
|
||||||
|
}
|
||||||
|
|
||||||
|
type MatchResult = PositiveMatchResult | NegativeMatchResult | CancelledMatch;
|
||||||
|
|
||||||
|
interface Options {
|
||||||
|
mode: Router["mode"];
|
||||||
|
}
|
||||||
|
|
||||||
|
const paramRegexp = /\{\{(.*?)\}\}/;
|
||||||
|
|
||||||
|
export class Router {
|
||||||
|
currentRoute: Route | null = null;
|
||||||
|
currentParams: RouteParams | null = null;
|
||||||
|
mode: "history" | "hash";
|
||||||
|
|
||||||
|
routes: { [id: string]: Route };
|
||||||
|
routeIds: string[];
|
||||||
|
env: RouterEnv;
|
||||||
|
|
||||||
|
constructor(env: Env, routes: Partial<Route>[], options: Options = { mode: "history" }) {
|
||||||
|
env.router = this;
|
||||||
|
this.mode = options.mode;
|
||||||
|
this.env = env as RouterEnv;
|
||||||
|
|
||||||
|
this.routes = {};
|
||||||
|
this.routeIds = [];
|
||||||
|
let nextId = 1;
|
||||||
|
for (let partialRoute of routes) {
|
||||||
|
if (!partialRoute.name) {
|
||||||
|
partialRoute.name = "__route__" + nextId++;
|
||||||
|
}
|
||||||
|
if (partialRoute.component) {
|
||||||
|
QWeb.register("__component__" + partialRoute.name, partialRoute.component);
|
||||||
|
}
|
||||||
|
if (partialRoute.redirect) {
|
||||||
|
this.validateDestination(partialRoute.redirect);
|
||||||
|
}
|
||||||
|
partialRoute.params = partialRoute.path ? findParams(partialRoute.path) : [];
|
||||||
|
this.routes[partialRoute.name] = partialRoute as Route;
|
||||||
|
this.routeIds.push(partialRoute.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
(this as any)._listener = () => this.matchAndApplyRules(this.currentPath());
|
||||||
|
window.addEventListener("popstate", (this as any)._listener);
|
||||||
|
|
||||||
|
// setup link and directive
|
||||||
|
env.qweb.addTemplate(LINK_TEMPLATE_NAME, LINK_TEMPLATE);
|
||||||
|
QWeb.addDirective(makeDirective(<RouterEnv>env));
|
||||||
|
}
|
||||||
|
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
// Public API
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
const result = await this.matchAndApplyRules(this.currentPath());
|
||||||
|
if (result.type === "match") {
|
||||||
|
this.currentRoute = result.route;
|
||||||
|
this.currentParams = result.params;
|
||||||
|
const currentPath = this.routeToPath(result.route, result.params);
|
||||||
|
if (currentPath !== this.currentPath()) {
|
||||||
|
this.setUrlFromPath(currentPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async navigate(to: Destination): Promise<boolean> {
|
||||||
|
const path = this.destToPath(to);
|
||||||
|
const initialName = this.currentRouteName;
|
||||||
|
const result = await this.matchAndApplyRules(path);
|
||||||
|
if (result.type === "match") {
|
||||||
|
const finalPath = this.routeToPath(result.route, result.params);
|
||||||
|
this.setUrlFromPath(finalPath);
|
||||||
|
this.currentRoute = result.route;
|
||||||
|
this.currentParams = result.params;
|
||||||
|
} else if (result.type === "nomatch") {
|
||||||
|
this.currentRoute = null;
|
||||||
|
this.currentParams = null;
|
||||||
|
}
|
||||||
|
if (this.currentRouteName !== initialName) {
|
||||||
|
this.env.qweb.forceUpdate();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
destToPath(dest: Destination): string {
|
||||||
|
this.validateDestination(dest);
|
||||||
|
return dest.path || this.routeToPath(this.routes[dest.to!], dest.params!);
|
||||||
|
}
|
||||||
|
|
||||||
|
get currentRouteName(): string | null {
|
||||||
|
return this.currentRoute && this.currentRoute.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
// Private helpers
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
|
private setUrlFromPath(path: string) {
|
||||||
|
const url = location.origin + path;
|
||||||
|
window.history.pushState({}, path, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
private validateDestination(dest: Destination) {
|
||||||
|
if ((!dest.path && !dest.to) || (dest.path && dest.to)) {
|
||||||
|
throw new Error(`Invalid destination: ${JSON.stringify(dest)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private routeToPath(route: Route, params: RouteParams): string {
|
||||||
|
const path = route.path;
|
||||||
|
const parts = path.split("/");
|
||||||
|
const l = parts.length;
|
||||||
|
for (let i = 0; i < l; i++) {
|
||||||
|
const part = parts[i];
|
||||||
|
const match = part.match(paramRegexp);
|
||||||
|
if (match) {
|
||||||
|
const key = match[1].split(".")[0];
|
||||||
|
parts[i] = <string>params[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parts.join("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
private currentPath(): string {
|
||||||
|
let result = this.mode === "history" ? window.location.pathname : window.location.hash.slice(1);
|
||||||
|
return result || "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
private match(path: string): MatchResult {
|
||||||
|
for (let routeId of this.routeIds) {
|
||||||
|
let route = this.routes[routeId];
|
||||||
|
let params = this.getRouteParams(route, path);
|
||||||
|
if (params) {
|
||||||
|
return {
|
||||||
|
type: "match",
|
||||||
|
route: route,
|
||||||
|
params: params
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { type: "nomatch" };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async matchAndApplyRules(path: string): Promise<MatchResult> {
|
||||||
|
const result = this.match(path);
|
||||||
|
if (result.type === "match") {
|
||||||
|
return this.applyRules(result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async applyRules(matchResult: PositiveMatchResult): Promise<MatchResult> {
|
||||||
|
const route = matchResult.route;
|
||||||
|
if (route.redirect) {
|
||||||
|
const path = this.destToPath(route.redirect);
|
||||||
|
return this.matchAndApplyRules(path);
|
||||||
|
}
|
||||||
|
if (route.beforeRouteEnter) {
|
||||||
|
const result = await route.beforeRouteEnter({
|
||||||
|
env: this.env,
|
||||||
|
from: this.currentRoute,
|
||||||
|
to: route
|
||||||
|
});
|
||||||
|
if (result === false) {
|
||||||
|
return { type: "cancelled" };
|
||||||
|
} else if (result !== true) {
|
||||||
|
// we want to navigate to another destination
|
||||||
|
const path = this.destToPath(result);
|
||||||
|
return this.matchAndApplyRules(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getRouteParams(route: Route, path: string): RouteParams | false {
|
||||||
|
if (route.path === "*") {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const descrParts = route.path.split("/");
|
||||||
|
const targetParts = path.split("/");
|
||||||
|
const l = descrParts.length;
|
||||||
|
if (l !== targetParts.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const result = {};
|
||||||
|
for (let i = 0; i < l; i++) {
|
||||||
|
const descr = descrParts[i];
|
||||||
|
let target: string | number = targetParts[i];
|
||||||
|
const match = descr.match(paramRegexp);
|
||||||
|
if (match) {
|
||||||
|
const [key, suffix] = match[1].split(".");
|
||||||
|
if (suffix === "number") {
|
||||||
|
target = parseInt(target, 10);
|
||||||
|
}
|
||||||
|
result[key] = target;
|
||||||
|
} else if (descr !== target) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findParams(str: string): string[] {
|
||||||
|
const globalParamRegexp = /\{\{(.*?)\}\}/g;
|
||||||
|
const result: string[] = [];
|
||||||
|
let m;
|
||||||
|
do {
|
||||||
|
m = globalParamRegexp.exec(str);
|
||||||
|
if (m) {
|
||||||
|
result.push(m[1].split(".")[0]);
|
||||||
|
}
|
||||||
|
} while (m);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { RouterEnv } from "./Router";
|
||||||
|
|
||||||
|
export function makeDirective(env: RouterEnv) {
|
||||||
|
return {
|
||||||
|
name: "routecomponent",
|
||||||
|
priority: 13,
|
||||||
|
atNodeEncounter({ node }): boolean {
|
||||||
|
let first = true;
|
||||||
|
const router = env.router;
|
||||||
|
for (let name of router.routeIds) {
|
||||||
|
const route = router.routes[name];
|
||||||
|
if (route.component) {
|
||||||
|
// make new t t-component element
|
||||||
|
const comp = node.ownerDocument.createElement("t");
|
||||||
|
comp.setAttribute("t-component", "__component__" + route.name);
|
||||||
|
comp.setAttribute(first ? "t-if" : "t-elif", `env.router.currentRouteName === '${route.name}'`);
|
||||||
|
first = false;
|
||||||
|
for (let param of route.params) {
|
||||||
|
comp.setAttribute(param, `env.router.currentParams.${param}`);
|
||||||
|
}
|
||||||
|
node.appendChild(comp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node.removeAttribute("t-routecomponent");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
-298
@@ -1,298 +0,0 @@
|
|||||||
import { Component, Env } from "./component";
|
|
||||||
import { EventBus } from "./event_bus";
|
|
||||||
import { Observer } from "./observer";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Owl Store
|
|
||||||
*
|
|
||||||
* We have here:
|
|
||||||
* - a Store class
|
|
||||||
* - a connect function
|
|
||||||
*
|
|
||||||
* The Owl store is our answer to the problem of managing complex state across
|
|
||||||
* components. The main idea is that the store owns some state, allow external
|
|
||||||
* code to modify it through actions/mutations, and for each state changes,
|
|
||||||
* connected component will be notified, and updated if necessary.
|
|
||||||
*
|
|
||||||
* Note that this code is partly inspired by VueX and React/Redux
|
|
||||||
*/
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Store Definition
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
type Mutation = ({ state, commit, getters }, payload: any) => void;
|
|
||||||
type Action = ({ commit, state, dispatch, env, getters }, payload: any) => void;
|
|
||||||
type Getter = ({ state, getters }, payload) => any;
|
|
||||||
|
|
||||||
interface StoreConfig {
|
|
||||||
env?: Env;
|
|
||||||
state?: any;
|
|
||||||
actions?: { [name: string]: Action };
|
|
||||||
getters?: { [name: string]: Getter };
|
|
||||||
mutations?: { [name: string]: Mutation };
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StoreOption {
|
|
||||||
debug?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Store extends EventBus {
|
|
||||||
state: any;
|
|
||||||
actions: any;
|
|
||||||
mutations: any;
|
|
||||||
_commitLevel: number = 0;
|
|
||||||
history: any[] = [];
|
|
||||||
debug: boolean;
|
|
||||||
env: any;
|
|
||||||
observer: Observer;
|
|
||||||
getters: { [name: string]: (payload?) => any };
|
|
||||||
_gettersCache: { [name: string]: {} };
|
|
||||||
|
|
||||||
constructor(config: StoreConfig, options: StoreOption = {}) {
|
|
||||||
super();
|
|
||||||
this.debug = options.debug || false;
|
|
||||||
this.state = config.state || {};
|
|
||||||
this.actions = config.actions;
|
|
||||||
this.mutations = config.mutations;
|
|
||||||
this.env = config.env;
|
|
||||||
this.observer = new Observer();
|
|
||||||
this.observer.notifyCB = () => {
|
|
||||||
this._gettersCache = {};
|
|
||||||
this.trigger("update");
|
|
||||||
};
|
|
||||||
this.observer.allowMutations = false;
|
|
||||||
this.observer.observe(this.state);
|
|
||||||
this.getters = {};
|
|
||||||
this._gettersCache = {};
|
|
||||||
|
|
||||||
if (this.debug) {
|
|
||||||
this.history.push({ state: this.state });
|
|
||||||
}
|
|
||||||
|
|
||||||
const cTypes = ["undefined", "number", "string"];
|
|
||||||
for (let entry of Object.entries(config.getters || {})) {
|
|
||||||
const name: string = entry[0];
|
|
||||||
const func: (...any) => any = entry[1];
|
|
||||||
this.getters[name] = payload => {
|
|
||||||
if (this._commitLevel === 0 && cTypes.indexOf(typeof payload) >= 0) {
|
|
||||||
this._gettersCache[name] = this._gettersCache[name] || {};
|
|
||||||
this._gettersCache[name][payload] =
|
|
||||||
this._gettersCache[name][payload] ||
|
|
||||||
func({ state: this.state, getters: this.getters }, payload);
|
|
||||||
return this._gettersCache[name][payload];
|
|
||||||
}
|
|
||||||
return func({ state: this.state, getters: this.getters }, payload);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(action: string, payload?: any): Promise<void> | void {
|
|
||||||
if (!this.actions[action]) {
|
|
||||||
throw new Error(`[Error] action ${action} is undefined`);
|
|
||||||
}
|
|
||||||
const result = this.actions[action](
|
|
||||||
{
|
|
||||||
commit: this.commit.bind(this),
|
|
||||||
dispatch: this.dispatch.bind(this),
|
|
||||||
env: this.env,
|
|
||||||
state: this.state,
|
|
||||||
getters: this.getters
|
|
||||||
},
|
|
||||||
payload
|
|
||||||
);
|
|
||||||
if (result instanceof Promise) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
result.then(() => resolve());
|
|
||||||
result.catch(reject);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
commit(type: string, payload?: any): any {
|
|
||||||
if (!this.mutations[type]) {
|
|
||||||
throw new Error(`[Error] mutation ${type} is undefined`);
|
|
||||||
}
|
|
||||||
this._commitLevel++;
|
|
||||||
this.observer.allowMutations = true;
|
|
||||||
|
|
||||||
const res = this.mutations[type].call(
|
|
||||||
null,
|
|
||||||
{
|
|
||||||
commit: this.commit.bind(this),
|
|
||||||
state: this.state,
|
|
||||||
getters: this.getters
|
|
||||||
},
|
|
||||||
payload
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this._commitLevel === 1) {
|
|
||||||
this.observer.allowMutations = false;
|
|
||||||
if (this.debug) {
|
|
||||||
this.history.push({
|
|
||||||
state: this.state,
|
|
||||||
mutation: type,
|
|
||||||
payload: payload
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this._commitLevel--;
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Connect function
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
function revNumber<T extends Object>(o: T): number {
|
|
||||||
if (o !== null && typeof o === "object" && (<any>o).__owl__) {
|
|
||||||
return (<any>o).__owl__.rev;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function deepRevNumber<T extends Object>(o: T): number {
|
|
||||||
if (o !== null && typeof o === "object" && (<any>o).__owl__) {
|
|
||||||
return (<any>o).__owl__.deepRev;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
type Constructor<T> = new (...args: any[]) => T;
|
|
||||||
interface EnvWithStore extends Env {
|
|
||||||
store: Store;
|
|
||||||
}
|
|
||||||
type HashFunction = (a: any, b: any) => number;
|
|
||||||
interface StoreOptions {
|
|
||||||
getStore?(Env): Store;
|
|
||||||
hashFunction?: HashFunction;
|
|
||||||
deep?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
let nextID = 1;
|
|
||||||
|
|
||||||
export function connect<E extends EnvWithStore, P, S>(
|
|
||||||
Comp: Constructor<Component<E, P, S>>,
|
|
||||||
mapStoreToProps,
|
|
||||||
options: StoreOptions = <StoreOptions>{}
|
|
||||||
) {
|
|
||||||
let hashFunction = options.hashFunction || null;
|
|
||||||
const getStore = options.getStore || (env => env.store);
|
|
||||||
|
|
||||||
if (!hashFunction) {
|
|
||||||
let deep = "deep" in options ? options.deep : true;
|
|
||||||
let defaultRevFunction = deep ? deepRevNumber : revNumber;
|
|
||||||
hashFunction = function({ storeProps }, options) {
|
|
||||||
const { currentStoreProps } = options;
|
|
||||||
if ("__owl__" in storeProps) {
|
|
||||||
return defaultRevFunction(storeProps);
|
|
||||||
}
|
|
||||||
let hash = 0;
|
|
||||||
for (let key in storeProps) {
|
|
||||||
const val = storeProps[key];
|
|
||||||
const hashVal = defaultRevFunction(val);
|
|
||||||
if (hashVal === 0) {
|
|
||||||
if (val !== currentStoreProps[key]) {
|
|
||||||
options.didChange = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
hash += hashVal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return hash;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const Result = class extends Comp {
|
|
||||||
constructor(parent, props?: any) {
|
|
||||||
const env = parent instanceof Component ? parent.env : parent;
|
|
||||||
const store = getStore(env);
|
|
||||||
const ownProps = Object.assign({}, props || {});
|
|
||||||
const storeProps = mapStoreToProps(store.state, ownProps, store.getters);
|
|
||||||
const mergedProps = Object.assign({}, props || {}, storeProps);
|
|
||||||
super(parent, mergedProps);
|
|
||||||
(<any>this.__owl__).ownProps = ownProps;
|
|
||||||
(<any>this.__owl__).currentStoreProps = storeProps;
|
|
||||||
(<any>this.__owl__).store = store;
|
|
||||||
(<any>this.__owl__).storeHash = (<HashFunction>hashFunction)(
|
|
||||||
{
|
|
||||||
state: store.state,
|
|
||||||
storeProps: storeProps,
|
|
||||||
revNumber,
|
|
||||||
deepRevNumber
|
|
||||||
},
|
|
||||||
{
|
|
||||||
currentStoreProps: storeProps
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* We do not use the mounted hook here for a subtle reason: we want the
|
|
||||||
* updates to be called for the parents before the children. However,
|
|
||||||
* if we use the mounted hook, this will be done in the reverse order.
|
|
||||||
*/
|
|
||||||
_callMounted() {
|
|
||||||
(<any>this.__owl__).store.on("update", this, this._checkUpdate);
|
|
||||||
super._callMounted();
|
|
||||||
}
|
|
||||||
willUnmount() {
|
|
||||||
(<any>this.__owl__).store.off("update", this);
|
|
||||||
super.willUnmount();
|
|
||||||
}
|
|
||||||
|
|
||||||
_checkUpdate() {
|
|
||||||
const ownProps = (<any>this.__owl__).ownProps;
|
|
||||||
const storeProps = mapStoreToProps(
|
|
||||||
(<any>this.__owl__).store.state,
|
|
||||||
ownProps,
|
|
||||||
(<any>this.__owl__).store.getters
|
|
||||||
);
|
|
||||||
const options: any = {
|
|
||||||
currentStoreProps: (<any>this.__owl__).currentStoreProps
|
|
||||||
};
|
|
||||||
const storeHash = (<HashFunction>hashFunction)(
|
|
||||||
{
|
|
||||||
state: (<any>this.__owl__).store.state,
|
|
||||||
storeProps: storeProps,
|
|
||||||
revNumber,
|
|
||||||
deepRevNumber
|
|
||||||
},
|
|
||||||
options
|
|
||||||
);
|
|
||||||
let didChange = options.didChange;
|
|
||||||
if (storeHash !== (<any>this.__owl__).storeHash) {
|
|
||||||
didChange = true;
|
|
||||||
(<any>this.__owl__).storeHash = storeHash;
|
|
||||||
}
|
|
||||||
if (didChange) {
|
|
||||||
(<any>this.__owl__).currentStoreProps = storeProps;
|
|
||||||
this._updateProps(ownProps, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_updateProps(nextProps, forceUpdate, patchQueue?: any[]) {
|
|
||||||
if ((<any>this.__owl__).ownProps !== nextProps) {
|
|
||||||
(<any>this.__owl__).currentStoreProps = mapStoreToProps(
|
|
||||||
(<any>this.__owl__).store.state,
|
|
||||||
nextProps,
|
|
||||||
(<any>this.__owl__).store.getters
|
|
||||||
);
|
|
||||||
}
|
|
||||||
(<any>this.__owl__).ownProps = nextProps;
|
|
||||||
const mergedProps = Object.assign(
|
|
||||||
{},
|
|
||||||
nextProps,
|
|
||||||
(<any>this.__owl__).currentStoreProps
|
|
||||||
);
|
|
||||||
return super._updateProps(mergedProps, forceUpdate, patchQueue);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// we assign here a unique name to the resulting anonymous class.
|
|
||||||
// this is necessary for Owl to be able to properly deduce templates.
|
|
||||||
// Otherwise, all connected components would have the same name, and then
|
|
||||||
// each component after the first will necessarily have the same template.
|
|
||||||
let name = `ConnectedComponent${nextID++}`;
|
|
||||||
Object.defineProperty(Result, "name", { value: name });
|
|
||||||
return Result;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { Component, Env } from "../component/component";
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Connect function
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type HashFunction = (a: any, b: any) => number;
|
||||||
|
|
||||||
|
export class ConnectedComponent<T extends Env, P, S> extends Component<T, P, S> {
|
||||||
|
deep: boolean = true;
|
||||||
|
getStore(env) {
|
||||||
|
return env.store;
|
||||||
|
}
|
||||||
|
|
||||||
|
hashFunction: HashFunction = ({ storeProps }, options) => {
|
||||||
|
const observer = (this.__owl__ as any).store.observer;
|
||||||
|
let refFunction = this.deep ? observer.deepRevNumber : observer.revNumber;
|
||||||
|
if ("__owl__" in storeProps) {
|
||||||
|
return refFunction.call(observer, storeProps);
|
||||||
|
}
|
||||||
|
const { currentStoreProps } = options;
|
||||||
|
let hash = 0;
|
||||||
|
for (let key in storeProps) {
|
||||||
|
const val = storeProps[key];
|
||||||
|
const hashVal = refFunction.call(observer, val);
|
||||||
|
if (hashVal === 0) {
|
||||||
|
if (val !== currentStoreProps[key]) {
|
||||||
|
options.didChange = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hash += hashVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
};
|
||||||
|
|
||||||
|
static mapStoreToProps(storeState, ownProps, getters) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
constructor(parent, props?: any) {
|
||||||
|
super(parent, props);
|
||||||
|
|
||||||
|
const store = this.getStore(this.env);
|
||||||
|
const ownProps = this.props || {};
|
||||||
|
const storeProps = (<any>this.constructor).mapStoreToProps(
|
||||||
|
store.state,
|
||||||
|
ownProps,
|
||||||
|
store.getters
|
||||||
|
);
|
||||||
|
const mergedProps = Object.assign({}, ownProps, storeProps);
|
||||||
|
|
||||||
|
this.props = mergedProps;
|
||||||
|
|
||||||
|
(<any>this.__owl__).ownProps = ownProps;
|
||||||
|
(<any>this.__owl__).currentStoreProps = storeProps;
|
||||||
|
(<any>this.__owl__).store = store;
|
||||||
|
(<any>this.__owl__).storeHash = this.hashFunction(
|
||||||
|
{
|
||||||
|
state: store.state,
|
||||||
|
storeProps: storeProps
|
||||||
|
},
|
||||||
|
{
|
||||||
|
currentStoreProps: storeProps
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* We do not use the mounted hook here for a subtle reason: we want the
|
||||||
|
* updates to be called for the parents before the children. However,
|
||||||
|
* if we use the mounted hook, this will be done in the reverse order.
|
||||||
|
*/
|
||||||
|
__callMounted() {
|
||||||
|
(<any>this.__owl__).store.on("update", this, this.__checkUpdate);
|
||||||
|
super.__callMounted();
|
||||||
|
}
|
||||||
|
willUnmount() {
|
||||||
|
(<any>this.__owl__).store.off("update", this);
|
||||||
|
super.willUnmount();
|
||||||
|
}
|
||||||
|
|
||||||
|
async __checkUpdate(updateId) {
|
||||||
|
if (updateId === (<any>this.__owl__).currentUpdateId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ownProps = (<any>this.__owl__).ownProps;
|
||||||
|
const storeProps = (<any>this.constructor).mapStoreToProps(
|
||||||
|
(<any>this.__owl__).store.state,
|
||||||
|
ownProps,
|
||||||
|
(<any>this.__owl__).store.getters
|
||||||
|
);
|
||||||
|
const options: any = {
|
||||||
|
currentStoreProps: (<any>this.__owl__).currentStoreProps
|
||||||
|
};
|
||||||
|
const storeHash = this.hashFunction(
|
||||||
|
{
|
||||||
|
state: (<any>this.__owl__).store.state,
|
||||||
|
storeProps: storeProps
|
||||||
|
},
|
||||||
|
options
|
||||||
|
);
|
||||||
|
let didChange = options.didChange;
|
||||||
|
if (storeHash !== (<any>this.__owl__).storeHash) {
|
||||||
|
didChange = true;
|
||||||
|
(<any>this.__owl__).storeHash = storeHash;
|
||||||
|
}
|
||||||
|
if (didChange) {
|
||||||
|
(<any>this.__owl__).currentStoreProps = storeProps;
|
||||||
|
await this.__updateProps(ownProps, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
__updateProps(nextProps, forceUpdate, patchQueue?: any[]) {
|
||||||
|
const __owl__ = <any>this.__owl__;
|
||||||
|
__owl__.currentUpdateId = __owl__.store._updateId;
|
||||||
|
if (__owl__.ownProps !== nextProps) {
|
||||||
|
__owl__.currentStoreProps = (<any>this.constructor).mapStoreToProps(
|
||||||
|
__owl__.store.state,
|
||||||
|
nextProps,
|
||||||
|
__owl__.store.getters
|
||||||
|
);
|
||||||
|
}
|
||||||
|
__owl__.ownProps = nextProps;
|
||||||
|
const mergedProps = Object.assign({}, nextProps, __owl__.currentStoreProps);
|
||||||
|
return super.__updateProps(mergedProps, forceUpdate, patchQueue);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import { Env } from "../component/component";
|
||||||
|
import { EventBus } from "../core/event_bus";
|
||||||
|
import { Observer } from "../core/observer";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Owl Store
|
||||||
|
*
|
||||||
|
* We have here:
|
||||||
|
* - a Store class
|
||||||
|
* - the ConnectedComponent class
|
||||||
|
*
|
||||||
|
* The Owl store is our answer to the problem of managing complex state across
|
||||||
|
* components. The main idea is that the store owns some state, allow external
|
||||||
|
* code to modify it through actions/mutations, and for each state changes,
|
||||||
|
* connected component will be notified, and updated if necessary.
|
||||||
|
*
|
||||||
|
* Note that this code is partly inspired by VueX and React/Redux
|
||||||
|
*/
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Store Definition
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Mutation = ({ state, commit, getters }, ...payload: any) => void;
|
||||||
|
type Action = ({ commit, state, dispatch, env, getters }, ...payload: any) => void;
|
||||||
|
type Getter = ({ state, getters }, payload) => any;
|
||||||
|
|
||||||
|
interface StoreConfig {
|
||||||
|
env?: Env;
|
||||||
|
state?: any;
|
||||||
|
actions?: { [name: string]: Action };
|
||||||
|
getters?: { [name: string]: Getter };
|
||||||
|
mutations?: { [name: string]: Mutation };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StoreOption {
|
||||||
|
debug?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Store extends EventBus {
|
||||||
|
state: any;
|
||||||
|
actions: any;
|
||||||
|
mutations: any;
|
||||||
|
_commitLevel: number = 0;
|
||||||
|
history: any[] = [];
|
||||||
|
debug: boolean;
|
||||||
|
env: any;
|
||||||
|
observer: Observer;
|
||||||
|
getters: { [name: string]: (payload?) => any };
|
||||||
|
_gettersCache: { [name: string]: {} };
|
||||||
|
_updateId: number = 1;
|
||||||
|
|
||||||
|
constructor(config: StoreConfig, options: StoreOption = {}) {
|
||||||
|
super();
|
||||||
|
this.debug = options.debug || false;
|
||||||
|
this.actions = config.actions;
|
||||||
|
this.mutations = config.mutations;
|
||||||
|
this.env = config.env;
|
||||||
|
this.observer = new Observer();
|
||||||
|
this.observer.notifyCB = this.__notifyComponents.bind(this);
|
||||||
|
this.observer.allowMutations = false;
|
||||||
|
this.state = this.observer.observe(config.state || {});
|
||||||
|
this.getters = {};
|
||||||
|
this._gettersCache = {};
|
||||||
|
|
||||||
|
if (this.debug) {
|
||||||
|
this.history.push({ state: this.state });
|
||||||
|
}
|
||||||
|
|
||||||
|
const cTypes = ["undefined", "number", "string"];
|
||||||
|
for (let entry of Object.entries(config.getters || {})) {
|
||||||
|
const name: string = entry[0];
|
||||||
|
const func: (...any) => any = entry[1];
|
||||||
|
this.getters[name] = payload => {
|
||||||
|
if (this._commitLevel === 0 && cTypes.indexOf(typeof payload) >= 0) {
|
||||||
|
this._gettersCache[name] = this._gettersCache[name] || {};
|
||||||
|
this._gettersCache[name][payload] =
|
||||||
|
this._gettersCache[name][payload] ||
|
||||||
|
func({ state: this.state, getters: this.getters }, payload);
|
||||||
|
return this._gettersCache[name][payload];
|
||||||
|
}
|
||||||
|
return func({ state: this.state, getters: this.getters }, payload);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(action: string, ...payload: any): Promise<void> | void {
|
||||||
|
if (!this.actions[action]) {
|
||||||
|
throw new Error(`[Error] action ${action} is undefined`);
|
||||||
|
}
|
||||||
|
const result = this.actions[action](
|
||||||
|
{
|
||||||
|
commit: this.commit.bind(this),
|
||||||
|
dispatch: this.dispatch.bind(this),
|
||||||
|
env: this.env,
|
||||||
|
state: this.state,
|
||||||
|
getters: this.getters
|
||||||
|
},
|
||||||
|
...payload
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
commit(type: string, ...payload: any): any {
|
||||||
|
if (!this.mutations[type]) {
|
||||||
|
throw new Error(`[Error] mutation ${type} is undefined`);
|
||||||
|
}
|
||||||
|
this._commitLevel++;
|
||||||
|
this.observer.allowMutations = true;
|
||||||
|
|
||||||
|
const res = this.mutations[type].call(
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
commit: this.commit.bind(this),
|
||||||
|
state: this.state,
|
||||||
|
getters: this.getters
|
||||||
|
},
|
||||||
|
...payload
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this._commitLevel === 1) {
|
||||||
|
this.observer.allowMutations = false;
|
||||||
|
if (this.debug) {
|
||||||
|
this.history.push({
|
||||||
|
state: this.state,
|
||||||
|
mutation: type,
|
||||||
|
payload: [...payload]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._commitLevel--;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instead of using trigger to emit an update event, we actually implement
|
||||||
|
* our own function to do that. The reason is that we need to be smarter than
|
||||||
|
* a simple trigger function: we need to wait for parent components to be
|
||||||
|
* done before doing children components. The reason is that if an update
|
||||||
|
* as an effect of destroying a children, we do not want to call the
|
||||||
|
* mapStoreToProps function of the child, nor rendering it.
|
||||||
|
*
|
||||||
|
* This method is not optimal if we have a bunch of asynchronous components:
|
||||||
|
* we wait sequentially for each component to be completed before updating the
|
||||||
|
* next. However, the only things that matters is that children are updated
|
||||||
|
* after their parents. So, this could be optimized by being smarter, and
|
||||||
|
* updating all widgets concurrently, except for parents/children.
|
||||||
|
*/
|
||||||
|
async __notifyComponents() {
|
||||||
|
this._updateId++;
|
||||||
|
const current = this._updateId;
|
||||||
|
this._gettersCache = {};
|
||||||
|
const subs = this.subscriptions.update || [];
|
||||||
|
for (let i = 0, iLen = subs.length; i < iLen; i++) {
|
||||||
|
const sub = subs[i];
|
||||||
|
const shouldCallback = sub.owner ? sub.owner.__owl__.isMounted : true;
|
||||||
|
if (shouldCallback) {
|
||||||
|
await sub.callback.call(sub.owner, current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-6
@@ -10,7 +10,7 @@
|
|||||||
* - debounce
|
* - debounce
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function whenReady(fn) {
|
export function whenReady(fn?: any) {
|
||||||
return new Promise(function(resolve) {
|
return new Promise(function(resolve) {
|
||||||
if (document.readyState !== "loading") {
|
if (document.readyState !== "loading") {
|
||||||
resolve();
|
resolve();
|
||||||
@@ -76,11 +76,7 @@ export function escape(str: string | number | undefined): string {
|
|||||||
*
|
*
|
||||||
* Inspired by https://davidwalsh.name/javascript-debounce-function
|
* Inspired by https://davidwalsh.name/javascript-debounce-function
|
||||||
*/
|
*/
|
||||||
export function debounce(
|
export function debounce(func: Function, wait: number, immediate?: boolean): Function {
|
||||||
func: Function,
|
|
||||||
wait: number,
|
|
||||||
immediate?: boolean
|
|
||||||
): Function {
|
|
||||||
let timeout;
|
let timeout;
|
||||||
return function(this: any) {
|
return function(this: any) {
|
||||||
const context = this;
|
const context = this;
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { attrsModule, classModule, eventListenersModule, propsModule } from "./modules";
|
||||||
|
import { init } from "./vdom";
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// patch
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export { h, VNode } from "./vdom";
|
||||||
|
|
||||||
|
export const patch = init([eventListenersModule, attrsModule, propsModule, classModule]);
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
import { Module, VNode, VNodeData } from "./vdom";
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// module/props.ts
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function updateProps(oldVnode: VNode, vnode: VNode): void {
|
||||||
|
var key: string,
|
||||||
|
cur: any,
|
||||||
|
old: any,
|
||||||
|
elm = vnode.elm,
|
||||||
|
oldProps = (oldVnode.data as VNodeData).props,
|
||||||
|
props = (vnode.data as VNodeData).props;
|
||||||
|
|
||||||
|
if (!oldProps && !props) return;
|
||||||
|
if (oldProps === props) return;
|
||||||
|
oldProps = oldProps || {};
|
||||||
|
props = props || {};
|
||||||
|
|
||||||
|
for (key in oldProps) {
|
||||||
|
if (!props[key]) {
|
||||||
|
delete (elm as any)[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (key in props) {
|
||||||
|
cur = props[key];
|
||||||
|
old = oldProps[key];
|
||||||
|
if (old !== cur && (key !== "value" || (elm as any)[key] !== cur)) {
|
||||||
|
(elm as any)[key] = cur;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const propsModule = {
|
||||||
|
create: updateProps,
|
||||||
|
update: updateProps
|
||||||
|
} as Module;
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// module/eventlisteners.ts
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function invokeHandler(handler: any, vnode?: VNode, event?: Event): void {
|
||||||
|
if (typeof handler === "function") {
|
||||||
|
// call function handler
|
||||||
|
handler.call(vnode, event, vnode);
|
||||||
|
} else if (typeof handler === "object") {
|
||||||
|
// call handler with arguments
|
||||||
|
if (typeof handler[0] === "function") {
|
||||||
|
// special case for single argument for performance
|
||||||
|
if (handler.length === 2) {
|
||||||
|
handler[0].call(vnode, handler[1], event, vnode);
|
||||||
|
} else {
|
||||||
|
var args = handler.slice(1);
|
||||||
|
args.push(event);
|
||||||
|
args.push(vnode);
|
||||||
|
handler[0].apply(vnode, args);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// call multiple handlers
|
||||||
|
for (let i = 0, iLen = handler.length; i < iLen; i++) {
|
||||||
|
invokeHandler(handler[i], vnode, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEvent(event: Event, vnode: VNode) {
|
||||||
|
var name = event.type,
|
||||||
|
on = (vnode.data as VNodeData).on;
|
||||||
|
|
||||||
|
// call event handler(s) if exists
|
||||||
|
if (on && on[name]) {
|
||||||
|
invokeHandler(on[name], vnode, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createListener() {
|
||||||
|
return function handler(event: Event) {
|
||||||
|
handleEvent(event, (handler as any).vnode);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateEventListeners(oldVnode: VNode, vnode?: VNode): void {
|
||||||
|
var oldOn = (oldVnode.data as VNodeData).on,
|
||||||
|
oldListener = (oldVnode as any).listener,
|
||||||
|
oldElm: Element = oldVnode.elm as Element,
|
||||||
|
on = vnode && (vnode.data as VNodeData).on,
|
||||||
|
elm: Element = (vnode && vnode.elm) as Element,
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
// optimization for reused immutable handlers
|
||||||
|
if (oldOn === on) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove existing listeners which no longer used
|
||||||
|
if (oldOn && oldListener) {
|
||||||
|
// if element changed or deleted we remove all existing listeners unconditionally
|
||||||
|
if (!on) {
|
||||||
|
for (name in oldOn) {
|
||||||
|
// remove listener if element was changed or existing listeners removed
|
||||||
|
oldElm.removeEventListener(name, oldListener, false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (name in oldOn) {
|
||||||
|
// remove listener if existing listener removed
|
||||||
|
if (!on[name]) {
|
||||||
|
oldElm.removeEventListener(name, oldListener, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add new listeners which has not already attached
|
||||||
|
if (on) {
|
||||||
|
// reuse existing listener or create new
|
||||||
|
var listener = ((vnode as any).listener = (oldVnode as any).listener || createListener());
|
||||||
|
// update vnode for listener
|
||||||
|
listener.vnode = vnode;
|
||||||
|
|
||||||
|
// if element changed or added we add all needed listeners unconditionally
|
||||||
|
if (!oldOn) {
|
||||||
|
for (name in on) {
|
||||||
|
// add listener if element was changed or new listeners added
|
||||||
|
elm.addEventListener(name, listener, false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (name in on) {
|
||||||
|
// add listener if new listener added
|
||||||
|
if (!oldOn[name]) {
|
||||||
|
elm.addEventListener(name, listener, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const eventListenersModule = {
|
||||||
|
create: updateEventListeners,
|
||||||
|
update: updateEventListeners,
|
||||||
|
destroy: updateEventListeners
|
||||||
|
} as Module;
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// attributes.ts
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const xlinkNS = "http://www.w3.org/1999/xlink";
|
||||||
|
const xmlNS = "http://www.w3.org/XML/1998/namespace";
|
||||||
|
const colonChar = 58;
|
||||||
|
const xChar = 120;
|
||||||
|
|
||||||
|
function updateAttrs(oldVnode: VNode, vnode: VNode): void {
|
||||||
|
var key: string,
|
||||||
|
elm: Element = vnode.elm as Element,
|
||||||
|
oldAttrs = (oldVnode.data as VNodeData).attrs,
|
||||||
|
attrs = (vnode.data as VNodeData).attrs;
|
||||||
|
|
||||||
|
if (!oldAttrs && !attrs) return;
|
||||||
|
if (oldAttrs === attrs) return;
|
||||||
|
oldAttrs = oldAttrs || {};
|
||||||
|
attrs = attrs || {};
|
||||||
|
|
||||||
|
// update modified attributes, add new attributes
|
||||||
|
for (key in attrs) {
|
||||||
|
const cur = attrs[key];
|
||||||
|
const old = oldAttrs[key];
|
||||||
|
if (old !== cur) {
|
||||||
|
if (cur === true) {
|
||||||
|
elm.setAttribute(key, "");
|
||||||
|
} else if (cur === false) {
|
||||||
|
elm.removeAttribute(key);
|
||||||
|
} else {
|
||||||
|
if (key.charCodeAt(0) !== xChar) {
|
||||||
|
elm.setAttribute(key, cur);
|
||||||
|
} else if (key.charCodeAt(3) === colonChar) {
|
||||||
|
// Assume xml namespace
|
||||||
|
elm.setAttributeNS(xmlNS, key, cur);
|
||||||
|
} else if (key.charCodeAt(5) === colonChar) {
|
||||||
|
// Assume xlink namespace
|
||||||
|
elm.setAttributeNS(xlinkNS, key, cur);
|
||||||
|
} else {
|
||||||
|
elm.setAttribute(key, cur);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// remove removed attributes
|
||||||
|
// use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value)
|
||||||
|
// the other option is to remove all attributes with value == undefined
|
||||||
|
for (key in oldAttrs) {
|
||||||
|
if (!(key in attrs)) {
|
||||||
|
elm.removeAttribute(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const attrsModule = {
|
||||||
|
create: updateAttrs,
|
||||||
|
update: updateAttrs
|
||||||
|
} as Module;
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// class.ts
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
function updateClass(oldVnode: VNode, vnode: VNode): void {
|
||||||
|
var cur: any,
|
||||||
|
name: string,
|
||||||
|
elm: Element,
|
||||||
|
oldClass = (oldVnode.data as VNodeData).class,
|
||||||
|
klass = (vnode.data as VNodeData).class;
|
||||||
|
|
||||||
|
if (!oldClass && !klass) return;
|
||||||
|
if (oldClass === klass) return;
|
||||||
|
oldClass = oldClass || {};
|
||||||
|
klass = klass || {};
|
||||||
|
|
||||||
|
elm = vnode.elm as Element;
|
||||||
|
|
||||||
|
for (name in oldClass) {
|
||||||
|
if (!klass[name]) {
|
||||||
|
elm.classList.remove(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (name in klass) {
|
||||||
|
cur = klass[name];
|
||||||
|
if (cur !== oldClass[name]) {
|
||||||
|
(elm.classList as any)[cur ? "add" : "remove"](name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const classModule = { create: updateClass, update: updateClass } as Module;
|
||||||
+38
-331
@@ -27,6 +27,21 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Props = Record<string, any>;
|
||||||
|
type Attrs = Record<string, string | number | boolean>;
|
||||||
|
type On = { [N in keyof HTMLElementEventMap]?: (ev: HTMLElementEventMap[N]) => void } & {
|
||||||
|
[event: string]: EventListener;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface Module {
|
||||||
|
pre: PreHook;
|
||||||
|
create: CreateHook;
|
||||||
|
update: UpdateHook;
|
||||||
|
destroy: DestroyHook;
|
||||||
|
remove: RemoveHook;
|
||||||
|
post: PostHook;
|
||||||
|
}
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// vnode.ts
|
// vnode.ts
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
@@ -41,7 +56,7 @@ export interface VNode {
|
|||||||
key: Key | undefined;
|
key: Key | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface VNodeData {
|
export interface VNodeData {
|
||||||
props?: Props;
|
props?: Props;
|
||||||
attrs?: Attrs;
|
attrs?: Attrs;
|
||||||
on?: On;
|
on?: On;
|
||||||
@@ -109,14 +124,7 @@ function createKeyToOldIdx(
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hooks: (keyof Module)[] = [
|
const hooks: (keyof Module)[] = ["create", "update", "remove", "destroy", "pre", "post"];
|
||||||
"create",
|
|
||||||
"update",
|
|
||||||
"remove",
|
|
||||||
"destroy",
|
|
||||||
"pre",
|
|
||||||
"post"
|
|
||||||
];
|
|
||||||
|
|
||||||
export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||||
let i: number,
|
let i: number,
|
||||||
@@ -138,13 +146,7 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
|||||||
function emptyNodeAt(elm: Element) {
|
function emptyNodeAt(elm: Element) {
|
||||||
const id = elm.id ? "#" + elm.id : "";
|
const id = elm.id ? "#" + elm.id : "";
|
||||||
const c = elm.className ? "." + elm.className.split(" ").join(".") : "";
|
const c = elm.className ? "." + elm.className.split(" ").join(".") : "";
|
||||||
return vnode(
|
return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm);
|
||||||
api.tagName(elm).toLowerCase() + id + c,
|
|
||||||
{},
|
|
||||||
[],
|
|
||||||
undefined,
|
|
||||||
elm
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createRmCb(childElm: Node, listeners: number) {
|
function createRmCb(childElm: Node, listeners: number) {
|
||||||
@@ -179,19 +181,14 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
|||||||
const dotIdx = sel.indexOf(".", hashIdx);
|
const dotIdx = sel.indexOf(".", hashIdx);
|
||||||
const hash = hashIdx > 0 ? hashIdx : sel.length;
|
const hash = hashIdx > 0 ? hashIdx : sel.length;
|
||||||
const dot = dotIdx > 0 ? dotIdx : sel.length;
|
const dot = dotIdx > 0 ? dotIdx : sel.length;
|
||||||
const tag =
|
const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel;
|
||||||
hashIdx !== -1 || dotIdx !== -1
|
|
||||||
? sel.slice(0, Math.min(hash, dot))
|
|
||||||
: sel;
|
|
||||||
const elm = (vnode.elm =
|
const elm = (vnode.elm =
|
||||||
isDef(data) && isDef((i = (data as VNodeData).ns))
|
isDef(data) && isDef((i = (data as VNodeData).ns))
|
||||||
? api.createElementNS(i, tag)
|
? api.createElementNS(i, tag)
|
||||||
: api.createElement(tag));
|
: api.createElement(tag));
|
||||||
if (hash < dot) elm.setAttribute("id", sel.slice(hash + 1, dot));
|
if (hash < dot) elm.setAttribute("id", sel.slice(hash + 1, dot));
|
||||||
if (dotIdx > 0)
|
if (dotIdx > 0) elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " "));
|
||||||
elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " "));
|
for (i = 0, iLen = cbs.create.length; i < iLen; ++i) cbs.create[i](emptyNode, vnode);
|
||||||
for (i = 0, iLen = cbs.create.length; i < iLen; ++i)
|
|
||||||
cbs.create[i](emptyNode, vnode);
|
|
||||||
if (array(children)) {
|
if (array(children)) {
|
||||||
for (i = 0, iLen = children.length; i < iLen; ++i) {
|
for (i = 0, iLen = children.length; i < iLen; ++i) {
|
||||||
const ch = children[i];
|
const ch = children[i];
|
||||||
@@ -237,8 +234,7 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
|||||||
data = vnode.data;
|
data = vnode.data;
|
||||||
if (data !== undefined) {
|
if (data !== undefined) {
|
||||||
if (isDef((i = data.hook)) && isDef((i = i.destroy))) i(vnode);
|
if (isDef((i = data.hook)) && isDef((i = i.destroy))) i(vnode);
|
||||||
for (i = 0, iLen = cbs.destroy.length; i < iLen; ++i)
|
for (i = 0, iLen = cbs.destroy.length; i < iLen; ++i) cbs.destroy[i](vnode);
|
||||||
cbs.destroy[i](vnode);
|
|
||||||
if (vnode.children !== undefined) {
|
if (vnode.children !== undefined) {
|
||||||
for (j = 0, jLen = vnode.children.length; j < jLen; ++j) {
|
for (j = 0, jLen = vnode.children.length; j < jLen; ++j) {
|
||||||
i = vnode.children[j];
|
i = vnode.children[j];
|
||||||
@@ -267,13 +263,8 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
|||||||
invokeDestroyHook(ch);
|
invokeDestroyHook(ch);
|
||||||
listeners = cbs.remove.length + 1;
|
listeners = cbs.remove.length + 1;
|
||||||
rm = createRmCb(ch.elm as Node, listeners);
|
rm = createRmCb(ch.elm as Node, listeners);
|
||||||
for (i = 0, iLen = cbs.remove.length; i < iLen; ++i)
|
for (i = 0, iLen = cbs.remove.length; i < iLen; ++i) cbs.remove[i](ch, rm);
|
||||||
cbs.remove[i](ch, rm);
|
if (isDef((i = ch.data)) && isDef((i = i.hook)) && isDef((i = i.remove))) {
|
||||||
if (
|
|
||||||
isDef((i = ch.data)) &&
|
|
||||||
isDef((i = i.hook)) &&
|
|
||||||
isDef((i = i.remove))
|
|
||||||
) {
|
|
||||||
i(ch, rm);
|
i(ch, rm);
|
||||||
} else {
|
} else {
|
||||||
rm();
|
rm();
|
||||||
@@ -335,11 +326,7 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
|||||||
} else if (sameVnode(oldEndVnode, newStartVnode)) {
|
} else if (sameVnode(oldEndVnode, newStartVnode)) {
|
||||||
// Vnode moved left
|
// Vnode moved left
|
||||||
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
|
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
|
||||||
api.insertBefore(
|
api.insertBefore(parentElm, oldEndVnode.elm as Node, oldStartVnode.elm as Node);
|
||||||
parentElm,
|
|
||||||
oldEndVnode.elm as Node,
|
|
||||||
oldStartVnode.elm as Node
|
|
||||||
);
|
|
||||||
oldEndVnode = oldCh[--oldEndIdx];
|
oldEndVnode = oldCh[--oldEndIdx];
|
||||||
newStartVnode = newCh[++newStartIdx];
|
newStartVnode = newCh[++newStartIdx];
|
||||||
} else {
|
} else {
|
||||||
@@ -366,11 +353,7 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
|||||||
} else {
|
} else {
|
||||||
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
|
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
|
||||||
oldCh[idxInOld] = undefined as any;
|
oldCh[idxInOld] = undefined as any;
|
||||||
api.insertBefore(
|
api.insertBefore(parentElm, elmToMove.elm as Node, oldStartVnode.elm as Node);
|
||||||
parentElm,
|
|
||||||
elmToMove.elm as Node,
|
|
||||||
oldStartVnode.elm as Node
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
newStartVnode = newCh[++newStartIdx];
|
newStartVnode = newCh[++newStartIdx];
|
||||||
}
|
}
|
||||||
@@ -379,31 +362,16 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
|||||||
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
|
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
|
||||||
if (oldStartIdx > oldEndIdx) {
|
if (oldStartIdx > oldEndIdx) {
|
||||||
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
|
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
|
||||||
addVnodes(
|
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
|
||||||
parentElm,
|
|
||||||
before,
|
|
||||||
newCh,
|
|
||||||
newStartIdx,
|
|
||||||
newEndIdx,
|
|
||||||
insertedVnodeQueue
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
|
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function patchVnode(
|
function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
|
||||||
oldVnode: VNode,
|
|
||||||
vnode: VNode,
|
|
||||||
insertedVnodeQueue: VNodeQueue
|
|
||||||
) {
|
|
||||||
let i: any, iLen: number, hook: any;
|
let i: any, iLen: number, hook: any;
|
||||||
if (
|
if (isDef((i = vnode.data)) && isDef((hook = i.hook)) && isDef((i = hook.prepatch))) {
|
||||||
isDef((i = vnode.data)) &&
|
|
||||||
isDef((hook = i.hook)) &&
|
|
||||||
isDef((i = hook.prepatch))
|
|
||||||
) {
|
|
||||||
i(oldVnode, vnode);
|
i(oldVnode, vnode);
|
||||||
}
|
}
|
||||||
const elm = (vnode.elm = oldVnode.elm as Node);
|
const elm = (vnode.elm = oldVnode.elm as Node);
|
||||||
@@ -411,20 +379,14 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
|||||||
let ch = vnode.children;
|
let ch = vnode.children;
|
||||||
if (oldVnode === vnode) return;
|
if (oldVnode === vnode) return;
|
||||||
if (vnode.data !== undefined) {
|
if (vnode.data !== undefined) {
|
||||||
for (i = 0, iLen = cbs.update.length; i < iLen; ++i)
|
for (i = 0, iLen = cbs.update.length; i < iLen; ++i) cbs.update[i](oldVnode, vnode);
|
||||||
cbs.update[i](oldVnode, vnode);
|
|
||||||
i = vnode.data.hook;
|
i = vnode.data.hook;
|
||||||
if (isDef(i) && isDef((i = i.update))) i(oldVnode, vnode);
|
if (isDef(i) && isDef((i = i.update))) i(oldVnode, vnode);
|
||||||
}
|
}
|
||||||
if (isUndef(vnode.text)) {
|
if (isUndef(vnode.text)) {
|
||||||
if (isDef(oldCh) && isDef(ch)) {
|
if (isDef(oldCh) && isDef(ch)) {
|
||||||
if (oldCh !== ch)
|
if (oldCh !== ch)
|
||||||
updateChildren(
|
updateChildren(elm, oldCh as Array<VNode>, ch as Array<VNode>, insertedVnodeQueue);
|
||||||
elm,
|
|
||||||
oldCh as Array<VNode>,
|
|
||||||
ch as Array<VNode>,
|
|
||||||
insertedVnodeQueue
|
|
||||||
);
|
|
||||||
} else if (isDef(ch)) {
|
} else if (isDef(ch)) {
|
||||||
if (isDef(oldVnode.text)) api.setTextContent(elm, "");
|
if (isDef(oldVnode.text)) api.setTextContent(elm, "");
|
||||||
addVnodes(
|
addVnodes(
|
||||||
@@ -436,23 +398,13 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
|||||||
insertedVnodeQueue
|
insertedVnodeQueue
|
||||||
);
|
);
|
||||||
} else if (isDef(oldCh)) {
|
} else if (isDef(oldCh)) {
|
||||||
removeVnodes(
|
removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
|
||||||
elm,
|
|
||||||
oldCh as Array<VNode>,
|
|
||||||
0,
|
|
||||||
(oldCh as Array<VNode>).length - 1
|
|
||||||
);
|
|
||||||
} else if (isDef(oldVnode.text)) {
|
} else if (isDef(oldVnode.text)) {
|
||||||
api.setTextContent(elm, "");
|
api.setTextContent(elm, "");
|
||||||
}
|
}
|
||||||
} else if (oldVnode.text !== vnode.text) {
|
} else if (oldVnode.text !== vnode.text) {
|
||||||
if (isDef(oldCh)) {
|
if (isDef(oldCh)) {
|
||||||
removeVnodes(
|
removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
|
||||||
elm,
|
|
||||||
oldCh as Array<VNode>,
|
|
||||||
0,
|
|
||||||
(oldCh as Array<VNode>).length - 1
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
api.setTextContent(elm, vnode.text as string);
|
api.setTextContent(elm, vnode.text as string);
|
||||||
}
|
}
|
||||||
@@ -510,11 +462,7 @@ interface DOMAPI {
|
|||||||
createElementNS: (namespaceURI: string, qualifiedName: string) => Element;
|
createElementNS: (namespaceURI: string, qualifiedName: string) => Element;
|
||||||
createTextNode: (text: string) => Text;
|
createTextNode: (text: string) => Text;
|
||||||
createComment: (text: string) => Comment;
|
createComment: (text: string) => Comment;
|
||||||
insertBefore: (
|
insertBefore: (parentNode: Node, newNode: Node, referenceNode: Node | null) => void;
|
||||||
parentNode: Node,
|
|
||||||
newNode: Node,
|
|
||||||
referenceNode: Node | null
|
|
||||||
) => void;
|
|
||||||
removeChild: (node: Node, child: Node) => void;
|
removeChild: (node: Node, child: Node) => void;
|
||||||
appendChild: (node: Node, child: Node) => void;
|
appendChild: (node: Node, child: Node) => void;
|
||||||
parentNode: (node: Node) => Node;
|
parentNode: (node: Node) => Node;
|
||||||
@@ -543,11 +491,7 @@ function createComment(text: string): Comment {
|
|||||||
return document.createComment(text);
|
return document.createComment(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertBefore(
|
function insertBefore(parentNode: Node, newNode: Node, referenceNode: Node | null): void {
|
||||||
parentNode: Node,
|
|
||||||
newNode: Node,
|
|
||||||
referenceNode: Node | null
|
|
||||||
): void {
|
|
||||||
parentNode.insertBefore(newNode, referenceNode);
|
parentNode.insertBefore(newNode, referenceNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -644,21 +588,13 @@ 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(
|
function addNS(data: any, children: VNodes | undefined, sel: string | undefined): void {
|
||||||
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;
|
let childData = children[i].data;
|
||||||
if (childData !== undefined) {
|
if (childData !== undefined) {
|
||||||
addNS(
|
addNS(childData, (children[i] as VNode).children as VNodes, children[i].sel);
|
||||||
childData,
|
|
||||||
(children[i] as VNode).children as VNodes,
|
|
||||||
children[i].sel
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -697,13 +633,7 @@ export function h(sel: any, b?: any, c?: any): VNode {
|
|||||||
if (children !== undefined) {
|
if (children !== undefined) {
|
||||||
for (i = 0, iLen = children.length; i < iLen; ++i) {
|
for (i = 0, iLen = children.length; i < iLen; ++i) {
|
||||||
if (primitive(children[i]))
|
if (primitive(children[i]))
|
||||||
children[i] = vnode(
|
children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
children[i],
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@@ -716,226 +646,3 @@ export function h(sel: any, b?: any, c?: any): VNode {
|
|||||||
}
|
}
|
||||||
return vnode(sel, data, children, text, undefined);
|
return vnode(sel, data, children, text, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// module/props.ts
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
type Props = Record<string, any>;
|
|
||||||
|
|
||||||
function updateProps(oldVnode: VNode, vnode: VNode): void {
|
|
||||||
var key: string,
|
|
||||||
cur: any,
|
|
||||||
old: any,
|
|
||||||
elm = vnode.elm,
|
|
||||||
oldProps = (oldVnode.data as VNodeData).props,
|
|
||||||
props = (vnode.data as VNodeData).props;
|
|
||||||
|
|
||||||
if (!oldProps && !props) return;
|
|
||||||
if (oldProps === props) return;
|
|
||||||
oldProps = oldProps || {};
|
|
||||||
props = props || {};
|
|
||||||
|
|
||||||
for (key in oldProps) {
|
|
||||||
if (!props[key]) {
|
|
||||||
delete (elm as any)[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (key in props) {
|
|
||||||
cur = props[key];
|
|
||||||
old = oldProps[key];
|
|
||||||
if (old !== cur && (key !== "value" || (elm as any)[key] !== cur)) {
|
|
||||||
(elm as any)[key] = cur;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const propsModule = {
|
|
||||||
create: updateProps,
|
|
||||||
update: updateProps
|
|
||||||
} as Module;
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// module/module.ts
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
interface Module {
|
|
||||||
pre: PreHook;
|
|
||||||
create: CreateHook;
|
|
||||||
update: UpdateHook;
|
|
||||||
destroy: DestroyHook;
|
|
||||||
remove: RemoveHook;
|
|
||||||
post: PostHook;
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// module/eventlisteners.ts
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
type On = {
|
|
||||||
[N in keyof HTMLElementEventMap]?: (ev: HTMLElementEventMap[N]) => void
|
|
||||||
} & {
|
|
||||||
[event: string]: EventListener;
|
|
||||||
};
|
|
||||||
|
|
||||||
function invokeHandler(handler: any, vnode?: VNode, event?: Event): void {
|
|
||||||
if (typeof handler === "function") {
|
|
||||||
// call function handler
|
|
||||||
handler.call(vnode, event, vnode);
|
|
||||||
} else if (typeof handler === "object") {
|
|
||||||
// call handler with arguments
|
|
||||||
if (typeof handler[0] === "function") {
|
|
||||||
// special case for single argument for performance
|
|
||||||
if (handler.length === 2) {
|
|
||||||
handler[0].call(vnode, handler[1], event, vnode);
|
|
||||||
} else {
|
|
||||||
var args = handler.slice(1);
|
|
||||||
args.push(event);
|
|
||||||
args.push(vnode);
|
|
||||||
handler[0].apply(vnode, args);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// call multiple handlers
|
|
||||||
for (let i = 0, iLen = handler.length; i < iLen; i++) {
|
|
||||||
invokeHandler(handler[i], vnode, event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleEvent(event: Event, vnode: VNode) {
|
|
||||||
var name = event.type,
|
|
||||||
on = (vnode.data as VNodeData).on;
|
|
||||||
|
|
||||||
// call event handler(s) if exists
|
|
||||||
if (on && on[name]) {
|
|
||||||
invokeHandler(on[name], vnode, event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createListener() {
|
|
||||||
return function handler(event: Event) {
|
|
||||||
handleEvent(event, (handler as any).vnode);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateEventListeners(oldVnode: VNode, vnode?: VNode): void {
|
|
||||||
var oldOn = (oldVnode.data as VNodeData).on,
|
|
||||||
oldListener = (oldVnode as any).listener,
|
|
||||||
oldElm: Element = oldVnode.elm as Element,
|
|
||||||
on = vnode && (vnode.data as VNodeData).on,
|
|
||||||
elm: Element = (vnode && vnode.elm) as Element,
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
// optimization for reused immutable handlers
|
|
||||||
if (oldOn === on) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove existing listeners which no longer used
|
|
||||||
if (oldOn && oldListener) {
|
|
||||||
// if element changed or deleted we remove all existing listeners unconditionally
|
|
||||||
if (!on) {
|
|
||||||
for (name in oldOn) {
|
|
||||||
// remove listener if element was changed or existing listeners removed
|
|
||||||
oldElm.removeEventListener(name, oldListener, false);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (name in oldOn) {
|
|
||||||
// remove listener if existing listener removed
|
|
||||||
if (!on[name]) {
|
|
||||||
oldElm.removeEventListener(name, oldListener, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// add new listeners which has not already attached
|
|
||||||
if (on) {
|
|
||||||
// reuse existing listener or create new
|
|
||||||
var listener = ((vnode as any).listener =
|
|
||||||
(oldVnode as any).listener || createListener());
|
|
||||||
// update vnode for listener
|
|
||||||
listener.vnode = vnode;
|
|
||||||
|
|
||||||
// if element changed or added we add all needed listeners unconditionally
|
|
||||||
if (!oldOn) {
|
|
||||||
for (name in on) {
|
|
||||||
// add listener if element was changed or new listeners added
|
|
||||||
elm.addEventListener(name, listener, false);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (name in on) {
|
|
||||||
// add listener if new listener added
|
|
||||||
if (!oldOn[name]) {
|
|
||||||
elm.addEventListener(name, listener, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const eventListenersModule = {
|
|
||||||
create: updateEventListeners,
|
|
||||||
update: updateEventListeners,
|
|
||||||
destroy: updateEventListeners
|
|
||||||
} as Module;
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// attributes.ts
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
type Attrs = Record<string, string | number | boolean>;
|
|
||||||
|
|
||||||
const xlinkNS = "http://www.w3.org/1999/xlink";
|
|
||||||
const xmlNS = "http://www.w3.org/XML/1998/namespace";
|
|
||||||
const colonChar = 58;
|
|
||||||
const xChar = 120;
|
|
||||||
|
|
||||||
function updateAttrs(oldVnode: VNode, vnode: VNode): void {
|
|
||||||
var key: string,
|
|
||||||
elm: Element = vnode.elm as Element,
|
|
||||||
oldAttrs = (oldVnode.data as VNodeData).attrs,
|
|
||||||
attrs = (vnode.data as VNodeData).attrs;
|
|
||||||
|
|
||||||
if (!oldAttrs && !attrs) return;
|
|
||||||
if (oldAttrs === attrs) return;
|
|
||||||
oldAttrs = oldAttrs || {};
|
|
||||||
attrs = attrs || {};
|
|
||||||
|
|
||||||
// update modified attributes, add new attributes
|
|
||||||
for (key in attrs) {
|
|
||||||
const cur = attrs[key];
|
|
||||||
const old = oldAttrs[key];
|
|
||||||
if (old !== cur) {
|
|
||||||
if (cur === true) {
|
|
||||||
elm.setAttribute(key, "");
|
|
||||||
} else if (cur === false) {
|
|
||||||
elm.removeAttribute(key);
|
|
||||||
} else {
|
|
||||||
if (key.charCodeAt(0) !== xChar) {
|
|
||||||
elm.setAttribute(key, cur);
|
|
||||||
} else if (key.charCodeAt(3) === colonChar) {
|
|
||||||
// Assume xml namespace
|
|
||||||
elm.setAttributeNS(xmlNS, key, cur);
|
|
||||||
} else if (key.charCodeAt(5) === colonChar) {
|
|
||||||
// Assume xlink namespace
|
|
||||||
elm.setAttributeNS(xlinkNS, key, cur);
|
|
||||||
} else {
|
|
||||||
elm.setAttribute(key, cur);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// remove removed attributes
|
|
||||||
// use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value)
|
|
||||||
// the other option is to remove all attributes with value == undefined
|
|
||||||
for (key in oldAttrs) {
|
|
||||||
if (!(key in attrs)) {
|
|
||||||
elm.removeAttribute(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const attrsModule = {
|
|
||||||
create: updateAttrs,
|
|
||||||
update: updateAttrs
|
|
||||||
} as Module;
|
|
||||||
|
|
||||||
export const patch = init([eventListenersModule, attrsModule, propsModule]);
|
|
||||||
@@ -1,17 +1,18 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`animations t-transition combined with t-widget 1`] = `
|
exports[`animations t-transition combined with component 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
let utils = this.utils;
|
let utils = this.constructor.utils;
|
||||||
let QWeb = this.constructor;
|
let QWeb = this.constructor;
|
||||||
|
let parent = context;
|
||||||
let owner = context;
|
let owner = context;
|
||||||
var h = this.utils.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);
|
||||||
//WIDGET
|
//COMPONENT
|
||||||
let def3;
|
let def3;
|
||||||
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
|
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
||||||
let _2_index = c1.length;
|
let _2_index = c1.length;
|
||||||
c1.push(null);
|
c1.push(null);
|
||||||
let props4 = {};
|
let props4 = {};
|
||||||
@@ -24,18 +25,18 @@ exports[`animations t-transition combined with t-widget 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!w4) {
|
if (!w4) {
|
||||||
let widgetKey4 = \`Child\`;
|
let componentKey4 = \`Child\`;
|
||||||
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
|
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
|
||||||
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
|
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
||||||
w4 = new W4(owner, props4);
|
w4 = new W4(parent, props4);
|
||||||
context.__owl__.cmap[4] = w4.__owl__.id;
|
parent.__owl__.cmap[4] = w4.__owl__.id;
|
||||||
def3 = w4._prepare();
|
def3 = w4.__prepare();
|
||||||
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
|
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, '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.forceUpdate, extra.patchQueue);
|
||||||
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);
|
||||||
@@ -43,19 +44,20 @@ exports[`animations t-transition combined with t-widget 1`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`animations t-transition combined with t-widget and t-if 1`] = `
|
exports[`animations t-transition combined with t-component and t-if 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
let utils = this.utils;
|
let utils = this.constructor.utils;
|
||||||
let QWeb = this.constructor;
|
let QWeb = this.constructor;
|
||||||
|
let parent = context;
|
||||||
let owner = context;
|
let owner = context;
|
||||||
var h = this.utils.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);
|
||||||
if (context['state'].display) {
|
if (context['state'].display) {
|
||||||
//WIDGET
|
//COMPONENT
|
||||||
let def3;
|
let def3;
|
||||||
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
|
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
||||||
let _2_index = c1.length;
|
let _2_index = c1.length;
|
||||||
c1.push(null);
|
c1.push(null);
|
||||||
let props4 = {};
|
let props4 = {};
|
||||||
@@ -68,18 +70,18 @@ exports[`animations t-transition combined with t-widget and t-if 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!w4) {
|
if (!w4) {
|
||||||
let widgetKey4 = \`Child\`;
|
let componentKey4 = \`Child\`;
|
||||||
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
|
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
|
||||||
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
|
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
||||||
w4 = new W4(owner, props4);
|
w4 = new W4(parent, props4);
|
||||||
context.__owl__.cmap[4] = w4.__owl__.id;
|
parent.__owl__.cmap[4] = w4.__owl__.id;
|
||||||
def3 = w4._prepare();
|
def3 = w4.__prepare();
|
||||||
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
|
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, '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.forceUpdate, extra.patchQueue);
|
||||||
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);
|
||||||
@@ -91,15 +93,16 @@ exports[`animations t-transition combined with t-widget and t-if 1`] = `
|
|||||||
exports[`animations t-transition with no delay/duration 1`] = `
|
exports[`animations t-transition with no delay/duration 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
let utils = this.constructor.utils;
|
||||||
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('span', p1, c1);
|
var vn1 = h('span', p1, c1);
|
||||||
p1.hook = {
|
p1.hook = {
|
||||||
insert: vn => {
|
insert: vn => {
|
||||||
this.utils.transitionInsert(vn, 'jupiler');
|
utils.transitionInsert(vn, 'jupiler');
|
||||||
},
|
},
|
||||||
remove: (vn, rm) => {
|
remove: (vn, rm) => {
|
||||||
this.utils.transitionRemove(vn, 'jupiler', rm);
|
utils.transitionRemove(vn, 'jupiler', rm);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
c1.push({text: \`blue\`});
|
c1.push({text: \`blue\`});
|
||||||
@@ -110,15 +113,16 @@ exports[`animations t-transition with no delay/duration 1`] = `
|
|||||||
exports[`animations t-transition, on a simple node (insert) 1`] = `
|
exports[`animations t-transition, on a simple node (insert) 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
let utils = this.constructor.utils;
|
||||||
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('span', p1, c1);
|
var vn1 = h('span', p1, c1);
|
||||||
p1.hook = {
|
p1.hook = {
|
||||||
insert: vn => {
|
insert: vn => {
|
||||||
this.utils.transitionInsert(vn, 'chimay');
|
utils.transitionInsert(vn, 'chimay');
|
||||||
},
|
},
|
||||||
remove: (vn, rm) => {
|
remove: (vn, rm) => {
|
||||||
this.utils.transitionRemove(vn, 'chimay', rm);
|
utils.transitionRemove(vn, 'chimay', rm);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
c1.push({text: \`blue\`});
|
c1.push({text: \`blue\`});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,33 +0,0 @@
|
|||||||
function anonymous(context, extra) {
|
|
||||||
var h = this.utils.h;
|
|
||||||
let c1 = [], p1 = {key:1};
|
|
||||||
var vn1 = h('div', p1, c1);
|
|
||||||
if (context['state'].display) {
|
|
||||||
//WIDGET
|
|
||||||
let _2_index = c1.length;
|
|
||||||
c1.push(null);
|
|
||||||
let def3;
|
|
||||||
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
|
|
||||||
let props4 = {};
|
|
||||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
|
|
||||||
w4.destroy();
|
|
||||||
w4 = false
|
|
||||||
}
|
|
||||||
if (!w4) {
|
|
||||||
let W4 = context.widgets['Child'];
|
|
||||||
if (!W4) {throw new Error(`Cannot find the definition of widget "Child"`)}
|
|
||||||
w4 = new W4(owner, props4);
|
|
||||||
context.__owl__.cmap[4] = w4.__owl__.id;
|
|
||||||
def3 = w4._prepare();
|
|
||||||
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert: (vn) => {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;this.utils.transitionInsert(vn.elm, 'chimay');},remove: () => {},destroy: (vn) => {let finalize = () => {
|
|
||||||
w4.destroy();
|
|
||||||
};
|
|
||||||
this.utils.transitionRemove(vn.elm, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
|
||||||
} else {
|
|
||||||
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
|
||||||
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
|
||||||
}
|
|
||||||
extra.promises.push(def3);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
+18
-33
@@ -1,5 +1,5 @@
|
|||||||
import { Component, Env } from "../src/component";
|
import { Component, Env } from "../src/component/component";
|
||||||
import { QWeb } from "../src/qweb_core";
|
import { QWeb } from "../src/qweb/index";
|
||||||
import {
|
import {
|
||||||
makeDeferred,
|
makeDeferred,
|
||||||
makeTestFixture,
|
makeTestFixture,
|
||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
// - fixture: a div, appended to the DOM, intended to be the target of dom
|
// - fixture: a div, appended to the DOM, intended to be the target of dom
|
||||||
// manipulations. Note that it is removed after each test.
|
// manipulations. Note that it is removed after each test.
|
||||||
// - qweb: a new QWeb instance
|
// - qweb: a new QWeb instance
|
||||||
// - env: a WEnv, necessary to create new widgets
|
// - env: a WEnv, necessary to create new components
|
||||||
// - cssEl: a stylesheet injected into the dom
|
// - cssEl: a stylesheet injected into the dom
|
||||||
|
|
||||||
let fixture: HTMLElement;
|
let fixture: HTMLElement;
|
||||||
@@ -170,16 +170,13 @@ describe("animations", () => {
|
|||||||
expect(spanNode.className).toBe("");
|
expect(spanNode.className).toBe("");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("t-transition combined with t-widget", async () => {
|
test("t-transition combined with component", async () => {
|
||||||
expect.assertions(5);
|
expect.assertions(5);
|
||||||
|
|
||||||
env.qweb.addTemplate(
|
env.qweb.addTemplate("Parent", `<div><Child t-transition="chimay"/></div>`);
|
||||||
"Parent",
|
|
||||||
`<div><t t-widget="Child" t-transition="chimay"/></div>`
|
|
||||||
);
|
|
||||||
env.qweb.addTemplate("Child", `<span>blue</span>`);
|
env.qweb.addTemplate("Child", `<span>blue</span>`);
|
||||||
class Parent extends Widget {
|
class Parent extends Widget {
|
||||||
widgets = { Child: Child };
|
components = { Child: Child };
|
||||||
}
|
}
|
||||||
class Child extends Widget {}
|
class Child extends Widget {}
|
||||||
const widget = new Parent(env);
|
const widget = new Parent(env);
|
||||||
@@ -209,16 +206,16 @@ describe("animations", () => {
|
|||||||
expect(fixture.innerHTML).toBe('<div><span class="">blue</span></div>');
|
expect(fixture.innerHTML).toBe('<div><span class="">blue</span></div>');
|
||||||
});
|
});
|
||||||
|
|
||||||
test("t-transition combined with t-widget and t-if", async () => {
|
test("t-transition combined with t-component and t-if", async () => {
|
||||||
expect.assertions(8);
|
expect.assertions(8);
|
||||||
|
|
||||||
env.qweb.addTemplate(
|
env.qweb.addTemplate(
|
||||||
"Parent",
|
"Parent",
|
||||||
`<div><t t-if="state.display" t-widget="Child" t-transition="chimay"/></div>`
|
`<div><t t-if="state.display" t-component="Child" t-transition="chimay"/></div>`
|
||||||
);
|
);
|
||||||
env.qweb.addTemplate("Child", `<span>blue</span>`);
|
env.qweb.addTemplate("Child", `<span>blue</span>`);
|
||||||
class Parent extends Widget {
|
class Parent extends Widget {
|
||||||
widgets = { Child: Child };
|
components = { Child: Child };
|
||||||
state = { display: true };
|
state = { display: true };
|
||||||
}
|
}
|
||||||
class Child extends Widget {}
|
class Child extends Widget {}
|
||||||
@@ -320,9 +317,7 @@ describe("animations", () => {
|
|||||||
expect(spans.length).toBe(1);
|
expect(spans.length).toBe(1);
|
||||||
expect(spans[0].className).toBe(`chimay-${phase} chimay-${phase}-active`);
|
expect(spans[0].className).toBe(`chimay-${phase} chimay-${phase}-active`);
|
||||||
cb();
|
cb();
|
||||||
expect(spans[0].className).toBe(
|
expect(spans[0].className).toBe(`chimay-${phase}-active chimay-${phase}-to`);
|
||||||
`chimay-${phase}-active chimay-${phase}-to`
|
|
||||||
);
|
|
||||||
def.resolve();
|
def.resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -330,9 +325,7 @@ describe("animations", () => {
|
|||||||
button!.click();
|
button!.click();
|
||||||
await def; // wait for the mocked repaint to be done
|
await def; // wait for the mocked repaint to be done
|
||||||
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||||
expect(fixture.innerHTML).toBe(
|
expect(fixture.innerHTML).toBe('<div><button>Toggle</button><span class="">blue</span></div>');
|
||||||
'<div><button>Toggle</button><span class="">blue</span></div>'
|
|
||||||
);
|
|
||||||
|
|
||||||
// click to remove the span, and click again to re-add it before transitionend
|
// click to remove the span, and click again to re-add it before transitionend
|
||||||
def = makeDeferred();
|
def = makeDeferred();
|
||||||
@@ -346,26 +339,24 @@ describe("animations", () => {
|
|||||||
|
|
||||||
await def; // wait for the mocked repaint to be done
|
await def; // wait for the mocked repaint to be done
|
||||||
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||||
expect(fixture.innerHTML).toBe(
|
expect(fixture.innerHTML).toBe('<div><button>Toggle</button><span class="">blue</span></div>');
|
||||||
'<div><button>Toggle</button><span class="">blue</span></div>'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("t-transition combined with t-widget, remove and re-add before transitionend", async () => {
|
test("t-transition combined with t-component, remove and re-add before transitionend", async () => {
|
||||||
expect.assertions(11);
|
expect.assertions(11);
|
||||||
|
|
||||||
env.qweb.addTemplates(
|
env.qweb.addTemplates(
|
||||||
`<templates>
|
`<templates>
|
||||||
<div t-name="Parent">
|
<div t-name="Parent">
|
||||||
<button t-on-click="toggle">Toggle</button>
|
<button t-on-click="toggle">Toggle</button>
|
||||||
<t t-if="state.flag" t-widget="Child" t-transition="chimay"/>
|
<t t-if="state.flag" t-component="Child" t-transition="chimay"/>
|
||||||
</div>
|
</div>
|
||||||
<span t-name="Child">blue</span>
|
<span t-name="Child">blue</span>
|
||||||
</templates>`
|
</templates>`
|
||||||
);
|
);
|
||||||
class Child extends Widget {}
|
class Child extends Widget {}
|
||||||
class Parent extends Widget {
|
class Parent extends Widget {
|
||||||
widgets = { Child };
|
components = { Child };
|
||||||
constructor(parent) {
|
constructor(parent) {
|
||||||
super(parent);
|
super(parent);
|
||||||
this.state = { flag: false };
|
this.state = { flag: false };
|
||||||
@@ -386,9 +377,7 @@ describe("animations", () => {
|
|||||||
expect(spans.length).toBe(1);
|
expect(spans.length).toBe(1);
|
||||||
expect(spans[0].className).toBe(`chimay-${phase} chimay-${phase}-active`);
|
expect(spans[0].className).toBe(`chimay-${phase} chimay-${phase}-active`);
|
||||||
cb();
|
cb();
|
||||||
expect(spans[0].className).toBe(
|
expect(spans[0].className).toBe(`chimay-${phase}-active chimay-${phase}-to`);
|
||||||
`chimay-${phase}-active chimay-${phase}-to`
|
|
||||||
);
|
|
||||||
def.resolve();
|
def.resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -396,9 +385,7 @@ describe("animations", () => {
|
|||||||
button!.click();
|
button!.click();
|
||||||
await def; // wait for the mocked repaint to be done
|
await def; // wait for the mocked repaint to be done
|
||||||
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||||
expect(fixture.innerHTML).toBe(
|
expect(fixture.innerHTML).toBe('<div><button>Toggle</button><span class="">blue</span></div>');
|
||||||
'<div><button>Toggle</button><span class="">blue</span></div>'
|
|
||||||
);
|
|
||||||
|
|
||||||
// click to remove the span, and click again to re-add it before transitionend
|
// click to remove the span, and click again to re-add it before transitionend
|
||||||
def = makeDeferred();
|
def = makeDeferred();
|
||||||
@@ -412,8 +399,6 @@ describe("animations", () => {
|
|||||||
|
|
||||||
await def; // wait for the mocked repaint to be done
|
await def; // wait for the mocked repaint to be done
|
||||||
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||||
expect(fixture.innerHTML).toBe(
|
expect(fixture.innerHTML).toBe('<div><button>Toggle</button><span class="">blue</span></div>');
|
||||||
'<div><button>Toggle</button><span class="">blue</span></div>'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`props validation props are validated in dev mode (code snapshot) 1`] = `
|
||||||
|
"function anonymous(context,extra
|
||||||
|
) {
|
||||||
|
let utils = this.constructor.utils;
|
||||||
|
let QWeb = this.constructor;
|
||||||
|
let parent = context;
|
||||||
|
let owner = context;
|
||||||
|
var h = this.h;
|
||||||
|
let c1 = [], p1 = {key:1};
|
||||||
|
var vn1 = h('div', p1, c1);
|
||||||
|
//COMPONENT
|
||||||
|
let def3;
|
||||||
|
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
||||||
|
let _2_index = c1.length;
|
||||||
|
c1.push(null);
|
||||||
|
let props4 = {message:1};
|
||||||
|
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
||||||
|
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
||||||
|
def3 = w4.__owl__.renderPromise;
|
||||||
|
} else {
|
||||||
|
w4.destroy();
|
||||||
|
w4 = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!w4) {
|
||||||
|
let componentKey4 = \`Child\`;
|
||||||
|
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
|
||||||
|
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
||||||
|
utils.validateProps(W4, props4)
|
||||||
|
w4 = new W4(parent, props4);
|
||||||
|
parent.__owl__.cmap[4] = w4.__owl__.id;
|
||||||
|
def3 = w4.__prepare();
|
||||||
|
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
||||||
|
} else {
|
||||||
|
utils.validateProps(w4.constructor, props4)
|
||||||
|
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
||||||
|
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
||||||
|
}
|
||||||
|
extra.promises.push(def3);
|
||||||
|
return vn1;
|
||||||
|
}"
|
||||||
|
`;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
import { Component, Env } from "../src/component";
|
import { Component, Env } from "../../src/component/component";
|
||||||
import { makeTestFixture, makeTestEnv } from "./helpers";
|
import { makeTestFixture, makeTestEnv } from "../helpers";
|
||||||
import { QWeb } from "../src";
|
import { QWeb } from "../../src/qweb";
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Setup and helpers
|
// Setup and helpers
|
||||||
@@ -45,19 +45,6 @@ describe("props validation", () => {
|
|||||||
}).not.toThrow();
|
}).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("props validation is also done on update props", async () => {
|
|
||||||
expect.assertions(1);
|
|
||||||
class TestWidget extends Widget {
|
|
||||||
static props = ["message"];
|
|
||||||
}
|
|
||||||
const w = new TestWidget(env, { message: "bottle" });
|
|
||||||
try {
|
|
||||||
await w._updateProps({});
|
|
||||||
} catch (e) {
|
|
||||||
expect(e.message).toBe("Missing props 'message' (widget 'TestWidget')");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("props: list of strings", async () => {
|
test("props: list of strings", async () => {
|
||||||
class TestWidget extends Widget {
|
class TestWidget extends Widget {
|
||||||
static props = ["message"];
|
static props = ["message"];
|
||||||
@@ -65,7 +52,7 @@ describe("props validation", () => {
|
|||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
new TestWidget(env);
|
new TestWidget(env);
|
||||||
}).toThrow("Missing props 'message' (widget 'TestWidget')");
|
}).toThrow("Missing props 'message' (component 'TestWidget')");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("validate simple types", async () => {
|
test("validate simple types", async () => {
|
||||||
@@ -93,7 +80,7 @@ describe("props validation", () => {
|
|||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
new TestWidget(env, { p: test.ko });
|
new TestWidget(env, { p: test.ko });
|
||||||
}).toThrow("Props 'p' of invalid type in widget");
|
}).toThrow("Props 'p' of invalid type in component");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -122,11 +109,10 @@ describe("props validation", () => {
|
|||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
new TestWidget(env, { p: test.ko });
|
new TestWidget(env, { p: test.ko });
|
||||||
}).toThrow("Props 'p' of invalid type in widget");
|
}).toThrow("Props 'p' of invalid type in component");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
test("can validate a prop with multiple types", async () => {
|
test("can validate a prop with multiple types", async () => {
|
||||||
let TestWidget = class extends Widget {
|
let TestWidget = class extends Widget {
|
||||||
static props = { p: [String, Boolean] };
|
static props = { p: [String, Boolean] };
|
||||||
@@ -139,7 +125,7 @@ describe("props validation", () => {
|
|||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
new TestWidget(env, { p: 1 });
|
new TestWidget(env, { p: 1 });
|
||||||
}).toThrow("Props 'p' of invalid type in widget");
|
}).toThrow("Props 'p' of invalid type in component");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can validate an optional props", async () => {
|
test("can validate an optional props", async () => {
|
||||||
@@ -155,10 +141,8 @@ describe("props validation", () => {
|
|||||||
expect(() => {
|
expect(() => {
|
||||||
new TestWidget(env, { p: 1 });
|
new TestWidget(env, { p: 1 });
|
||||||
}).toThrow();
|
}).toThrow();
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
test("can validate an array with given primitive type", async () => {
|
test("can validate an array with given primitive type", async () => {
|
||||||
let TestWidget = class extends Widget {
|
let TestWidget = class extends Widget {
|
||||||
static props = { p: { type: Array, element: String } };
|
static props = { p: { type: Array, element: String } };
|
||||||
@@ -222,7 +206,7 @@ describe("props validation", () => {
|
|||||||
type: Object,
|
type: Object,
|
||||||
shape: {
|
shape: {
|
||||||
id: Number,
|
id: Number,
|
||||||
url: [Boolean, {type: Array, element: Number}],
|
url: [Boolean, { type: Array, element: Number }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -236,15 +220,103 @@ describe("props validation", () => {
|
|||||||
expect(() => {
|
expect(() => {
|
||||||
new TestWidget(env, { p: { id: 1, url: [12, true] } });
|
new TestWidget(env, { p: { id: 1, url: [12, true] } });
|
||||||
}).toThrow();
|
}).toThrow();
|
||||||
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("props are validated in dev mode (code snapshot)", async () => {
|
||||||
|
env.qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="App">
|
||||||
|
<Child message="1"/>
|
||||||
|
</div>
|
||||||
|
<div t-name="Child"><t t-esc="props.message"/></div>
|
||||||
|
</templates>`);
|
||||||
|
|
||||||
|
class Child extends Widget {
|
||||||
|
static props = ["message"];
|
||||||
|
}
|
||||||
|
class App extends Widget {
|
||||||
|
components = { Child };
|
||||||
|
}
|
||||||
|
const app = new App(env);
|
||||||
|
await app.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
|
||||||
|
// need to make sure there are 2 call to update props. one at component
|
||||||
|
// creation, and one at update time.
|
||||||
|
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("props: list of strings with optional props", async () => {
|
||||||
|
class TestWidget extends Widget {
|
||||||
|
static props = ["message", "someProp?"];
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
QWeb.utils.validateProps(TestWidget, { someProp: 1 });
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
QWeb.utils.validateProps(TestWidget, { message: 1 });
|
||||||
|
}).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("props: can be defined with a boolean", async () => {
|
||||||
|
class TestWidget extends Widget {
|
||||||
|
static props = { message: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
QWeb.utils.validateProps(TestWidget, {});
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("props: extra props cause an error", async () => {
|
||||||
|
class TestWidget extends Widget {
|
||||||
|
static props = ["message"];
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
QWeb.utils.validateProps(TestWidget, { message: 1, flag: true });
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("props: extra props cause an error, part 2", async () => {
|
||||||
|
class TestWidget extends Widget {
|
||||||
|
static props = {message: true};
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
QWeb.utils.validateProps(TestWidget, { message: 1, flag: true });
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("props: optional prop do not cause an error", async () => {
|
||||||
|
class TestWidget extends Widget {
|
||||||
|
static props = ["message?"];
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
QWeb.utils.validateProps(TestWidget, { message: 1});
|
||||||
|
}).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("optional prop do not cause an error if value is undefined", async () => {
|
||||||
|
class TestWidget extends Widget {
|
||||||
|
static props = {message: {type: String, optional: true}};
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
QWeb.utils.validateProps(TestWidget, { message: undefined});
|
||||||
|
}).not.toThrow();
|
||||||
|
expect(() => {
|
||||||
|
QWeb.utils.validateProps(TestWidget, { message: null});
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
describe("default props", () => {
|
describe("default props", () => {
|
||||||
test("can set default values", async () => {
|
test("can set default values", async () => {
|
||||||
class TestWidget extends Widget {
|
class TestWidget extends Widget {
|
||||||
static defaultProps = {p: 4}
|
static defaultProps = { p: 4 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const w = new TestWidget(env, {});
|
const w = new TestWidget(env, {});
|
||||||
@@ -253,11 +325,11 @@ describe("default props", () => {
|
|||||||
|
|
||||||
test("default values are also set whenever component is updated", async () => {
|
test("default values are also set whenever component is updated", async () => {
|
||||||
class TestWidget extends Widget {
|
class TestWidget extends Widget {
|
||||||
static defaultProps = {p: 4}
|
static defaultProps = { p: 4 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const w = new TestWidget(env, { p: 1 });
|
const w = new TestWidget(env, { p: 1 });
|
||||||
await w._updateProps({});
|
await w.__updateProps({});
|
||||||
expect(w.props.p).toBe(4);
|
expect(w.props.p).toBe(4);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { EventBus } from "../src/event_bus";
|
import { EventBus } from "../../src/core/event_bus";
|
||||||
|
|
||||||
describe("event bus behaviour", () => {
|
describe("event bus behaviour", () => {
|
||||||
test("can subscribe and be notified", () => {
|
test("can subscribe and be notified", () => {
|
||||||
@@ -23,9 +23,7 @@ describe("event bus behaviour", () => {
|
|||||||
test("throw error if callback is undefined", () => {
|
test("throw error if callback is undefined", () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
const bus = new EventBus();
|
const bus = new EventBus();
|
||||||
expect(() => bus.on("event", {}, <any>undefined)).toThrow(
|
expect(() => bus.on("event", {}, <any>undefined)).toThrow(`Missing callback`);
|
||||||
`Missing callback`
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can unsubscribe", () => {
|
test("can unsubscribe", () => {
|
||||||
@@ -0,0 +1,466 @@
|
|||||||
|
import { Observer } from "../../src/core/observer";
|
||||||
|
import { nextMicroTick } from "../helpers";
|
||||||
|
|
||||||
|
describe("observer", () => {
|
||||||
|
test("properly observe objects", () => {
|
||||||
|
const observer = new Observer();
|
||||||
|
const obj: any = observer.observe({});
|
||||||
|
|
||||||
|
expect(typeof obj).toBe("object");
|
||||||
|
expect(observer.revNumber(obj)).toBe(1);
|
||||||
|
expect(observer.deepRevNumber(obj)).toBe(1);
|
||||||
|
expect(observer.rev).toBe(1);
|
||||||
|
|
||||||
|
const obj2: any = observer.observe({ a: 1 });
|
||||||
|
expect(observer.revNumber(obj2)).toBe(1);
|
||||||
|
expect(observer.revNumber(obj)).toBe(1);
|
||||||
|
expect(observer.deepRevNumber(obj)).toBe(1);
|
||||||
|
expect(observer.rev).toBe(1);
|
||||||
|
|
||||||
|
obj2.a = 2;
|
||||||
|
|
||||||
|
expect(observer.revNumber(obj)).toBe(1);
|
||||||
|
expect(observer.revNumber(obj2)).toBe(2);
|
||||||
|
expect(observer.rev).toBe(2);
|
||||||
|
|
||||||
|
expect(obj2).toEqual({
|
||||||
|
a: 2
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("properly handle null or undefined", () => {
|
||||||
|
const observer = new Observer();
|
||||||
|
const obj: any = observer.observe({ a: null, b: undefined });
|
||||||
|
|
||||||
|
expect(observer.revNumber(obj)).toBe(1);
|
||||||
|
expect(observer.deepRevNumber(obj)).toBe(1);
|
||||||
|
expect(observer.rev).toBe(1);
|
||||||
|
|
||||||
|
obj.a = 3;
|
||||||
|
expect(observer.revNumber(obj)).toBe(2);
|
||||||
|
expect(observer.deepRevNumber(obj)).toBe(2);
|
||||||
|
expect(observer.rev).toBe(2);
|
||||||
|
|
||||||
|
obj.b = 5;
|
||||||
|
expect(observer.revNumber(obj)).toBe(3);
|
||||||
|
expect(observer.deepRevNumber(obj)).toBe(3);
|
||||||
|
expect(observer.rev).toBe(3);
|
||||||
|
|
||||||
|
obj.a = null;
|
||||||
|
obj.b = undefined;
|
||||||
|
expect(observer.revNumber(obj)).toBe(5);
|
||||||
|
expect(observer.deepRevNumber(obj)).toBe(5);
|
||||||
|
expect(observer.rev).toBe(5);
|
||||||
|
expect(obj).toEqual({
|
||||||
|
a: null,
|
||||||
|
b: undefined
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("properly handle dates", () => {
|
||||||
|
const observer = new Observer();
|
||||||
|
const date = new Date();
|
||||||
|
const obj: any = observer.observe({ date });
|
||||||
|
|
||||||
|
expect(observer.revNumber(obj)).toBe(1);
|
||||||
|
expect(observer.deepRevNumber(obj)).toBe(1);
|
||||||
|
expect(observer.rev).toBe(1);
|
||||||
|
expect(typeof obj.date.getFullYear()).toBe('number');
|
||||||
|
expect(obj.date).toBe(date);
|
||||||
|
|
||||||
|
obj.date = new Date();
|
||||||
|
|
||||||
|
expect(observer.revNumber(obj)).toBe(2);
|
||||||
|
expect(observer.deepRevNumber(obj)).toBe(2);
|
||||||
|
expect(observer.rev).toBe(2);
|
||||||
|
expect(obj.date).not.toBe(date);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can change values in array", () => {
|
||||||
|
const observer = new Observer();
|
||||||
|
const obj: any = observer.observe({ arr: [1, 2] });
|
||||||
|
|
||||||
|
expect(Array.isArray(obj.arr)).toBe(true);
|
||||||
|
expect(observer.revNumber(obj.arr)).toBe(1);
|
||||||
|
expect(observer.deepRevNumber(obj.arr)).toBe(1);
|
||||||
|
expect(observer.rev).toBe(1);
|
||||||
|
|
||||||
|
obj.arr[0] = "nope";
|
||||||
|
expect(observer.revNumber(obj.arr)).toBe(2);
|
||||||
|
expect(observer.deepRevNumber(obj.arr)).toBe(2);
|
||||||
|
expect(observer.revNumber(obj)).toBe(1);
|
||||||
|
expect(observer.deepRevNumber(obj)).toBe(2);
|
||||||
|
expect(observer.rev).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("various object property changes", () => {
|
||||||
|
const observer = new Observer();
|
||||||
|
const obj: any = observer.observe({ a: 1 });
|
||||||
|
|
||||||
|
expect(observer.revNumber(obj)).toBe(1);
|
||||||
|
expect(observer.deepRevNumber(obj)).toBe(1);
|
||||||
|
expect(observer.rev).toBe(1);
|
||||||
|
|
||||||
|
obj.a = 2;
|
||||||
|
|
||||||
|
expect(observer.revNumber(obj)).toBe(2);
|
||||||
|
expect(observer.deepRevNumber(obj)).toBe(2);
|
||||||
|
expect(observer.rev).toBe(2);
|
||||||
|
|
||||||
|
// same value again
|
||||||
|
obj.a = 2;
|
||||||
|
expect(observer.revNumber(obj)).toBe(2);
|
||||||
|
expect(observer.deepRevNumber(obj)).toBe(2);
|
||||||
|
expect(observer.rev).toBe(2);
|
||||||
|
|
||||||
|
obj.a = 3;
|
||||||
|
expect(observer.revNumber(obj)).toBe(3);
|
||||||
|
expect(observer.deepRevNumber(obj)).toBe(3);
|
||||||
|
expect(observer.rev).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("properly observe arrays", () => {
|
||||||
|
const observer = new Observer();
|
||||||
|
const arr: any = observer.observe([]);
|
||||||
|
|
||||||
|
expect(Array.isArray(arr)).toBe(true);
|
||||||
|
expect(arr.length).toBe(0);
|
||||||
|
expect(observer.revNumber(arr)).toBe(1);
|
||||||
|
expect(observer.deepRevNumber(arr)).toBe(1);
|
||||||
|
expect(observer.rev).toBe(1);
|
||||||
|
|
||||||
|
arr.push(1);
|
||||||
|
expect(observer.revNumber(arr)).toBe(2);
|
||||||
|
expect(observer.deepRevNumber(arr)).toBe(2);
|
||||||
|
expect(observer.rev).toBe(2);
|
||||||
|
expect(arr.length).toBe(1);
|
||||||
|
expect(arr).toEqual([1]);
|
||||||
|
|
||||||
|
arr.splice(1, 0, "hey");
|
||||||
|
expect(observer.revNumber(arr)).toBe(3);
|
||||||
|
expect(observer.deepRevNumber(arr)).toBe(3);
|
||||||
|
expect(observer.rev).toBe(3);
|
||||||
|
expect(arr).toEqual([1, "hey"]);
|
||||||
|
expect(arr.length).toBe(2);
|
||||||
|
|
||||||
|
arr.unshift("lindemans");
|
||||||
|
//it generates 3 primitive operations
|
||||||
|
expect(observer.revNumber(arr)).toBe(6);
|
||||||
|
expect(observer.deepRevNumber(arr)).toBe(6);
|
||||||
|
expect(observer.rev).toBe(6);
|
||||||
|
expect(arr).toEqual(["lindemans", 1, "hey"]);
|
||||||
|
expect(arr.length).toBe(3);
|
||||||
|
|
||||||
|
arr.reverse();
|
||||||
|
//it generates 2 primitive operations
|
||||||
|
expect(observer.revNumber(arr)).toBe(8);
|
||||||
|
expect(observer.deepRevNumber(arr)).toBe(8);
|
||||||
|
expect(observer.rev).toBe(8);
|
||||||
|
expect(arr).toEqual(["hey", 1, "lindemans"]);
|
||||||
|
expect(arr.length).toBe(3);
|
||||||
|
|
||||||
|
arr.pop(); // one set, one delete
|
||||||
|
expect(observer.revNumber(arr)).toBe(10);
|
||||||
|
expect(observer.deepRevNumber(arr)).toBe(10);
|
||||||
|
expect(observer.rev).toBe(10);
|
||||||
|
expect(arr).toEqual(["hey", 1]);
|
||||||
|
expect(arr.length).toBe(2);
|
||||||
|
|
||||||
|
arr.shift(); // 2 sets, 1 delete
|
||||||
|
expect(observer.revNumber(arr)).toBe(13);
|
||||||
|
expect(observer.deepRevNumber(arr)).toBe(13);
|
||||||
|
expect(observer.rev).toBe(13);
|
||||||
|
expect(arr).toEqual([1]);
|
||||||
|
expect(arr.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("object pushed into arrays are observed", () => {
|
||||||
|
const observer = new Observer();
|
||||||
|
const arr: any = observer.observe([]);
|
||||||
|
expect(observer.rev).toBe(1);
|
||||||
|
|
||||||
|
arr.push({ kriek: 5 });
|
||||||
|
expect(observer.rev).toBe(2);
|
||||||
|
expect(observer.revNumber(arr)).toBe(2);
|
||||||
|
expect(observer.revNumber(arr[0])).toBe(2);
|
||||||
|
|
||||||
|
arr[0].kriek = 6;
|
||||||
|
|
||||||
|
expect(observer.rev).toBe(3);
|
||||||
|
expect(observer.revNumber(arr)).toBe(2);
|
||||||
|
expect(observer.deepRevNumber(arr)).toBe(3);
|
||||||
|
expect(observer.revNumber(arr[0])).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("set new property on observed object", async () => {
|
||||||
|
const observer = new Observer();
|
||||||
|
observer.notifyCB = jest.fn();
|
||||||
|
const state: any = observer.observe({ a: 1 });
|
||||||
|
expect(observer.notifyCB).toBeCalledTimes(0);
|
||||||
|
expect(observer.rev).toBe(1);
|
||||||
|
expect(observer.revNumber(state)).toBe(1);
|
||||||
|
|
||||||
|
state.b = 8;
|
||||||
|
|
||||||
|
await nextMicroTick();
|
||||||
|
|
||||||
|
expect(observer.notifyCB).toBeCalledTimes(1);
|
||||||
|
expect(observer.rev).toBe(2);
|
||||||
|
expect(observer.revNumber(state)).toBe(2);
|
||||||
|
|
||||||
|
expect(state.b).toBe(8);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("delete property from observed object", async () => {
|
||||||
|
const observer = new Observer();
|
||||||
|
observer.notifyCB = jest.fn();
|
||||||
|
const state: any = observer.observe({ a: 1, b: 8 });
|
||||||
|
observer.observe(state);
|
||||||
|
|
||||||
|
expect(observer.notifyCB).toBeCalledTimes(0);
|
||||||
|
expect(observer.rev).toBe(1);
|
||||||
|
expect(observer.revNumber(state)).toBe(1);
|
||||||
|
|
||||||
|
delete state.b;
|
||||||
|
await nextMicroTick();
|
||||||
|
|
||||||
|
expect(observer.notifyCB).toBeCalledTimes(1);
|
||||||
|
expect(observer.rev).toBe(2);
|
||||||
|
expect(observer.revNumber(state)).toBe(2);
|
||||||
|
|
||||||
|
expect(state).toEqual({ a: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("set element in observed array", async () => {
|
||||||
|
const observer = new Observer();
|
||||||
|
observer.notifyCB = jest.fn();
|
||||||
|
const state: any = observer.observe(["a"]);
|
||||||
|
|
||||||
|
expect(observer.rev).toBe(1);
|
||||||
|
expect(observer.revNumber(state)).toBe(1);
|
||||||
|
expect(observer.deepRevNumber(state)).toBe(1);
|
||||||
|
expect(observer.notifyCB).toBeCalledTimes(0);
|
||||||
|
|
||||||
|
state[1] = "b";
|
||||||
|
|
||||||
|
await nextMicroTick();
|
||||||
|
|
||||||
|
expect(observer.rev).toBe(2);
|
||||||
|
expect(observer.revNumber(state)).toBe(2);
|
||||||
|
expect(observer.deepRevNumber(state)).toBe(2);
|
||||||
|
expect(observer.notifyCB).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
expect(state).toEqual(["a", "b"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("properly observe arrays in object", () => {
|
||||||
|
const observer = new Observer();
|
||||||
|
const state: any = observer.observe({ arr: [] });
|
||||||
|
|
||||||
|
expect(observer.rev).toBe(1);
|
||||||
|
expect(observer.revNumber(state.arr)).toBe(1);
|
||||||
|
expect(observer.deepRevNumber(state.arr)).toBe(1);
|
||||||
|
expect(state.arr.length).toBe(0);
|
||||||
|
|
||||||
|
state.arr.push(1);
|
||||||
|
expect(observer.rev).toBe(2);
|
||||||
|
expect(observer.revNumber(state.arr)).toBe(2);
|
||||||
|
expect(observer.deepRevNumber(state.arr)).toBe(2);
|
||||||
|
expect(state.arr.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("properly observe objects in array", () => {
|
||||||
|
const observer = new Observer();
|
||||||
|
const state: any = observer.observe({ arr: [{ something: 1 }] });
|
||||||
|
observer.observe(state);
|
||||||
|
|
||||||
|
expect(observer.rev).toBe(1);
|
||||||
|
expect(observer.revNumber(state.arr)).toBe(1);
|
||||||
|
expect(observer.revNumber(state.arr[0])).toBe(1);
|
||||||
|
|
||||||
|
state.arr[0].something = 2;
|
||||||
|
expect(observer.rev).toBe(2);
|
||||||
|
expect(observer.revNumber(state.arr)).toBe(1);
|
||||||
|
expect(observer.revNumber(state.arr[0])).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("properly observe objects in object", () => {
|
||||||
|
const observer = new Observer();
|
||||||
|
const state: any = observer.observe({ a: { b: 1 } });
|
||||||
|
|
||||||
|
expect(observer.rev).toBe(1);
|
||||||
|
expect(observer.revNumber(state)).toBe(1);
|
||||||
|
expect(observer.revNumber(state.a)).toBe(1);
|
||||||
|
|
||||||
|
state.a.b = 2;
|
||||||
|
expect(observer.rev).toBe(2);
|
||||||
|
expect(observer.revNumber(state)).toBe(1);
|
||||||
|
expect(observer.revNumber(state.a)).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("reobserve new object values", () => {
|
||||||
|
const observer = new Observer();
|
||||||
|
const obj: any = observer.observe({ a: 1 });
|
||||||
|
|
||||||
|
expect(observer.rev).toBe(1);
|
||||||
|
expect(observer.revNumber(obj)).toBe(1);
|
||||||
|
|
||||||
|
obj.a = { b: 2 };
|
||||||
|
|
||||||
|
expect(observer.rev).toBe(2);
|
||||||
|
expect(observer.revNumber(obj)).toBe(2);
|
||||||
|
expect(observer.revNumber(obj.a)).toBe(2);
|
||||||
|
obj.a.b = 3;
|
||||||
|
expect(observer.rev).toBe(3);
|
||||||
|
expect(observer.revNumber(obj)).toBe(2);
|
||||||
|
expect(observer.revNumber(obj.a)).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("deep observe misc changes", () => {
|
||||||
|
const observer = new Observer();
|
||||||
|
const state: any = observer.observe({ o: { a: 1 }, arr: [1], n: 13 });
|
||||||
|
expect(observer.revNumber(state)).toBe(1);
|
||||||
|
expect(observer.deepRevNumber(state)).toBe(1);
|
||||||
|
|
||||||
|
state.o.a = 2;
|
||||||
|
expect(observer.rev).toBe(2);
|
||||||
|
expect(observer.revNumber(state)).toBe(1);
|
||||||
|
expect(observer.deepRevNumber(state)).toBe(2);
|
||||||
|
|
||||||
|
state.arr.push(2);
|
||||||
|
expect(observer.rev).toBe(3);
|
||||||
|
expect(observer.revNumber(state)).toBe(1);
|
||||||
|
expect(observer.deepRevNumber(state)).toBe(3);
|
||||||
|
|
||||||
|
state.n = 155;
|
||||||
|
expect(observer.rev).toBe(4);
|
||||||
|
expect(observer.revNumber(state)).toBe(2);
|
||||||
|
expect(observer.deepRevNumber(state)).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("properly handle already observed state", () => {
|
||||||
|
const observer = new Observer();
|
||||||
|
const obj1: any = observer.observe({ a: 1 });
|
||||||
|
const obj2: any = observer.observe({ b: 1 });
|
||||||
|
|
||||||
|
expect(observer.revNumber(obj1)).toBe(1);
|
||||||
|
expect(observer.revNumber(obj2)).toBe(1);
|
||||||
|
|
||||||
|
obj1.a = 2;
|
||||||
|
obj2.b = 3;
|
||||||
|
expect(observer.revNumber(obj1)).toBe(2);
|
||||||
|
expect(observer.revNumber(obj2)).toBe(2);
|
||||||
|
|
||||||
|
obj2.b = obj1;
|
||||||
|
expect(observer.revNumber(obj1)).toBe(2);
|
||||||
|
expect(observer.revNumber(obj2)).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can set a property more than once", () => {
|
||||||
|
const observer = new Observer();
|
||||||
|
const obj: any = observer.observe({});
|
||||||
|
|
||||||
|
expect(observer.revNumber(obj)).toBe(1);
|
||||||
|
expect(observer.deepRevNumber(obj)).toBe(1);
|
||||||
|
expect(observer.rev).toBe(1);
|
||||||
|
|
||||||
|
obj.aku = "always finds annoying problems";
|
||||||
|
expect(observer.revNumber(obj)).toBe(2);
|
||||||
|
expect(observer.deepRevNumber(obj)).toBe(2);
|
||||||
|
expect(observer.rev).toBe(2);
|
||||||
|
|
||||||
|
obj.aku = "always finds good problems";
|
||||||
|
|
||||||
|
expect(observer.revNumber(obj)).toBe(3);
|
||||||
|
expect(observer.deepRevNumber(obj)).toBe(3);
|
||||||
|
expect(observer.rev).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("properly handle swapping elements", () => {
|
||||||
|
const observer = new Observer();
|
||||||
|
const obj: any = observer.observe({ a: { arr: [] }, b: 1 });
|
||||||
|
|
||||||
|
// swap a and b
|
||||||
|
const b = obj.b;
|
||||||
|
obj.b = obj.a;
|
||||||
|
obj.a = b;
|
||||||
|
expect(observer.rev).toBe(3);
|
||||||
|
|
||||||
|
// push something into array to make sure it works
|
||||||
|
obj.b.arr.push("blanche");
|
||||||
|
expect(observer.rev).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("properly handle assigning observed obj containing array", () => {
|
||||||
|
const observer = new Observer();
|
||||||
|
const obj: any = observer.observe({ a: { arr: [], val: "test" } });
|
||||||
|
|
||||||
|
expect(observer.rev).toBe(1);
|
||||||
|
obj.a = { ...obj.a, val: "test2" };
|
||||||
|
expect(observer.rev).toBe(2);
|
||||||
|
|
||||||
|
// push something into array to make sure it works
|
||||||
|
obj.a.arr.push("blanche");
|
||||||
|
expect(observer.rev).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("accept cycles in observed state", () => {
|
||||||
|
const observer = new Observer();
|
||||||
|
let obj1: any = {};
|
||||||
|
let obj2: any = { b: obj1, key: 1 };
|
||||||
|
obj1.a = obj2;
|
||||||
|
obj1 = observer.observe(obj1);
|
||||||
|
obj2 = obj1.a;
|
||||||
|
|
||||||
|
expect(observer.revNumber(obj1)).toBe(1);
|
||||||
|
expect(observer.revNumber(obj2)).toBe(1);
|
||||||
|
|
||||||
|
obj2.key = 3;
|
||||||
|
expect(observer.revNumber(obj1)).toBe(1);
|
||||||
|
expect(observer.revNumber(obj2)).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("call callback when state is changed", async () => {
|
||||||
|
const observer = new Observer();
|
||||||
|
observer.notifyCB = jest.fn();
|
||||||
|
const obj: any = observer.observe({ a: 1, b: { c: 2 }, d: [{ e: 3 }], f: 4 });
|
||||||
|
|
||||||
|
expect(observer.notifyCB).toBeCalledTimes(0);
|
||||||
|
|
||||||
|
obj.a = 2;
|
||||||
|
await nextMicroTick();
|
||||||
|
expect(observer.notifyCB).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
obj.b.c = 3;
|
||||||
|
await nextMicroTick();
|
||||||
|
expect(observer.notifyCB).toBeCalledTimes(2);
|
||||||
|
|
||||||
|
obj.d[0].e = 5;
|
||||||
|
await nextMicroTick();
|
||||||
|
expect(observer.notifyCB).toBeCalledTimes(3);
|
||||||
|
|
||||||
|
obj.a = 111;
|
||||||
|
obj.f = 222;
|
||||||
|
await nextMicroTick();
|
||||||
|
expect(observer.notifyCB).toBeCalledTimes(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throw error when state is mutated in object if allowMutation=false", async () => {
|
||||||
|
const observer = new Observer();
|
||||||
|
observer.allowMutations = false;
|
||||||
|
const obj: any = observer.observe({ a: 1 });
|
||||||
|
expect(() => {
|
||||||
|
obj.a = 2;
|
||||||
|
}).toThrow('Observed state cannot be changed here! (key: "a", val: "2")');
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throw error when state is mutated in array if allowMutation=false", async () => {
|
||||||
|
const observer = new Observer();
|
||||||
|
observer.allowMutations = false;
|
||||||
|
const obj: any = observer.observe({ a: [1] });
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
obj.a.push(2);
|
||||||
|
}).toThrow('Observed state cannot be changed here! (key: "1", val: "2")');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -52,14 +52,10 @@ interface FileData {
|
|||||||
sections: MarkDownSection[];
|
sections: MarkDownSection[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function isLinkValid(
|
function isLinkValid(link: MarkDownLink, current: FileData, files: FileData[]): boolean {
|
||||||
link: MarkDownLink,
|
|
||||||
current: FileData,
|
|
||||||
files: FileData[]
|
|
||||||
): boolean {
|
|
||||||
const parts = link.link.split("#");
|
const parts = link.link.split("#");
|
||||||
const currentParts = current.name.split("/");
|
const currentParts = current.name.split("/");
|
||||||
const path = currentParts.length > 1 ? currentParts[0] + '/' : "";
|
const path = currentParts.length > 1 ? currentParts[0] + "/" : "";
|
||||||
const fullName = path + parts[0];
|
const fullName = path + parts[0];
|
||||||
if (parts.length === 1) {
|
if (parts.length === 1) {
|
||||||
// no # in url
|
// no # in url
|
||||||
@@ -70,8 +66,7 @@ function isLinkValid(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const file =
|
const file = parts[0] === "" ? current : files.find(f => f.name === fullName);
|
||||||
parts[0] === "" ? current : files.find(f => f.name === fullName);
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-17
@@ -1,8 +1,9 @@
|
|||||||
import { Env } from "../src/component";
|
import { Env } from "../src/component/component";
|
||||||
import { EvalContext, QWeb, UTILS } from "../src/qweb_core";
|
import { EvalContext, QWeb } from "../src/qweb/qweb";
|
||||||
import { patch } from "../src/vdom";
|
import { patch } from "../src/vdom";
|
||||||
import "../src/qweb_directives";
|
import "../src/qweb/base_directives";
|
||||||
import "../src/qweb_extensions";
|
import "../src/qweb/extensions";
|
||||||
|
import "../src/component/directive";
|
||||||
|
|
||||||
export function nextMicroTick(): Promise<void> {
|
export function nextMicroTick(): Promise<void> {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
@@ -68,34 +69,40 @@ export function renderToDOM(
|
|||||||
return result.elm as HTMLElement;
|
return result.elm as HTMLElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderToString(
|
/**
|
||||||
qweb: QWeb,
|
* Render a template to an html string. The big difference with the
|
||||||
t: string,
|
* renderToString method in QWeb is that we use the renderToDom method, which
|
||||||
context: EvalContext = {}
|
* snapshots the resulting template function. Doing so gives us a large body
|
||||||
): string {
|
* of reference to evaluate changes in the generated QWeb code.
|
||||||
|
*
|
||||||
|
* Note that the result of renderToString is guaranteed to be the same as the
|
||||||
|
* one from QWeb.
|
||||||
|
*/
|
||||||
|
export function renderToString(qweb: QWeb, t: string, context: EvalContext = {}): string {
|
||||||
const node = renderToDOM(qweb, t, context);
|
const node = renderToDOM(qweb, t, context);
|
||||||
return node instanceof Text ? node.textContent! : node.outerHTML;
|
const result = node instanceof Text ? node.textContent! : node.outerHTML;
|
||||||
|
if (result !== qweb.renderToString(t, context)) {
|
||||||
|
throw new Error("HTML string returned by renderToString helper does not match QWeb render");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// hereafter, we define two helpers to patch/unpatch the nextFrame utils. This
|
// hereafter, we define two helpers to patch/unpatch the nextFrame utils. This
|
||||||
// is useful for animations tests, as we hook before repaints to trigger
|
// is useful for animations tests, as we hook before repaints to trigger
|
||||||
// animations (thanks to requestAnimationFrame). Patching nextFrame allows to
|
// animations (thanks to requestAnimationFrame). Patching nextFrame allows to
|
||||||
// simulate calls to this hook. One must not forget to unpatch afterwards.
|
// simulate calls to this hook. One must not forget to unpatch afterwards.
|
||||||
let nextFrame = UTILS.nextFrame;
|
let nextFrame = QWeb.utils.nextFrame;
|
||||||
export function patchNextFrame(f: Function) {
|
export function patchNextFrame(f: Function) {
|
||||||
UTILS.nextFrame = (cb: () => void) => {
|
QWeb.utils.nextFrame = (cb: () => void) => {
|
||||||
setTimeout(() => f(cb));
|
setTimeout(() => f(cb));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unpatchNextFrame() {
|
export function unpatchNextFrame() {
|
||||||
UTILS.nextFrame = nextFrame;
|
QWeb.utils.nextFrame = nextFrame;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function editInput(
|
export async function editInput(input: HTMLInputElement | HTMLTextAreaElement, value: string) {
|
||||||
input: HTMLInputElement | HTMLTextAreaElement,
|
|
||||||
value: string
|
|
||||||
) {
|
|
||||||
input.value = value;
|
input.value = value;
|
||||||
input.dispatchEvent(new Event("input"));
|
input.dispatchEvent(new Event("input"));
|
||||||
input.dispatchEvent(new Event("change"));
|
input.dispatchEvent(new Event("change"));
|
||||||
|
|||||||
@@ -1,394 +0,0 @@
|
|||||||
import { Observer } from "../src/observer";
|
|
||||||
import { nextMicroTick } from "./helpers";
|
|
||||||
|
|
||||||
describe("observer", () => {
|
|
||||||
test("properly observe objects", () => {
|
|
||||||
const observer = new Observer();
|
|
||||||
const obj: any = {};
|
|
||||||
|
|
||||||
observer.observe(obj);
|
|
||||||
expect(obj.__owl__.rev).toBe(1);
|
|
||||||
expect(observer.rev).toBe(1);
|
|
||||||
|
|
||||||
const ob2: any = { a: 1 };
|
|
||||||
observer.observe(ob2);
|
|
||||||
expect(ob2.__owl__.rev).toBe(1);
|
|
||||||
ob2.a = 2;
|
|
||||||
expect(observer.rev).toBe(2);
|
|
||||||
expect(ob2.__owl__.rev).toBe(2);
|
|
||||||
|
|
||||||
ob2.b = 3;
|
|
||||||
expect(observer.rev).toBe(2);
|
|
||||||
expect(ob2.__owl__.rev).toBe(2);
|
|
||||||
|
|
||||||
observer.set(ob2, "b", 4);
|
|
||||||
expect(observer.rev).toBe(3);
|
|
||||||
expect(ob2.__owl__.rev).toBe(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("properly handle null or undefined", () => {
|
|
||||||
const observer = new Observer();
|
|
||||||
const obj: any = { a: null, b: undefined };
|
|
||||||
|
|
||||||
observer.observe(obj);
|
|
||||||
expect(obj.__owl__.rev).toBe(1);
|
|
||||||
expect(observer.rev).toBe(1);
|
|
||||||
|
|
||||||
obj.a = 3;
|
|
||||||
expect(obj.__owl__.rev).toBe(2);
|
|
||||||
|
|
||||||
obj.b = 5;
|
|
||||||
expect(obj.__owl__.rev).toBe(3);
|
|
||||||
|
|
||||||
obj.a = null;
|
|
||||||
obj.b = undefined;
|
|
||||||
expect(obj.__owl__.rev).toBe(5);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("can change values in array", () => {
|
|
||||||
const observer = new Observer();
|
|
||||||
const obj: any = { arr: [1, 2] };
|
|
||||||
|
|
||||||
observer.observe(obj);
|
|
||||||
expect(obj.arr.__owl__.rev).toBe(1);
|
|
||||||
expect(observer.rev).toBe(1);
|
|
||||||
|
|
||||||
obj.arr[0] = "nope";
|
|
||||||
expect(obj.arr.__owl__.rev).toBe(1);
|
|
||||||
expect(observer.rev).toBe(1);
|
|
||||||
|
|
||||||
observer.set(obj.arr, 0, "yep");
|
|
||||||
expect(obj.arr.__owl__.rev).toBe(2);
|
|
||||||
expect(observer.rev).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("various object property changes", () => {
|
|
||||||
const observer = new Observer();
|
|
||||||
const obj: any = { a: 1 };
|
|
||||||
observer.observe(obj);
|
|
||||||
expect(obj.__owl__.rev).toBe(1);
|
|
||||||
obj.a = 2;
|
|
||||||
expect(observer.rev).toBe(2);
|
|
||||||
expect(obj.__owl__.rev).toBe(2);
|
|
||||||
|
|
||||||
// same value again
|
|
||||||
obj.a = 2;
|
|
||||||
expect(observer.rev).toBe(2);
|
|
||||||
expect(obj.__owl__.rev).toBe(2);
|
|
||||||
|
|
||||||
obj.a = 3;
|
|
||||||
expect(observer.rev).toBe(3);
|
|
||||||
expect(obj.__owl__.rev).toBe(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("properly observe arrays", () => {
|
|
||||||
const observer = new Observer();
|
|
||||||
const arr: any = [];
|
|
||||||
observer.observe(arr);
|
|
||||||
expect(arr.__owl__.rev).toBe(1);
|
|
||||||
expect(observer.rev).toBe(1);
|
|
||||||
expect(arr.length).toBe(0);
|
|
||||||
|
|
||||||
arr.push(1);
|
|
||||||
expect(arr.__owl__.rev).toBe(2);
|
|
||||||
expect(observer.rev).toBe(2);
|
|
||||||
expect(arr.length).toBe(1);
|
|
||||||
|
|
||||||
arr.splice(1, 0, "hey");
|
|
||||||
expect(arr.__owl__.rev).toBe(3);
|
|
||||||
expect(observer.rev).toBe(3);
|
|
||||||
expect(arr.length).toBe(2);
|
|
||||||
|
|
||||||
arr.unshift("lindemans");
|
|
||||||
expect(arr.__owl__.rev).toBe(4);
|
|
||||||
|
|
||||||
arr.reverse();
|
|
||||||
expect(arr.__owl__.rev).toBe(5);
|
|
||||||
|
|
||||||
arr.pop();
|
|
||||||
expect(arr.__owl__.rev).toBe(6);
|
|
||||||
|
|
||||||
arr.shift();
|
|
||||||
expect(arr.__owl__.rev).toBe(7);
|
|
||||||
|
|
||||||
arr.sort();
|
|
||||||
expect(arr.__owl__.rev).toBe(8);
|
|
||||||
|
|
||||||
expect(arr).toEqual([1]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("object pushed into arrays are observed", () => {
|
|
||||||
const observer = new Observer();
|
|
||||||
const arr: any = [];
|
|
||||||
observer.observe(arr);
|
|
||||||
expect(observer.rev).toBe(1);
|
|
||||||
|
|
||||||
arr.push({ kriek: 5 });
|
|
||||||
expect(observer.rev).toBe(2);
|
|
||||||
expect(arr.__owl__.rev).toBe(2);
|
|
||||||
expect(arr[0].__owl__.rev).toBe(2);
|
|
||||||
|
|
||||||
arr[0].kriek = 6;
|
|
||||||
expect(observer.rev).toBe(3);
|
|
||||||
expect(arr.__owl__.rev).toBe(2);
|
|
||||||
expect(arr[0].__owl__.rev).toBe(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("set new property on observed object", async () => {
|
|
||||||
const observer = new Observer();
|
|
||||||
observer.notifyCB = jest.fn();
|
|
||||||
const state: any = { a: 1 };
|
|
||||||
observer.observe(state);
|
|
||||||
expect(state.__owl__.rev).toBe(1);
|
|
||||||
expect(observer.rev).toBe(1);
|
|
||||||
expect(observer.notifyCB).toBeCalledTimes(0);
|
|
||||||
|
|
||||||
Observer.set(state, "b", 8);
|
|
||||||
await nextMicroTick();
|
|
||||||
expect(state.__owl__.rev).toBe(2);
|
|
||||||
expect(observer.rev).toBe(2);
|
|
||||||
expect(observer.notifyCB).toBeCalledTimes(1);
|
|
||||||
expect(state.b).toBe(8);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("delete property from observed object", async () => {
|
|
||||||
const observer = new Observer();
|
|
||||||
observer.notifyCB = jest.fn();
|
|
||||||
const state: any = { a: 1, b: 8 };
|
|
||||||
observer.observe(state);
|
|
||||||
expect(state.__owl__.rev).toBe(1);
|
|
||||||
expect(observer.rev).toBe(1);
|
|
||||||
expect(observer.notifyCB).toBeCalledTimes(0);
|
|
||||||
|
|
||||||
Observer.delete(state, "b");
|
|
||||||
await nextMicroTick();
|
|
||||||
expect(state.__owl__.rev).toBe(2);
|
|
||||||
expect(observer.rev).toBe(2);
|
|
||||||
expect(observer.notifyCB).toBeCalledTimes(1);
|
|
||||||
expect(state).toEqual({ a: 1 });
|
|
||||||
});
|
|
||||||
|
|
||||||
test("set element in observed array", async () => {
|
|
||||||
const observer = new Observer();
|
|
||||||
observer.notifyCB = jest.fn();
|
|
||||||
const state: any = ["a"];
|
|
||||||
observer.observe(state);
|
|
||||||
expect(state.__owl__.rev).toBe(1);
|
|
||||||
expect(observer.rev).toBe(1);
|
|
||||||
expect(observer.notifyCB).toBeCalledTimes(0);
|
|
||||||
|
|
||||||
Observer.set(state, 1, "b");
|
|
||||||
await nextMicroTick();
|
|
||||||
expect(state.__owl__.rev).toBe(2);
|
|
||||||
expect(observer.rev).toBe(2);
|
|
||||||
expect(observer.notifyCB).toBeCalledTimes(1);
|
|
||||||
expect(state).toEqual(["a", "b"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("properly observe arrays in object", () => {
|
|
||||||
const observer = new Observer();
|
|
||||||
const state: any = { arr: [] };
|
|
||||||
observer.observe(state);
|
|
||||||
expect(state.arr.__owl__.rev).toBe(1);
|
|
||||||
expect(observer.rev).toBe(1);
|
|
||||||
expect(state.arr.length).toBe(0);
|
|
||||||
|
|
||||||
state.arr.push(1);
|
|
||||||
expect(state.arr.__owl__.rev).toBe(2);
|
|
||||||
expect(observer.rev).toBe(2);
|
|
||||||
expect(state.arr.length).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("properly observe objects in array", () => {
|
|
||||||
const observer = new Observer();
|
|
||||||
const state: any = { arr: [{ something: 1 }] };
|
|
||||||
observer.observe(state);
|
|
||||||
expect(state.arr.__owl__.rev).toBe(1);
|
|
||||||
expect(state.arr[0].__owl__.rev).toBe(1);
|
|
||||||
|
|
||||||
state.arr[0].something = 2;
|
|
||||||
expect(state.arr.__owl__.rev).toBe(1);
|
|
||||||
expect(state.arr[0].__owl__.rev).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("properly observe objects in object", () => {
|
|
||||||
const observer = new Observer();
|
|
||||||
const state: any = { a: { b: 1 } };
|
|
||||||
observer.observe(state);
|
|
||||||
expect(state.__owl__.rev).toBe(1);
|
|
||||||
expect(state.a.__owl__.rev).toBe(1);
|
|
||||||
|
|
||||||
state.a.b = 2;
|
|
||||||
expect(state.__owl__.rev).toBe(1);
|
|
||||||
expect(state.a.__owl__.rev).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("reobserve new object values", () => {
|
|
||||||
const observer = new Observer();
|
|
||||||
const obj: any = { a: 1 };
|
|
||||||
observer.observe(obj);
|
|
||||||
expect(obj.__owl__.rev).toBe(1);
|
|
||||||
obj.a = { b: 2 };
|
|
||||||
expect(observer.rev).toBe(2);
|
|
||||||
expect(obj.__owl__.rev).toBe(2);
|
|
||||||
|
|
||||||
// we start at 2 because it is the new observer rev number
|
|
||||||
expect(obj.a.__owl__.rev).toBe(2);
|
|
||||||
|
|
||||||
obj.a.b = 3;
|
|
||||||
expect(observer.rev).toBe(3);
|
|
||||||
expect(obj.__owl__.rev).toBe(2);
|
|
||||||
expect(obj.a.__owl__.rev).toBe(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("deep observe misc changes", () => {
|
|
||||||
const observer = new Observer();
|
|
||||||
const state: any = { o: { a: 1 }, arr: [1], n: 13 };
|
|
||||||
observer.observe(state);
|
|
||||||
expect(state.__owl__.rev).toBe(1);
|
|
||||||
expect(state.__owl__.deepRev).toBe(1);
|
|
||||||
|
|
||||||
state.o.a = 2;
|
|
||||||
expect(observer.rev).toBe(2);
|
|
||||||
expect(state.__owl__.rev).toBe(1);
|
|
||||||
expect(state.__owl__.deepRev).toBe(2);
|
|
||||||
|
|
||||||
state.arr.push(2);
|
|
||||||
expect(state.__owl__.rev).toBe(1);
|
|
||||||
expect(state.__owl__.deepRev).toBe(3);
|
|
||||||
|
|
||||||
state.n = 155;
|
|
||||||
expect(state.__owl__.rev).toBe(2);
|
|
||||||
expect(state.__owl__.deepRev).toBe(4);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("properly handle already observed state", () => {
|
|
||||||
const observer = new Observer();
|
|
||||||
const obj1: any = { a: 1 };
|
|
||||||
const obj2: any = { b: 1 };
|
|
||||||
observer.observe(obj1);
|
|
||||||
observer.observe(obj2);
|
|
||||||
expect(obj1.__owl__.rev).toBe(1);
|
|
||||||
expect(obj2.__owl__.rev).toBe(1);
|
|
||||||
|
|
||||||
obj1.a = 2;
|
|
||||||
obj2.b = 3;
|
|
||||||
expect(obj1.__owl__.rev).toBe(2);
|
|
||||||
expect(obj2.__owl__.rev).toBe(2);
|
|
||||||
|
|
||||||
obj2.b = obj1;
|
|
||||||
expect(obj1.__owl__.rev).toBe(2);
|
|
||||||
expect(obj2.__owl__.rev).toBe(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("can set a property more than once", () => {
|
|
||||||
const observer = new Observer();
|
|
||||||
const obj: any = {};
|
|
||||||
|
|
||||||
observer.observe(obj);
|
|
||||||
expect(obj.__owl__.rev).toBe(1);
|
|
||||||
expect(observer.rev).toBe(1);
|
|
||||||
expect(obj.__owl__.deepRev).toBe(1);
|
|
||||||
|
|
||||||
observer.set(obj, "aku", "always finds annoying problems");
|
|
||||||
expect(observer.rev).toBe(2);
|
|
||||||
expect(obj.__owl__.rev).toBe(2);
|
|
||||||
expect(obj.__owl__.deepRev).toBe(2);
|
|
||||||
|
|
||||||
observer.set(obj, "aku", "always finds good problems");
|
|
||||||
expect(observer.rev).toBe(3);
|
|
||||||
expect(obj.__owl__.rev).toBe(3);
|
|
||||||
expect(obj.__owl__.deepRev).toBe(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("properly handle swapping elements", () => {
|
|
||||||
const observer = new Observer();
|
|
||||||
const obj: any = { a: { arr: [] }, b: 1 };
|
|
||||||
observer.observe(obj);
|
|
||||||
|
|
||||||
// swap a and b
|
|
||||||
const b = obj.b;
|
|
||||||
obj.b = obj.a;
|
|
||||||
obj.a = b;
|
|
||||||
expect(observer.rev).toBe(3);
|
|
||||||
|
|
||||||
// push something into array to make sure it works
|
|
||||||
obj.b.arr.push("blanche");
|
|
||||||
expect(observer.rev).toBe(4);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("properly handle assigning observed obj containing array", () => {
|
|
||||||
const observer = new Observer();
|
|
||||||
const obj: any = { a: { arr: [], val: "test" } };
|
|
||||||
observer.observe(obj);
|
|
||||||
|
|
||||||
obj.a = { ...obj.a, val: "test2" };
|
|
||||||
expect(observer.rev).toBe(2);
|
|
||||||
|
|
||||||
// push something into array to make sure it works
|
|
||||||
obj.a.arr.push("blanche");
|
|
||||||
expect(observer.rev).toBe(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("accept cycles in observed state", () => {
|
|
||||||
const observer = new Observer();
|
|
||||||
const obj1: any = {};
|
|
||||||
const obj2: any = { b: obj1, key: 1 };
|
|
||||||
obj1.a = obj2;
|
|
||||||
observer.observe(obj1);
|
|
||||||
expect(obj1.__owl__.rev).toBe(1);
|
|
||||||
expect(obj2.__owl__.rev).toBe(1);
|
|
||||||
|
|
||||||
obj2.key = 3;
|
|
||||||
expect(obj1.__owl__.rev).toBe(1);
|
|
||||||
expect(obj2.__owl__.rev).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("call callback when state is changed", async () => {
|
|
||||||
const observer = new Observer();
|
|
||||||
observer.notifyCB = jest.fn();
|
|
||||||
const obj: any = { a: 1, b: { c: 2 }, d: [{ e: 3 }], f: 4 };
|
|
||||||
|
|
||||||
observer.observe(obj);
|
|
||||||
expect(observer.notifyCB).toBeCalledTimes(0);
|
|
||||||
|
|
||||||
obj.a = 2;
|
|
||||||
await nextMicroTick();
|
|
||||||
expect(observer.notifyCB).toBeCalledTimes(1);
|
|
||||||
|
|
||||||
obj.b.c = 3;
|
|
||||||
await nextMicroTick();
|
|
||||||
expect(observer.notifyCB).toBeCalledTimes(2);
|
|
||||||
|
|
||||||
obj.d[0].e = 5;
|
|
||||||
await nextMicroTick();
|
|
||||||
expect(observer.notifyCB).toBeCalledTimes(3);
|
|
||||||
|
|
||||||
obj.a = 111;
|
|
||||||
obj.f = 222;
|
|
||||||
await nextMicroTick();
|
|
||||||
expect(observer.notifyCB).toBeCalledTimes(4);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("throw error when state is mutated in object if allowMutation=false", async () => {
|
|
||||||
const observer = new Observer();
|
|
||||||
observer.allowMutations = false;
|
|
||||||
const obj: any = { a: 1 };
|
|
||||||
observer.observe(obj);
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
obj.a = 2;
|
|
||||||
}).toThrow('Observed state cannot be changed here! (key: "a", val: "2")');
|
|
||||||
});
|
|
||||||
|
|
||||||
test("throw error when state is mutated in array if allowMutation=false", async () => {
|
|
||||||
const observer = new Observer();
|
|
||||||
observer.allowMutations = false;
|
|
||||||
const obj: any = { a: [1] };
|
|
||||||
observer.observe(obj);
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
obj.a.push(2);
|
|
||||||
}).toThrow("Array cannot be changed here");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
+142
-119
@@ -3,7 +3,7 @@
|
|||||||
exports[`attributes class and t-attf-class with ternary operation 1`] = `
|
exports[`attributes class and t-attf-class with ternary operation 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
var _1 = 'hello ' + (context['value']?'world':'');
|
var _1 = 'hello ' + (context['value']?'world':'');
|
||||||
let c2 = [], p2 = {key:2,attrs:{class: _1}};
|
let c2 = [], p2 = {key:2,attrs:{class: _1}};
|
||||||
var vn2 = h('div', p2, c2);
|
var vn2 = h('div', p2, c2);
|
||||||
@@ -14,7 +14,7 @@ exports[`attributes class and t-attf-class with ternary operation 1`] = `
|
|||||||
exports[`attributes dynamic attribute falsy variable 1`] = `
|
exports[`attributes dynamic attribute falsy variable 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
var _1 = context['value'];
|
var _1 = context['value'];
|
||||||
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
|
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
|
||||||
var vn2 = h('div', p2, c2);
|
var vn2 = h('div', p2, c2);
|
||||||
@@ -25,7 +25,7 @@ exports[`attributes dynamic attribute falsy variable 1`] = `
|
|||||||
exports[`attributes dynamic attribute with a dash 1`] = `
|
exports[`attributes dynamic attribute with a dash 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
var _1 = context['id'];
|
var _1 = context['id'];
|
||||||
let c2 = [], p2 = {key:2,attrs:{\\"data-action-id\\": _1}};
|
let c2 = [], p2 = {key:2,attrs:{\\"data-action-id\\": _1}};
|
||||||
var vn2 = h('div', p2, c2);
|
var vn2 = h('div', p2, c2);
|
||||||
@@ -36,7 +36,7 @@ exports[`attributes dynamic attribute with a dash 1`] = `
|
|||||||
exports[`attributes dynamic attributes 1`] = `
|
exports[`attributes dynamic attributes 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
var _1 = 'bar';
|
var _1 = 'bar';
|
||||||
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
|
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
|
||||||
var vn2 = h('div', p2, c2);
|
var vn2 = h('div', p2, c2);
|
||||||
@@ -47,7 +47,7 @@ exports[`attributes dynamic attributes 1`] = `
|
|||||||
exports[`attributes dynamic formatted attributes with a dash 1`] = `
|
exports[`attributes dynamic formatted attributes with a dash 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
var _1 = \`Some text \${context['id']}\`;
|
var _1 = \`Some text \${context['id']}\`;
|
||||||
let c2 = [], p2 = {key:2,attrs:{\\"aria-label\\": _1}};
|
let c2 = [], p2 = {key:2,attrs:{\\"aria-label\\": _1}};
|
||||||
var vn2 = h('div', p2, c2);
|
var vn2 = h('div', p2, c2);
|
||||||
@@ -58,7 +58,7 @@ exports[`attributes dynamic formatted attributes with a dash 1`] = `
|
|||||||
exports[`attributes fixed variable 1`] = `
|
exports[`attributes fixed variable 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
var _1 = context['value'];
|
var _1 = context['value'];
|
||||||
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
|
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
|
||||||
var vn2 = h('div', p2, c2);
|
var vn2 = h('div', p2, c2);
|
||||||
@@ -69,7 +69,7 @@ exports[`attributes fixed variable 1`] = `
|
|||||||
exports[`attributes format expression 1`] = `
|
exports[`attributes format expression 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
var _1 = (context['value']+37);
|
var _1 = (context['value']+37);
|
||||||
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
|
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
|
||||||
var vn2 = h('div', p2, c2);
|
var vn2 = h('div', p2, c2);
|
||||||
@@ -80,7 +80,7 @@ exports[`attributes format expression 1`] = `
|
|||||||
exports[`attributes format expression, other format 1`] = `
|
exports[`attributes format expression, other format 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
var _1 = (context['value']+37);
|
var _1 = (context['value']+37);
|
||||||
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
|
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
|
||||||
var vn2 = h('div', p2, c2);
|
var vn2 = h('div', p2, c2);
|
||||||
@@ -91,7 +91,7 @@ exports[`attributes format expression, other format 1`] = `
|
|||||||
exports[`attributes format literal 1`] = `
|
exports[`attributes format literal 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
var _1 = \`bar\`;
|
var _1 = \`bar\`;
|
||||||
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
|
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
|
||||||
var vn2 = h('div', p2, c2);
|
var vn2 = h('div', p2, c2);
|
||||||
@@ -102,7 +102,7 @@ exports[`attributes format literal 1`] = `
|
|||||||
exports[`attributes format multiple 1`] = `
|
exports[`attributes format multiple 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
var _1 = \`a \${context['value1']} is \${context['value2']} of \${context['value3']} ]\`;
|
var _1 = \`a \${context['value1']} is \${context['value2']} of \${context['value3']} ]\`;
|
||||||
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
|
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
|
||||||
var vn2 = h('div', p2, c2);
|
var vn2 = h('div', p2, c2);
|
||||||
@@ -113,7 +113,7 @@ exports[`attributes format multiple 1`] = `
|
|||||||
exports[`attributes format value 1`] = `
|
exports[`attributes format value 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
var _1 = \`b\${context['value']}r\`;
|
var _1 = \`b\${context['value']}r\`;
|
||||||
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
|
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
|
||||||
var vn2 = h('div', p2, c2);
|
var vn2 = h('div', p2, c2);
|
||||||
@@ -124,12 +124,13 @@ exports[`attributes format value 1`] = `
|
|||||||
exports[`attributes from object variables set previously 1`] = `
|
exports[`attributes from object variables set previously 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
let utils = this.constructor.utils;
|
||||||
|
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);
|
||||||
var _2 = {a:'b'};
|
var _2 = {a:'b'};
|
||||||
var _3 = _2.a;
|
let _3 = utils.toObj(_2.a);
|
||||||
let c4 = [], p4 = {key:4,attrs:{class: _3}};
|
let c4 = [], p4 = {key:4,class:_3};
|
||||||
var vn4 = h('span', p4, c4);
|
var vn4 = h('span', p4, c4);
|
||||||
c1.push(vn4);
|
c1.push(vn4);
|
||||||
return vn1;
|
return vn1;
|
||||||
@@ -139,12 +140,13 @@ exports[`attributes from object variables set previously 1`] = `
|
|||||||
exports[`attributes from variables set previously 1`] = `
|
exports[`attributes from variables set previously 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
let utils = this.constructor.utils;
|
||||||
|
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);
|
||||||
var _2 = 'def';
|
var _2 = 'def';
|
||||||
var _3 = _2;
|
let _3 = utils.toObj(_2);
|
||||||
let c4 = [], p4 = {key:4,attrs:{class: _3}};
|
let c4 = [], p4 = {key:4,class:_3};
|
||||||
var vn4 = h('span', p4, c4);
|
var vn4 = h('span', p4, c4);
|
||||||
c1.push(vn4);
|
c1.push(vn4);
|
||||||
return vn1;
|
return vn1;
|
||||||
@@ -154,7 +156,7 @@ exports[`attributes from variables set previously 1`] = `
|
|||||||
exports[`attributes object 1`] = `
|
exports[`attributes object 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
var _1 = context['value'];
|
var _1 = context['value'];
|
||||||
let c2 = [], p2 = {key:2,attrs:{}};
|
let c2 = [], p2 = {key:2,attrs:{}};
|
||||||
if (_1 instanceof Array) {
|
if (_1 instanceof Array) {
|
||||||
@@ -172,7 +174,7 @@ exports[`attributes object 1`] = `
|
|||||||
exports[`attributes static attributes 1`] = `
|
exports[`attributes static attributes 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
var _1 = 'a';
|
var _1 = 'a';
|
||||||
var _2 = 'b';
|
var _2 = 'b';
|
||||||
var _3 = 'c';
|
var _3 = 'c';
|
||||||
@@ -185,7 +187,7 @@ exports[`attributes static attributes 1`] = `
|
|||||||
exports[`attributes static attributes on void elements 1`] = `
|
exports[`attributes static attributes on void elements 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
var _1 = '/test.jpg';
|
var _1 = '/test.jpg';
|
||||||
var _2 = 'Test';
|
var _2 = 'Test';
|
||||||
let c3 = [], p3 = {key:3,attrs:{src: _1,alt: _2}};
|
let c3 = [], p3 = {key:3,attrs:{src: _1,alt: _2}};
|
||||||
@@ -197,7 +199,7 @@ exports[`attributes static attributes on void elements 1`] = `
|
|||||||
exports[`attributes static attributes with dashes 1`] = `
|
exports[`attributes static attributes with dashes 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
var _1 = 'Close';
|
var _1 = 'Close';
|
||||||
let c2 = [], p2 = {key:2,attrs:{\\"aria-label\\": _1}};
|
let c2 = [], p2 = {key:2,attrs:{\\"aria-label\\": _1}};
|
||||||
var vn2 = h('div', p2, c2);
|
var vn2 = h('div', p2, c2);
|
||||||
@@ -208,33 +210,33 @@ exports[`attributes static attributes with dashes 1`] = `
|
|||||||
exports[`attributes t-att-class and class should combine together 1`] = `
|
exports[`attributes t-att-class and class should combine together 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
let utils = this.constructor.utils;
|
||||||
var _1 = 'hello';
|
var h = this.h;
|
||||||
var _3 = context['value'];
|
let _2 = {'hello':true};
|
||||||
var _2 = 'hello' + (_3 ? ' ' + _3 : '');
|
Object.assign(_2, utils.toObj(context['value']))
|
||||||
let c4 = [], p4 = {key:4,attrs:{class: _2}};
|
let c3 = [], p3 = {key:3,class:_2};
|
||||||
var vn4 = h('div', p4, c4);
|
var vn3 = h('div', p3, c3);
|
||||||
return vn4;
|
return vn3;
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`attributes t-att-class with object 1`] = `
|
exports[`attributes t-att-class with object 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
let utils = this.constructor.utils;
|
||||||
var _1 = 'static';
|
var h = this.h;
|
||||||
var _3 = this.utils.objectToAttrString({a:context['b'],c:context['d'],e:context['f']});
|
let _2 = {'static':true};
|
||||||
var _2 = 'static' + (_3 ? ' ' + _3 : '');
|
Object.assign(_2, utils.toObj({a:context['b'],c:context['d'],e:context['f']}))
|
||||||
let c4 = [], p4 = {key:4,attrs:{class: _2}};
|
let c3 = [], p3 = {key:3,class:_2};
|
||||||
var vn4 = h('div', p4, c4);
|
var vn3 = h('div', p3, c3);
|
||||||
return vn4;
|
return vn3;
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`attributes t-attf-class should combine with class 1`] = `
|
exports[`attributes t-attf-class should combine with class 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
var _1 = 'hello ' + \`world\`;
|
var _1 = 'hello ' + \`world\`;
|
||||||
let c2 = [], p2 = {key:2,attrs:{class: _1}};
|
let c2 = [], p2 = {key:2,attrs:{class: _1}};
|
||||||
var vn2 = h('div', p2, c2);
|
var vn2 = h('div', p2, c2);
|
||||||
@@ -245,7 +247,7 @@ exports[`attributes t-attf-class should combine with class 1`] = `
|
|||||||
exports[`attributes tuple literal 1`] = `
|
exports[`attributes tuple literal 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
var _1 = ['foo','bar'];
|
var _1 = ['foo','bar'];
|
||||||
let c2 = [], p2 = {key:2,attrs:{}};
|
let c2 = [], p2 = {key:2,attrs:{}};
|
||||||
if (_1 instanceof Array) {
|
if (_1 instanceof Array) {
|
||||||
@@ -263,7 +265,7 @@ exports[`attributes tuple literal 1`] = `
|
|||||||
exports[`attributes tuple variable 1`] = `
|
exports[`attributes tuple variable 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
var _1 = context['value'];
|
var _1 = context['value'];
|
||||||
let c2 = [], p2 = {key:2,attrs:{}};
|
let c2 = [], p2 = {key:2,attrs:{}};
|
||||||
if (_1 instanceof Array) {
|
if (_1 instanceof Array) {
|
||||||
@@ -281,7 +283,7 @@ exports[`attributes tuple variable 1`] = `
|
|||||||
exports[`debugging t-debug 1`] = `
|
exports[`debugging t-debug 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
debugger;
|
debugger;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
@@ -299,7 +301,7 @@ exports[`debugging t-debug 1`] = `
|
|||||||
exports[`debugging t-log 1`] = `
|
exports[`debugging t-log 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.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);
|
||||||
var _2 = 42;
|
var _2 = 42;
|
||||||
@@ -312,7 +314,7 @@ exports[`foreach does not pollute the rendering context 1`] = `
|
|||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
context = Object.create(context);
|
context = Object.create(context);
|
||||||
var h = this.utils.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);
|
||||||
var _2 = [1];
|
var _2 = [1];
|
||||||
@@ -342,7 +344,7 @@ exports[`foreach iterate on items (on a element node) 1`] = `
|
|||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
context = Object.create(context);
|
context = Object.create(context);
|
||||||
var h = this.utils.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);
|
||||||
var _2 = [1,2];
|
var _2 = [1,2];
|
||||||
@@ -375,7 +377,7 @@ exports[`foreach iterate on items 1`] = `
|
|||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
context = Object.create(context);
|
context = Object.create(context);
|
||||||
var h = this.utils.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);
|
||||||
var _2 = [3,2,1];
|
var _2 = [3,2,1];
|
||||||
@@ -417,7 +419,7 @@ exports[`foreach iterate, dict param 1`] = `
|
|||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
context = Object.create(context);
|
context = Object.create(context);
|
||||||
var h = this.utils.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);
|
||||||
var _2 = context['value'];
|
var _2 = context['value'];
|
||||||
@@ -459,7 +461,7 @@ exports[`foreach iterate, position 1`] = `
|
|||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
context = Object.create(context);
|
context = Object.create(context);
|
||||||
var h = this.utils.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);
|
||||||
var _2 = Array(5);
|
var _2 = Array(5);
|
||||||
@@ -498,7 +500,7 @@ exports[`foreach warn if no key in some case 1`] = `
|
|||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
context = Object.create(context);
|
context = Object.create(context);
|
||||||
var h = this.utils.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);
|
||||||
var _2 = [1,2];
|
var _2 = [1,2];
|
||||||
@@ -530,7 +532,7 @@ exports[`foreach warn if no key in some case 1`] = `
|
|||||||
exports[`loading templates can initialize qweb with a string 1`] = `
|
exports[`loading templates can initialize qweb with a string 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.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);
|
||||||
c1.push({text: \`jupiler\`});
|
c1.push({text: \`jupiler\`});
|
||||||
@@ -541,7 +543,7 @@ exports[`loading templates can initialize qweb with a string 1`] = `
|
|||||||
exports[`loading templates can load a few templates from a xml string 1`] = `
|
exports[`loading templates can load a few templates from a xml string 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('ul', p1, c1);
|
var vn1 = h('ul', p1, c1);
|
||||||
let c2 = [], p2 = {key:2};
|
let c2 = [], p2 = {key:2};
|
||||||
@@ -559,8 +561,9 @@ exports[`loading templates can load a few templates from a xml string 1`] = `
|
|||||||
exports[`misc global 1`] = `
|
exports[`misc global 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
|
let utils = this.constructor.utils;
|
||||||
context = Object.create(context);
|
context = Object.create(context);
|
||||||
var h = this.utils.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);
|
||||||
var _2 = [4,5,6];
|
var _2 = [4,5,6];
|
||||||
@@ -626,7 +629,7 @@ exports[`misc global 1`] = `
|
|||||||
c1.push(vn21);
|
c1.push(vn21);
|
||||||
var _22 = context['toto'];
|
var _22 = context['toto'];
|
||||||
if (_22 || _22 === 0) {
|
if (_22 || _22 === 0) {
|
||||||
var frag23 = this.utils.getFragment(_22)
|
var frag23 = utils.getFragment(_22)
|
||||||
var p24 = {hook: {
|
var p24 = {hook: {
|
||||||
insert: n => n.elm.parentNode.replaceChild(frag23, n.elm),
|
insert: n => n.elm.parentNode.replaceChild(frag23, n.elm),
|
||||||
}};
|
}};
|
||||||
@@ -642,7 +645,7 @@ exports[`misc global 1`] = `
|
|||||||
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
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
var _1 = 'checkbox';
|
var _1 = 'checkbox';
|
||||||
var _2 = context['flag'];
|
var _2 = context['flag'];
|
||||||
let c3 = [], p3 = {key:3,attrs:{type: _1,checked: _2},props:{checked: _2}};
|
let c3 = [], p3 = {key:3,attrs:{type: _1,checked: _2},props:{checked: _2}};
|
||||||
@@ -654,7 +657,7 @@ exports[`special cases for some boolean html attributes/properties input type= c
|
|||||||
exports[`special cases for some boolean html attributes/properties various boolean html attributes 1`] = `
|
exports[`special cases for some boolean html attributes/properties various boolean html attributes 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.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);
|
||||||
var _2 = 'checkbox';
|
var _2 = 'checkbox';
|
||||||
@@ -694,7 +697,7 @@ exports[`special cases for some boolean html attributes/properties various boole
|
|||||||
exports[`static templates div with a span child node 1`] = `
|
exports[`static templates div with a span child node 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.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);
|
||||||
let c2 = [], p2 = {key:2};
|
let c2 = [], p2 = {key:2};
|
||||||
@@ -708,7 +711,7 @@ exports[`static templates div with a span child node 1`] = `
|
|||||||
exports[`static templates div with a text node 1`] = `
|
exports[`static templates div with a text node 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.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);
|
||||||
c1.push({text: \`word\`});
|
c1.push({text: \`word\`});
|
||||||
@@ -719,7 +722,7 @@ exports[`static templates div with a text node 1`] = `
|
|||||||
exports[`static templates empty div 1`] = `
|
exports[`static templates empty div 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.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);
|
||||||
return vn1;
|
return vn1;
|
||||||
@@ -729,7 +732,7 @@ exports[`static templates empty div 1`] = `
|
|||||||
exports[`static templates simple dynamic value 1`] = `
|
exports[`static templates simple dynamic value 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
var _1 = context['text'];
|
var _1 = context['text'];
|
||||||
if (_1 || _1 === 0) {
|
if (_1 || _1 === 0) {
|
||||||
var vn2 = {text: _1};
|
var vn2 = {text: _1};
|
||||||
@@ -741,7 +744,7 @@ exports[`static templates simple dynamic value 1`] = `
|
|||||||
exports[`static templates simple string 1`] = `
|
exports[`static templates simple string 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
var vn1 = {text: \`hello vdom\`};
|
var vn1 = {text: \`hello vdom\`};
|
||||||
return vn1;
|
return vn1;
|
||||||
}"
|
}"
|
||||||
@@ -750,7 +753,7 @@ exports[`static templates simple string 1`] = `
|
|||||||
exports[`static templates simple string, with some dynamic value 1`] = `
|
exports[`static templates simple string, with some dynamic value 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
var vn1 = {text: \`hello \`};
|
var vn1 = {text: \`hello \`};
|
||||||
var _2 = context['text'];
|
var _2 = context['text'];
|
||||||
if (_2 || _2 === 0) {
|
if (_2 || _2 === 0) {
|
||||||
@@ -763,7 +766,7 @@ exports[`static templates simple string, with some dynamic value 1`] = `
|
|||||||
exports[`t-call (template calling basic caller 1`] = `
|
exports[`t-call (template calling basic caller 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.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);
|
||||||
c1.push({text: \`ok\`});
|
c1.push({text: \`ok\`});
|
||||||
@@ -774,7 +777,7 @@ exports[`t-call (template calling basic caller 1`] = `
|
|||||||
exports[`t-call (template calling inherit context 1`] = `
|
exports[`t-call (template calling inherit context 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.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);
|
||||||
var _2 = 1;
|
var _2 = 1;
|
||||||
@@ -788,7 +791,7 @@ exports[`t-call (template calling inherit context 1`] = `
|
|||||||
exports[`t-call (template calling scoped parameters 1`] = `
|
exports[`t-call (template calling scoped parameters 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.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);
|
||||||
{
|
{
|
||||||
@@ -803,10 +806,26 @@ exports[`t-call (template calling scoped parameters 1`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`t-call (template calling t-call with t-if 1`] = `
|
||||||
|
"function anonymous(context,extra
|
||||||
|
) {
|
||||||
|
var h = this.h;
|
||||||
|
let c1 = [], p1 = {key:1};
|
||||||
|
var vn1 = h('div', p1, c1);
|
||||||
|
if (context['flag']) {
|
||||||
|
let c2 = [], p2 = {key:2};
|
||||||
|
var vn2 = h('span', p2, c2);
|
||||||
|
c1.push(vn2);
|
||||||
|
c2.push({text: \`ok\`});
|
||||||
|
}
|
||||||
|
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
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
let c2 = [], p2 = {key:2};
|
let c2 = [], p2 = {key:2};
|
||||||
var vn2 = h('div', p2, c2);
|
var vn2 = h('div', p2, c2);
|
||||||
c2.push({text: \`ok\`});
|
c2.push({text: \`ok\`});
|
||||||
@@ -817,7 +836,7 @@ exports[`t-call (template calling with unused body 1`] = `
|
|||||||
exports[`t-call (template calling with unused setbody 1`] = `
|
exports[`t-call (template calling with unused setbody 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
{
|
{
|
||||||
let _1 = 3;
|
let _1 = 3;
|
||||||
let c2 = [], p2 = {key:2};
|
let c2 = [], p2 = {key:2};
|
||||||
@@ -831,7 +850,7 @@ exports[`t-call (template calling with unused setbody 1`] = `
|
|||||||
exports[`t-call (template calling with used body 1`] = `
|
exports[`t-call (template calling with used body 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
let c2 = [], p2 = {key:2};
|
let c2 = [], p2 = {key:2};
|
||||||
var vn2 = h('h1', p2, c2);
|
var vn2 = h('h1', p2, c2);
|
||||||
c2.push({text: \`ok\`});
|
c2.push({text: \`ok\`});
|
||||||
@@ -842,7 +861,7 @@ exports[`t-call (template calling with used body 1`] = `
|
|||||||
exports[`t-call (template calling with used set body 1`] = `
|
exports[`t-call (template calling with used set body 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('span', p1, c1);
|
var vn1 = h('span', p1, c1);
|
||||||
{
|
{
|
||||||
@@ -858,7 +877,7 @@ exports[`t-call (template calling with used set body 1`] = `
|
|||||||
exports[`t-esc escaping on a node 1`] = `
|
exports[`t-esc escaping on a node 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1,on:{}};
|
let c1 = [], p1 = {key:1,on:{}};
|
||||||
var vn1 = h('span', p1, c1);
|
var vn1 = h('span', p1, c1);
|
||||||
var _2 = 'ok';
|
var _2 = 'ok';
|
||||||
@@ -872,7 +891,7 @@ exports[`t-esc escaping on a node 1`] = `
|
|||||||
exports[`t-esc escaping on a node with a body 1`] = `
|
exports[`t-esc escaping on a node with a body 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1,on:{}};
|
let c1 = [], p1 = {key:1,on:{}};
|
||||||
var vn1 = h('span', p1, c1);
|
var vn1 = h('span', p1, c1);
|
||||||
var _2 = 'ok';
|
var _2 = 'ok';
|
||||||
@@ -888,7 +907,7 @@ exports[`t-esc escaping on a node with a body 1`] = `
|
|||||||
exports[`t-esc escaping on a node with a body, as a default 1`] = `
|
exports[`t-esc escaping on a node with a body, as a default 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1,on:{}};
|
let c1 = [], p1 = {key:1,on:{}};
|
||||||
var vn1 = h('span', p1, c1);
|
var vn1 = h('span', p1, c1);
|
||||||
var _2 = context['var'];
|
var _2 = context['var'];
|
||||||
@@ -904,7 +923,7 @@ exports[`t-esc escaping on a node with a body, as a default 1`] = `
|
|||||||
exports[`t-esc literal 1`] = `
|
exports[`t-esc literal 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('span', p1, c1);
|
var vn1 = h('span', p1, c1);
|
||||||
var _2 = 'ok';
|
var _2 = 'ok';
|
||||||
@@ -918,7 +937,7 @@ exports[`t-esc literal 1`] = `
|
|||||||
exports[`t-esc variable 1`] = `
|
exports[`t-esc variable 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('span', p1, c1);
|
var vn1 = h('span', p1, c1);
|
||||||
var _2 = context['var'];
|
var _2 = context['var'];
|
||||||
@@ -932,7 +951,7 @@ exports[`t-esc variable 1`] = `
|
|||||||
exports[`t-if boolean value condition elif 1`] = `
|
exports[`t-if boolean value condition elif 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.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);
|
||||||
if (context['color']=='black') {
|
if (context['color']=='black') {
|
||||||
@@ -954,7 +973,7 @@ exports[`t-if boolean value condition elif 1`] = `
|
|||||||
exports[`t-if boolean value condition else 1`] = `
|
exports[`t-if boolean value condition else 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.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);
|
||||||
let c2 = [], p2 = {key:2};
|
let c2 = [], p2 = {key:2};
|
||||||
@@ -978,7 +997,7 @@ exports[`t-if boolean value condition else 1`] = `
|
|||||||
exports[`t-if boolean value condition false else 1`] = `
|
exports[`t-if boolean value condition false else 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.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);
|
||||||
let c2 = [], p2 = {key:2};
|
let c2 = [], p2 = {key:2};
|
||||||
@@ -1002,7 +1021,7 @@ exports[`t-if boolean value condition false else 1`] = `
|
|||||||
exports[`t-if boolean value condition missing 1`] = `
|
exports[`t-if boolean value condition missing 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('span', p1, c1);
|
var vn1 = h('span', p1, c1);
|
||||||
if (context['condition']) {
|
if (context['condition']) {
|
||||||
@@ -1015,7 +1034,7 @@ exports[`t-if boolean value condition missing 1`] = `
|
|||||||
exports[`t-if boolean value false condition 1`] = `
|
exports[`t-if boolean value false condition 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.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);
|
||||||
if (context['condition']) {
|
if (context['condition']) {
|
||||||
@@ -1028,7 +1047,7 @@ exports[`t-if boolean value false condition 1`] = `
|
|||||||
exports[`t-if boolean value true condition 1`] = `
|
exports[`t-if boolean value true condition 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.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);
|
||||||
if (context['condition']) {
|
if (context['condition']) {
|
||||||
@@ -1041,7 +1060,7 @@ exports[`t-if boolean value true condition 1`] = `
|
|||||||
exports[`t-if can use some boolean operators in expressions 1`] = `
|
exports[`t-if can use some boolean operators in expressions 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.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);
|
||||||
if (context['cond1']&&context['cond2']) {
|
if (context['cond1']&&context['cond2']) {
|
||||||
@@ -1075,7 +1094,7 @@ exports[`t-if can use some boolean operators in expressions 1`] = `
|
|||||||
exports[`t-key can use t-key directive on a node 1`] = `
|
exports[`t-key can use t-key directive on a node 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:context['beer'].id};
|
let c1 = [], p1 = {key:context['beer'].id};
|
||||||
var vn1 = h('div', p1, c1);
|
var vn1 = h('div', p1, c1);
|
||||||
var _2 = context['beer'].name;
|
var _2 = context['beer'].name;
|
||||||
@@ -1090,7 +1109,7 @@ exports[`t-key t-key directive in a list 1`] = `
|
|||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
context = Object.create(context);
|
context = Object.create(context);
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('ul', p1, c1);
|
var vn1 = h('ul', p1, c1);
|
||||||
var _2 = context['beers'];
|
var _2 = context['beers'];
|
||||||
@@ -1125,7 +1144,7 @@ exports[`t-on can bind event handler 1`] = `
|
|||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
let owner = context;
|
let owner = context;
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1,on:{}};
|
let c1 = [], p1 = {key:1,on:{}};
|
||||||
var vn1 = h('button', p1, c1);
|
var vn1 = h('button', p1, c1);
|
||||||
if (!context['add']) {
|
if (!context['add']) {
|
||||||
@@ -1142,7 +1161,7 @@ exports[`t-on can bind handlers with arguments 1`] = `
|
|||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
let owner = context;
|
let owner = context;
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1,on:{}};
|
let c1 = [], p1 = {key:1,on:{}};
|
||||||
var vn1 = h('button', p1, c1);
|
var vn1 = h('button', p1, c1);
|
||||||
if (!context['add']) {
|
if (!context['add']) {
|
||||||
@@ -1158,7 +1177,7 @@ exports[`t-on can bind handlers with empty object (with non empty inner string)
|
|||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
let owner = context;
|
let owner = context;
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1,on:{}};
|
let c1 = [], p1 = {key:1,on:{}};
|
||||||
var vn1 = h('button', p1, c1);
|
var vn1 = h('button', p1, c1);
|
||||||
if (!context['doSomething']) {
|
if (!context['doSomething']) {
|
||||||
@@ -1174,7 +1193,7 @@ exports[`t-on can bind handlers with empty object 1`] = `
|
|||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
let owner = context;
|
let owner = context;
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1,on:{}};
|
let c1 = [], p1 = {key:1,on:{}};
|
||||||
var vn1 = h('button', p1, c1);
|
var vn1 = h('button', p1, c1);
|
||||||
if (!context['doSomething']) {
|
if (!context['doSomething']) {
|
||||||
@@ -1191,7 +1210,7 @@ exports[`t-on can bind handlers with loop variable as argument 1`] = `
|
|||||||
) {
|
) {
|
||||||
let owner = context;
|
let owner = context;
|
||||||
context = Object.create(context);
|
context = Object.create(context);
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('ul', p1, c1);
|
var vn1 = h('ul', p1, c1);
|
||||||
var _2 = ['someval'];
|
var _2 = ['someval'];
|
||||||
@@ -1228,7 +1247,7 @@ exports[`t-on can bind handlers with object arguments 1`] = `
|
|||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
let owner = context;
|
let owner = context;
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1,on:{}};
|
let c1 = [], p1 = {key:1,on:{}};
|
||||||
var vn1 = h('button', p1, c1);
|
var vn1 = h('button', p1, c1);
|
||||||
if (!context['add']) {
|
if (!context['add']) {
|
||||||
@@ -1244,7 +1263,7 @@ exports[`t-on can bind two event handlers 1`] = `
|
|||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
let owner = context;
|
let owner = context;
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1,on:{}};
|
let c1 = [], p1 = {key:1,on:{}};
|
||||||
var vn1 = h('button', p1, c1);
|
var vn1 = h('button', p1, c1);
|
||||||
if (!context['handleClick']) {
|
if (!context['handleClick']) {
|
||||||
@@ -1266,7 +1285,7 @@ exports[`t-on handler is bound to proper owner 1`] = `
|
|||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
let owner = context;
|
let owner = context;
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1,on:{}};
|
let c1 = [], p1 = {key:1,on:{}};
|
||||||
var vn1 = h('button', p1, c1);
|
var vn1 = h('button', p1, c1);
|
||||||
if (!context['add']) {
|
if (!context['add']) {
|
||||||
@@ -1283,7 +1302,7 @@ exports[`t-on t-on with prevent and self modifiers (order matters) 1`] = `
|
|||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
let owner = context;
|
let owner = context;
|
||||||
var h = this.utils.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);
|
||||||
let c2 = [], p2 = {key:2,on:{}};
|
let c2 = [], p2 = {key:2,on:{}};
|
||||||
@@ -1306,7 +1325,7 @@ exports[`t-on t-on with prevent and/or stop modifiers 1`] = `
|
|||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
let owner = context;
|
let owner = context;
|
||||||
var h = this.utils.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);
|
||||||
let c2 = [], p2 = {key:2,on:{}};
|
let c2 = [], p2 = {key:2,on:{}};
|
||||||
@@ -1344,7 +1363,7 @@ exports[`t-on t-on with self and prevent modifiers (order matters) 1`] = `
|
|||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
let owner = context;
|
let owner = context;
|
||||||
var h = this.utils.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);
|
||||||
let c2 = [], p2 = {key:2,on:{}};
|
let c2 = [], p2 = {key:2,on:{}};
|
||||||
@@ -1367,7 +1386,7 @@ exports[`t-on t-on with self modifier 1`] = `
|
|||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
let owner = context;
|
let owner = context;
|
||||||
var h = this.utils.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);
|
||||||
let c2 = [], p2 = {key:2,on:{}};
|
let c2 = [], p2 = {key:2,on:{}};
|
||||||
@@ -1401,12 +1420,13 @@ exports[`t-on t-on with self modifier 1`] = `
|
|||||||
exports[`t-raw literal 1`] = `
|
exports[`t-raw literal 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
let utils = this.constructor.utils;
|
||||||
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('span', p1, c1);
|
var vn1 = h('span', p1, c1);
|
||||||
var _2 = 'ok';
|
var _2 = 'ok';
|
||||||
if (_2 || _2 === 0) {
|
if (_2 || _2 === 0) {
|
||||||
var frag3 = this.utils.getFragment(_2)
|
var frag3 = utils.getFragment(_2)
|
||||||
var p4 = {hook: {
|
var p4 = {hook: {
|
||||||
insert: n => n.elm.parentNode.replaceChild(frag3, n.elm),
|
insert: n => n.elm.parentNode.replaceChild(frag3, n.elm),
|
||||||
}};
|
}};
|
||||||
@@ -1420,12 +1440,13 @@ exports[`t-raw literal 1`] = `
|
|||||||
exports[`t-raw not escaping 1`] = `
|
exports[`t-raw not escaping 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
let utils = this.constructor.utils;
|
||||||
|
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);
|
||||||
var _2 = context['var'];
|
var _2 = context['var'];
|
||||||
if (_2 || _2 === 0) {
|
if (_2 || _2 === 0) {
|
||||||
var frag3 = this.utils.getFragment(_2)
|
var frag3 = utils.getFragment(_2)
|
||||||
var p4 = {hook: {
|
var p4 = {hook: {
|
||||||
insert: n => n.elm.parentNode.replaceChild(frag3, n.elm),
|
insert: n => n.elm.parentNode.replaceChild(frag3, n.elm),
|
||||||
}};
|
}};
|
||||||
@@ -1439,7 +1460,8 @@ exports[`t-raw not escaping 1`] = `
|
|||||||
exports[`t-raw t-raw and another sibling node 1`] = `
|
exports[`t-raw t-raw and another sibling node 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
let utils = this.constructor.utils;
|
||||||
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('span', p1, c1);
|
var vn1 = h('span', p1, c1);
|
||||||
let c2 = [], p2 = {key:2};
|
let c2 = [], p2 = {key:2};
|
||||||
@@ -1448,7 +1470,7 @@ exports[`t-raw t-raw and another sibling node 1`] = `
|
|||||||
c2.push({text: \`hello\`});
|
c2.push({text: \`hello\`});
|
||||||
var _3 = context['var'];
|
var _3 = context['var'];
|
||||||
if (_3 || _3 === 0) {
|
if (_3 || _3 === 0) {
|
||||||
var frag4 = this.utils.getFragment(_3)
|
var frag4 = utils.getFragment(_3)
|
||||||
var p5 = {hook: {
|
var p5 = {hook: {
|
||||||
insert: n => n.elm.parentNode.replaceChild(frag4, n.elm),
|
insert: n => n.elm.parentNode.replaceChild(frag4, n.elm),
|
||||||
}};
|
}};
|
||||||
@@ -1462,12 +1484,13 @@ exports[`t-raw t-raw and another sibling node 1`] = `
|
|||||||
exports[`t-raw variable 1`] = `
|
exports[`t-raw variable 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
let utils = this.constructor.utils;
|
||||||
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('span', p1, c1);
|
var vn1 = h('span', p1, c1);
|
||||||
var _2 = context['var'];
|
var _2 = context['var'];
|
||||||
if (_2 || _2 === 0) {
|
if (_2 || _2 === 0) {
|
||||||
var frag3 = this.utils.getFragment(_2)
|
var frag3 = utils.getFragment(_2)
|
||||||
var p4 = {hook: {
|
var p4 = {hook: {
|
||||||
insert: n => n.elm.parentNode.replaceChild(frag3, n.elm),
|
insert: n => n.elm.parentNode.replaceChild(frag3, n.elm),
|
||||||
}};
|
}};
|
||||||
@@ -1481,7 +1504,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
|
||||||
) {
|
) {
|
||||||
var h = this.utils.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);
|
||||||
let c2 = [], p2 = {key:2};
|
let c2 = [], p2 = {key:2};
|
||||||
@@ -1500,7 +1523,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
|
||||||
) {
|
) {
|
||||||
var h = this.utils.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);
|
||||||
let c2 = [], p2 = {key:2};
|
let c2 = [], p2 = {key:2};
|
||||||
@@ -1520,7 +1543,7 @@ exports[`t-ref refs in a loop 1`] = `
|
|||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
context = Object.create(context);
|
context = Object.create(context);
|
||||||
var h = this.utils.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);
|
||||||
var _2 = context['items'];
|
var _2 = context['items'];
|
||||||
@@ -1558,7 +1581,7 @@ exports[`t-ref refs in a loop 1`] = `
|
|||||||
exports[`t-set evaluate value expression 1`] = `
|
exports[`t-set evaluate value expression 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.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);
|
||||||
var _2 = 1+2;
|
var _2 = 1+2;
|
||||||
@@ -1572,7 +1595,7 @@ exports[`t-set evaluate value expression 1`] = `
|
|||||||
exports[`t-set evaluate value expression, part 2 1`] = `
|
exports[`t-set evaluate value expression, part 2 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.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);
|
||||||
var _2 = context['somevariable']+2;
|
var _2 = context['somevariable']+2;
|
||||||
@@ -1586,7 +1609,7 @@ exports[`t-set evaluate value expression, part 2 1`] = `
|
|||||||
exports[`t-set set from attribute literal 1`] = `
|
exports[`t-set set from attribute literal 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.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);
|
||||||
var _2 = 'ok';
|
var _2 = 'ok';
|
||||||
@@ -1600,7 +1623,7 @@ exports[`t-set set from attribute literal 1`] = `
|
|||||||
exports[`t-set set from attribute lookup 1`] = `
|
exports[`t-set set from attribute lookup 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.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);
|
||||||
var _2 = context['value'];
|
var _2 = context['value'];
|
||||||
@@ -1614,7 +1637,7 @@ exports[`t-set set from attribute lookup 1`] = `
|
|||||||
exports[`t-set set from body literal 1`] = `
|
exports[`t-set set from body literal 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
var vn1 = {text: \`ok\`};
|
var vn1 = {text: \`ok\`};
|
||||||
return vn1;
|
return vn1;
|
||||||
}"
|
}"
|
||||||
@@ -1623,7 +1646,7 @@ exports[`t-set set from body literal 1`] = `
|
|||||||
exports[`t-set set from body lookup 1`] = `
|
exports[`t-set set from body lookup 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.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);
|
||||||
var _2 = context['value'];
|
var _2 = context['value'];
|
||||||
@@ -1637,7 +1660,7 @@ exports[`t-set set from body lookup 1`] = `
|
|||||||
exports[`t-set set from empty body 1`] = `
|
exports[`t-set set from empty body 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.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);
|
||||||
return vn1;
|
return vn1;
|
||||||
@@ -1647,7 +1670,7 @@ exports[`t-set set from empty body 1`] = `
|
|||||||
exports[`t-set t-set and t-if 1`] = `
|
exports[`t-set t-set and t-if 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.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);
|
||||||
var _2 = context['value'];
|
var _2 = context['value'];
|
||||||
@@ -1661,7 +1684,7 @@ exports[`t-set t-set and t-if 1`] = `
|
|||||||
exports[`t-set t-set evaluates an expression only once 1`] = `
|
exports[`t-set t-set evaluates an expression only once 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.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);
|
||||||
var _2 = context['value']+' artois';
|
var _2 = context['value']+' artois';
|
||||||
@@ -1679,7 +1702,7 @@ exports[`t-set t-set should reuse variable if possible 1`] = `
|
|||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
context = Object.create(context);
|
context = Object.create(context);
|
||||||
var h = this.utils.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);
|
||||||
var _2 = 1;
|
var _2 = 1;
|
||||||
@@ -1716,7 +1739,7 @@ exports[`t-set t-set should reuse variable if possible 1`] = `
|
|||||||
exports[`t-set value priority 1`] = `
|
exports[`t-set value priority 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.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);
|
||||||
var _2 = 1;
|
var _2 = 1;
|
||||||
@@ -1730,7 +1753,7 @@ exports[`t-set value priority 1`] = `
|
|||||||
exports[`whitespace handling consecutives whitespaces are condensed into a single space 1`] = `
|
exports[`whitespace handling consecutives whitespaces are condensed into a single space 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.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);
|
||||||
c1.push({text: \` abc \`});
|
c1.push({text: \` abc \`});
|
||||||
@@ -1741,7 +1764,7 @@ exports[`whitespace handling consecutives whitespaces are condensed into a singl
|
|||||||
exports[`whitespace handling nothing is done in pre tags 1`] = `
|
exports[`whitespace handling nothing is done in pre tags 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('pre', p1, c1);
|
var vn1 = h('pre', p1, c1);
|
||||||
c1.push({text: \` \`});
|
c1.push({text: \` \`});
|
||||||
@@ -1752,7 +1775,7 @@ exports[`whitespace handling nothing is done in pre tags 1`] = `
|
|||||||
exports[`whitespace handling nothing is done in pre tags 2`] = `
|
exports[`whitespace handling nothing is done in pre tags 2`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('pre', p1, c1);
|
var vn1 = h('pre', p1, c1);
|
||||||
c1.push({text: \`
|
c1.push({text: \`
|
||||||
@@ -1765,7 +1788,7 @@ exports[`whitespace handling nothing is done in pre tags 2`] = `
|
|||||||
exports[`whitespace handling nothing is done in pre tags 3`] = `
|
exports[`whitespace handling nothing is done in pre tags 3`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.h;
|
var h = this.h;
|
||||||
let c1 = [], p1 = {key:1};
|
let c1 = [], p1 = {key:1};
|
||||||
var vn1 = h('pre', p1, c1);
|
var vn1 = h('pre', p1, c1);
|
||||||
c1.push({text: \`
|
c1.push({text: \`
|
||||||
@@ -1778,7 +1801,7 @@ exports[`whitespace handling nothing is done in pre tags 3`] = `
|
|||||||
exports[`whitespace handling white space only text nodes are condensed into a single space 1`] = `
|
exports[`whitespace handling white space only text nodes are condensed into a single space 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.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);
|
||||||
c1.push({text: \` \`});
|
c1.push({text: \` \`});
|
||||||
@@ -1789,7 +1812,7 @@ exports[`whitespace handling white space only text nodes are condensed into a si
|
|||||||
exports[`whitespace handling whitespace only text nodes with newlines are removed 1`] = `
|
exports[`whitespace handling whitespace only text nodes with newlines are removed 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
var h = this.utils.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);
|
||||||
let c2 = [], p2 = {key:2};
|
let c2 = [], p2 = {key:2};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { QWeb } from "../src/qweb_core";
|
import { QWeb } from "../../src/qweb/index";
|
||||||
import { normalize, renderToDOM, renderToString, trim } from "./helpers";
|
import { normalize, renderToDOM, renderToString, trim, nextTick } from "../helpers";
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Setup and helpers
|
// Setup and helpers
|
||||||
@@ -25,12 +25,12 @@ describe("static templates", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("simple dynamic value", () => {
|
test("simple dynamic value", () => {
|
||||||
qweb.addTemplate("test", "<t><t t-esc=\"text\"/></t>");
|
qweb.addTemplate("test", '<t><t t-esc="text"/></t>');
|
||||||
expect(renderToString(qweb, "test", { text: "hello vdom" })).toBe("hello vdom");
|
expect(renderToString(qweb, "test", { text: "hello vdom" })).toBe("hello vdom");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("simple string, with some dynamic value", () => {
|
test("simple string, with some dynamic value", () => {
|
||||||
qweb.addTemplate("test", "<t>hello <t t-esc=\"text\"/></t>");
|
qweb.addTemplate("test", '<t>hello <t t-esc="text"/></t>');
|
||||||
expect(renderToString(qweb, "test", { text: "vdom" })).toBe("hello vdom");
|
expect(renderToString(qweb, "test", { text: "vdom" })).toBe("hello vdom");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -52,9 +52,7 @@ describe("static templates", () => {
|
|||||||
|
|
||||||
describe("error handling", () => {
|
describe("error handling", () => {
|
||||||
test("invalid xml", () => {
|
test("invalid xml", () => {
|
||||||
expect(() => qweb.addTemplate("test", "<div>")).toThrow(
|
expect(() => qweb.addTemplate("test", "<div>")).toThrow("Invalid XML in template");
|
||||||
"Invalid XML in template"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("template with text node and tag", () => {
|
test("template with text node and tag", () => {
|
||||||
@@ -71,6 +69,7 @@ describe("error handling", () => {
|
|||||||
|
|
||||||
test("cannot add twice the same template", () => {
|
test("cannot add twice the same template", () => {
|
||||||
qweb.addTemplate("test", `<t></t>`);
|
qweb.addTemplate("test", `<t></t>`);
|
||||||
|
expect(() => qweb.addTemplate("test", "<div/>", true)).not.toThrow("already defined");
|
||||||
expect(() => qweb.addTemplate("test", "<div/>")).toThrow("already defined");
|
expect(() => qweb.addTemplate("test", "<div/>")).toThrow("already defined");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -89,19 +88,14 @@ describe("error handling", () => {
|
|||||||
|
|
||||||
test("nice error when t-on is evaluated with a missing event", () => {
|
test("nice error when t-on is evaluated with a missing event", () => {
|
||||||
qweb.addTemplate("templatename", `<div t-on="somemethod"></div>`);
|
qweb.addTemplate("templatename", `<div t-on="somemethod"></div>`);
|
||||||
expect(() =>
|
expect(() => qweb.render("templatename", { someMethod() {} }, { handlers: [] })).toThrow(
|
||||||
qweb.render("templatename", { someMethod() {} }, { handlers: [] })
|
"Missing event name with t-on directive"
|
||||||
).toThrow("Missing event name with t-on directive");
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("error when unknown directive", () => {
|
test("error when unknown directive", () => {
|
||||||
qweb.addTemplate(
|
qweb.addTemplate("templatename", `<div t-best-beer="rochefort 10">test</div>`);
|
||||||
"templatename",
|
expect(() => qweb.render("templatename")).toThrow("Unknown QWeb directive: 't-best-beer'");
|
||||||
`<div t-best-beer="rochefort 10">test</div>`
|
|
||||||
);
|
|
||||||
expect(() => qweb.render("templatename")).toThrow(
|
|
||||||
"Unknown QWeb directive: 't-best-beer'"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -118,9 +112,7 @@ describe("t-esc", () => {
|
|||||||
|
|
||||||
test.skip("escaping", () => {
|
test.skip("escaping", () => {
|
||||||
qweb.addTemplate("test", `<span><t t-esc="var"/></span>`);
|
qweb.addTemplate("test", `<span><t t-esc="var"/></span>`);
|
||||||
expect(renderToString(qweb, "test", { var: "<ok>" })).toBe(
|
expect(renderToString(qweb, "test", { var: "<ok>" })).toBe("<span><ok></span>");
|
||||||
"<span><ok></span>"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("escaping on a node", () => {
|
test("escaping on a node", () => {
|
||||||
@@ -152,9 +144,7 @@ describe("t-raw", () => {
|
|||||||
|
|
||||||
test("not escaping", () => {
|
test("not escaping", () => {
|
||||||
qweb.addTemplate("test", `<div><t t-raw="var"/></div>`);
|
qweb.addTemplate("test", `<div><t t-raw="var"/></div>`);
|
||||||
expect(renderToString(qweb, "test", { var: "<ok></ok>" })).toBe(
|
expect(renderToString(qweb, "test", { var: "<ok></ok>" })).toBe("<div><ok></ok></div>");
|
||||||
"<div><ok></ok></div>"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("t-raw and another sibling node", () => {
|
test("t-raw and another sibling node", () => {
|
||||||
@@ -167,10 +157,7 @@ describe("t-raw", () => {
|
|||||||
|
|
||||||
describe("t-set", () => {
|
describe("t-set", () => {
|
||||||
test("set from attribute literal", () => {
|
test("set from attribute literal", () => {
|
||||||
qweb.addTemplate(
|
qweb.addTemplate("test", `<div><t t-set="value" t-value="'ok'"/><t t-esc="value"/></div>`);
|
||||||
"test",
|
|
||||||
`<div><t t-set="value" t-value="'ok'"/><t t-esc="value"/></div>`
|
|
||||||
);
|
|
||||||
expect(renderToString(qweb, "test")).toBe("<div>ok</div>");
|
expect(renderToString(qweb, "test")).toBe("<div>ok</div>");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -182,24 +169,16 @@ describe("t-set", () => {
|
|||||||
<t t-if="v === 'ok'">grimbergen</t>
|
<t t-if="v === 'ok'">grimbergen</t>
|
||||||
</div>`
|
</div>`
|
||||||
);
|
);
|
||||||
expect(renderToString(qweb, "test", { value: "ok" })).toBe(
|
expect(renderToString(qweb, "test", { value: "ok" })).toBe("<div>grimbergen</div>");
|
||||||
"<div>grimbergen</div>"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("set from body literal", () => {
|
test("set from body literal", () => {
|
||||||
qweb.addTemplate(
|
qweb.addTemplate("test", `<t><t t-set="value">ok</t><t t-esc="value"/></t>`);
|
||||||
"test",
|
|
||||||
`<t><t t-set="value">ok</t><t t-esc="value"/></t>`
|
|
||||||
);
|
|
||||||
expect(renderToString(qweb, "test")).toBe("ok");
|
expect(renderToString(qweb, "test")).toBe("ok");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("set from attribute lookup", () => {
|
test("set from attribute lookup", () => {
|
||||||
qweb.addTemplate(
|
qweb.addTemplate("test", `<div><t t-set="stuff" t-value="value"/><t t-esc="stuff"/></div>`);
|
||||||
"test",
|
|
||||||
`<div><t t-set="stuff" t-value="value"/><t t-esc="stuff"/></div>`
|
|
||||||
);
|
|
||||||
expect(renderToString(qweb, "test", { value: "ok" })).toBe("<div>ok</div>");
|
expect(renderToString(qweb, "test", { value: "ok" })).toBe("<div>ok</div>");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -231,18 +210,12 @@ describe("t-set", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("value priority", () => {
|
test("value priority", () => {
|
||||||
qweb.addTemplate(
|
qweb.addTemplate("test", `<div><t t-set="value" t-value="1">2</t><t t-esc="value"/></div>`);
|
||||||
"test",
|
|
||||||
`<div><t t-set="value" t-value="1">2</t><t t-esc="value"/></div>`
|
|
||||||
);
|
|
||||||
expect(renderToString(qweb, "test")).toBe("<div>1</div>");
|
expect(renderToString(qweb, "test")).toBe("<div>1</div>");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("evaluate value expression", () => {
|
test("evaluate value expression", () => {
|
||||||
qweb.addTemplate(
|
qweb.addTemplate("test", `<div><t t-set="value" t-value="1 + 2"/><t t-esc="value"/></div>`);
|
||||||
"test",
|
|
||||||
`<div><t t-set="value" t-value="1 + 2"/><t t-esc="value"/></div>`
|
|
||||||
);
|
|
||||||
expect(renderToString(qweb, "test")).toBe("<div>3</div>");
|
expect(renderToString(qweb, "test")).toBe("<div>3</div>");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -267,25 +240,19 @@ describe("t-set", () => {
|
|||||||
"test",
|
"test",
|
||||||
`<div><t t-set="value" t-value="somevariable + 2"/><t t-esc="value"/></div>`
|
`<div><t t-set="value" t-value="somevariable + 2"/><t t-esc="value"/></div>`
|
||||||
);
|
);
|
||||||
expect(renderToString(qweb, "test", { somevariable: 43 })).toBe(
|
expect(renderToString(qweb, "test", { somevariable: 43 })).toBe("<div>45</div>");
|
||||||
"<div>45</div>"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("t-if", () => {
|
describe("t-if", () => {
|
||||||
test("boolean value true condition", () => {
|
test("boolean value true condition", () => {
|
||||||
qweb.addTemplate("test", `<div><t t-if="condition">ok</t></div>`);
|
qweb.addTemplate("test", `<div><t t-if="condition">ok</t></div>`);
|
||||||
expect(renderToString(qweb, "test", { condition: true })).toBe(
|
expect(renderToString(qweb, "test", { condition: true })).toBe("<div>ok</div>");
|
||||||
"<div>ok</div>"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("boolean value false condition", () => {
|
test("boolean value false condition", () => {
|
||||||
qweb.addTemplate("test", `<div><t t-if="condition">ok</t></div>`);
|
qweb.addTemplate("test", `<div><t t-if="condition">ok</t></div>`);
|
||||||
expect(renderToString(qweb, "test", { condition: false })).toBe(
|
expect(renderToString(qweb, "test", { condition: false })).toBe("<div></div>");
|
||||||
"<div></div>"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("boolean value condition missing", () => {
|
test("boolean value condition missing", () => {
|
||||||
@@ -302,9 +269,7 @@ describe("t-if", () => {
|
|||||||
<t t-else="">beer</t></div>
|
<t t-else="">beer</t></div>
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
expect(renderToString(qweb, "test", { color: "red" })).toBe(
|
expect(renderToString(qweb, "test", { color: "red" })).toBe("<div>red is dead</div>");
|
||||||
"<div>red is dead</div>"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("boolean value condition else", () => {
|
test("boolean value condition else", () => {
|
||||||
@@ -330,9 +295,7 @@ describe("t-if", () => {
|
|||||||
`
|
`
|
||||||
);
|
);
|
||||||
const result = trim(renderToString(qweb, "test", { condition: false }));
|
const result = trim(renderToString(qweb, "test", { condition: false }));
|
||||||
expect(result).toBe(
|
expect(result).toBe("<div><span>begin</span>fail-else<span>end</span></div>");
|
||||||
"<div><span>begin</span>fail-else<span>end</span></div>"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can use some boolean operators in expressions", () => {
|
test("can use some boolean operators in expressions", () => {
|
||||||
@@ -357,9 +320,7 @@ describe("t-if", () => {
|
|||||||
m: 5,
|
m: 5,
|
||||||
n: 2
|
n: 2
|
||||||
};
|
};
|
||||||
expect(normalize(renderToString(qweb, "test", context))).toBe(
|
expect(normalize(renderToString(qweb, "test", context))).toBe("<div>andormgtnlt</div>");
|
||||||
"<div>andormgtnlt</div>"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -484,10 +445,7 @@ describe("attributes", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("format multiple", () => {
|
test("format multiple", () => {
|
||||||
qweb.addTemplate(
|
qweb.addTemplate("test", `<div t-attf-foo="a {{value1}} is {{value2}} of {{value3}} ]"/>`);
|
||||||
"test",
|
|
||||||
`<div t-attf-foo="a {{value1}} is {{value2}} of {{value3}} ]"/>`
|
|
||||||
);
|
|
||||||
const result = renderToString(qweb, "test", {
|
const result = renderToString(qweb, "test", {
|
||||||
value1: 0,
|
value1: 0,
|
||||||
value2: 1,
|
value2: 1,
|
||||||
@@ -523,19 +481,13 @@ describe("attributes", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("class and t-attf-class with ternary operation", () => {
|
test("class and t-attf-class with ternary operation", () => {
|
||||||
qweb.addTemplate(
|
qweb.addTemplate("test", `<div class="hello" t-attf-class="{{value ? 'world' : ''}}"/>`);
|
||||||
"test",
|
|
||||||
`<div class="hello" t-attf-class="{{value ? 'world' : ''}}"/>`
|
|
||||||
);
|
|
||||||
const result = renderToString(qweb, "test", { value: true });
|
const result = renderToString(qweb, "test", { value: true });
|
||||||
expect(result).toBe(`<div class="hello world"></div>`);
|
expect(result).toBe(`<div class="hello world"></div>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("t-att-class with object", () => {
|
test("t-att-class with object", () => {
|
||||||
qweb.addTemplate(
|
qweb.addTemplate("test", `<div class="static" t-att-class="{a: b, c: d, e: f}"/>`);
|
||||||
"test",
|
|
||||||
`<div class="static" t-att-class="{a: b, c: d, e: f}"/>`
|
|
||||||
);
|
|
||||||
const result = renderToString(qweb, "test", { b: true, d: false, f: true });
|
const result = renderToString(qweb, "test", { b: true, d: false, f: true });
|
||||||
expect(result).toBe(`<div class="static a e"></div>`);
|
expect(result).toBe(`<div class="static a e"></div>`);
|
||||||
});
|
});
|
||||||
@@ -549,6 +501,13 @@ describe("t-call (template calling", () => {
|
|||||||
expect(renderToString(qweb, "caller")).toBe(expected);
|
expect(renderToString(qweb, "caller")).toBe(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("t-call with t-if", () => {
|
||||||
|
qweb.addTemplate("sub", "<span>ok</span>");
|
||||||
|
qweb.addTemplate("caller", '<div><t t-if="flag" t-call="sub"/></div>');
|
||||||
|
const expected = "<div><span>ok</span></div>";
|
||||||
|
expect(renderToString(qweb, "caller", { flag: true })).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
test("t-call not allowed on a non t node", () => {
|
test("t-call not allowed on a non t node", () => {
|
||||||
qweb.addTemplate("_basic-callee", "<t>ok</t>");
|
qweb.addTemplate("_basic-callee", "<t>ok</t>");
|
||||||
qweb.addTemplate("caller", '<div t-call="_basic-callee"/>');
|
qweb.addTemplate("caller", '<div t-call="_basic-callee"/>');
|
||||||
@@ -564,10 +523,7 @@ describe("t-call (template calling", () => {
|
|||||||
|
|
||||||
test("with unused setbody", () => {
|
test("with unused setbody", () => {
|
||||||
qweb.addTemplate("_basic-callee", "<div>ok</div>");
|
qweb.addTemplate("_basic-callee", "<div>ok</div>");
|
||||||
qweb.addTemplate(
|
qweb.addTemplate("caller", '<t t-call="_basic-callee"><t t-set="qux" t-value="3"/></t>');
|
||||||
"caller",
|
|
||||||
'<t t-call="_basic-callee"><t t-set="qux" t-value="3"/></t>'
|
|
||||||
);
|
|
||||||
const expected = "<div>ok</div>";
|
const expected = "<div>ok</div>";
|
||||||
expect(renderToString(qweb, "caller")).toBe(expected);
|
expect(renderToString(qweb, "caller")).toBe(expected);
|
||||||
});
|
});
|
||||||
@@ -673,9 +629,7 @@ describe("foreach", () => {
|
|||||||
</t>
|
</t>
|
||||||
</div>`
|
</div>`
|
||||||
);
|
);
|
||||||
const result = trim(
|
const result = trim(renderToString(qweb, "test", { value: { a: 1, b: 2, c: 3 } }));
|
||||||
renderToString(qweb, "test", { value: { a: 1, b: 2, c: 3 } })
|
|
||||||
);
|
|
||||||
const expected = `<div>[0:a1][1:b2][2:c3]</div>`;
|
const expected = `<div>[0:a1][1:b2][2:c3]</div>`;
|
||||||
expect(result).toBe(expected);
|
expect(result).toBe(expected);
|
||||||
});
|
});
|
||||||
@@ -722,14 +676,8 @@ describe("foreach", () => {
|
|||||||
describe("misc", () => {
|
describe("misc", () => {
|
||||||
test("global", () => {
|
test("global", () => {
|
||||||
qweb.addTemplate("_callee-asc", `<Año t-att-falló="'agüero'" t-raw="0"/>`);
|
qweb.addTemplate("_callee-asc", `<Año t-att-falló="'agüero'" t-raw="0"/>`);
|
||||||
qweb.addTemplate(
|
qweb.addTemplate("_callee-uses-foo", `<span t-esc="foo">foo default</span>`);
|
||||||
"_callee-uses-foo",
|
qweb.addTemplate("_callee-asc-toto", `<div t-raw="toto">toto default</div>`);
|
||||||
`<span t-esc="foo">foo default</span>`
|
|
||||||
);
|
|
||||||
qweb.addTemplate(
|
|
||||||
"_callee-asc-toto",
|
|
||||||
`<div t-raw="toto">toto default</div>`
|
|
||||||
);
|
|
||||||
qweb.addTemplate(
|
qweb.addTemplate(
|
||||||
"caller",
|
"caller",
|
||||||
`
|
`
|
||||||
@@ -842,10 +790,7 @@ describe("t-on", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("can bind handlers with object arguments", () => {
|
test("can bind handlers with object arguments", () => {
|
||||||
qweb.addTemplate(
|
qweb.addTemplate("test", `<button t-on-click="add({val: 5})">Click</button>`);
|
||||||
"test",
|
|
||||||
`<button t-on-click="add({val: 5})">Click</button>`
|
|
||||||
);
|
|
||||||
let a = 1;
|
let a = 1;
|
||||||
const node = renderToDOM(
|
const node = renderToDOM(
|
||||||
qweb,
|
qweb,
|
||||||
@@ -863,10 +808,7 @@ describe("t-on", () => {
|
|||||||
|
|
||||||
test("can bind handlers with empty object", () => {
|
test("can bind handlers with empty object", () => {
|
||||||
expect.assertions(2);
|
expect.assertions(2);
|
||||||
qweb.addTemplate(
|
qweb.addTemplate("test", `<button t-on-click="doSomething({})">Click</button>`);
|
||||||
"test",
|
|
||||||
`<button t-on-click="doSomething({})">Click</button>`
|
|
||||||
);
|
|
||||||
const node = renderToDOM(
|
const node = renderToDOM(
|
||||||
qweb,
|
qweb,
|
||||||
"test",
|
"test",
|
||||||
@@ -882,10 +824,7 @@ describe("t-on", () => {
|
|||||||
|
|
||||||
test("can bind handlers with empty object (with non empty inner string)", () => {
|
test("can bind handlers with empty object (with non empty inner string)", () => {
|
||||||
expect.assertions(2);
|
expect.assertions(2);
|
||||||
qweb.addTemplate(
|
qweb.addTemplate("test", `<button t-on-click="doSomething({ })">Click</button>`);
|
||||||
"test",
|
|
||||||
`<button t-on-click="doSomething({ })">Click</button>`
|
|
||||||
);
|
|
||||||
const node = renderToDOM(
|
const node = renderToDOM(
|
||||||
qweb,
|
qweb,
|
||||||
"test",
|
"test",
|
||||||
@@ -1185,13 +1124,10 @@ describe("whitespace handling", () => {
|
|||||||
|
|
||||||
describe("t-key", () => {
|
describe("t-key", () => {
|
||||||
test("can use t-key directive on a node", () => {
|
test("can use t-key directive on a node", () => {
|
||||||
qweb.addTemplate(
|
qweb.addTemplate("test", `<div t-key="beer.id"><t t-esc="beer.name"/></div>`);
|
||||||
"test",
|
expect(renderToString(qweb, "test", { beer: { id: 12, name: "Chimay Rouge" } })).toBe(
|
||||||
`<div t-key="beer.id"><t t-esc="beer.name"/></div>`
|
"<div>Chimay Rouge</div>"
|
||||||
);
|
);
|
||||||
expect(
|
|
||||||
renderToString(qweb, "test", { beer: { id: 12, name: "Chimay Rouge" } })
|
|
||||||
).toBe("<div>Chimay Rouge</div>");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("t-key directive in a list", () => {
|
test("t-key directive in a list", () => {
|
||||||
@@ -1242,3 +1178,15 @@ describe("debugging", () => {
|
|||||||
console.log = consoleLog;
|
console.log = consoleLog;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("update on event bus", () => {
|
||||||
|
test("two consecutive forceUpdates only causes one listener update", async () => {
|
||||||
|
const fn = jest.fn();
|
||||||
|
qweb.on("update", null, fn);
|
||||||
|
qweb.forceUpdate();
|
||||||
|
qweb.forceUpdate();
|
||||||
|
expect(fn).toBeCalledTimes(0);
|
||||||
|
await nextTick();
|
||||||
|
expect(fn).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
import { compileExpr, tokenize } from "../src/qweb_expressions";
|
import { compileExpr, tokenize } from "../../src/qweb/expression_parser";
|
||||||
|
|
||||||
describe("tokenizer", () => {
|
describe("tokenizer", () => {
|
||||||
test("simple tokens", () => {
|
test("simple tokens", () => {
|
||||||
expect(tokenize("1.3")).toEqual([
|
expect(tokenize("1.3")).toEqual([{ type: "VALUE", value: "1.3" }]);
|
||||||
{ type: "VALUE", value: "1.3" }
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(tokenize("{}")).toEqual([
|
expect(tokenize("{}")).toEqual([
|
||||||
{ type: "LEFT_BRACE", value: "{" },
|
{ type: "LEFT_BRACE", value: "{" },
|
||||||
@@ -29,10 +27,7 @@ describe("tokenizer", () => {
|
|||||||
{ type: "VALUE", value: "2" },
|
{ type: "VALUE", value: "2" },
|
||||||
{ type: "RIGHT_BRACE", value: "}" }
|
{ type: "RIGHT_BRACE", value: "}" }
|
||||||
]);
|
]);
|
||||||
expect(tokenize("a,")).toEqual([
|
expect(tokenize("a,")).toEqual([{ type: "SYMBOL", value: "a" }, { type: "COMMA", value: "," }]);
|
||||||
{ type: "SYMBOL", value: "a" },
|
|
||||||
{ type: "COMMA", value: "," }
|
|
||||||
]);
|
|
||||||
expect(tokenize("][")).toEqual([
|
expect(tokenize("][")).toEqual([
|
||||||
{ type: "RIGHT_BRACKET", value: "]" },
|
{ type: "RIGHT_BRACKET", value: "]" },
|
||||||
{ type: "LEFT_BRACKET", value: "[" }
|
{ type: "LEFT_BRACKET", value: "[" }
|
||||||
@@ -40,11 +35,13 @@ describe("tokenizer", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("various operators", () => {
|
test("various operators", () => {
|
||||||
expect(tokenize(">= <= < >")).toEqual([
|
expect(tokenize(">= <= < > !== !=")).toEqual([
|
||||||
{ type: "OPERATOR", value: ">=" },
|
{ type: "OPERATOR", value: ">=" },
|
||||||
{ type: "OPERATOR", value: "<=" },
|
{ type: "OPERATOR", value: "<=" },
|
||||||
{ type: "OPERATOR", value: "<" },
|
{ type: "OPERATOR", value: "<" },
|
||||||
{ type: "OPERATOR", value: ">" },
|
{ type: "OPERATOR", value: ">" },
|
||||||
|
{ type: "OPERATOR", value: "!==" },
|
||||||
|
{ type: "OPERATOR", value: "!=" }
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -52,25 +49,17 @@ describe("tokenizer", () => {
|
|||||||
expect(() => tokenize("'")).toThrow("Invalid expression");
|
expect(() => tokenize("'")).toThrow("Invalid expression");
|
||||||
expect(() => tokenize("'\\")).toThrow("Invalid expression");
|
expect(() => tokenize("'\\")).toThrow("Invalid expression");
|
||||||
expect(() => tokenize("'\\'")).toThrow("Invalid expression");
|
expect(() => tokenize("'\\'")).toThrow("Invalid expression");
|
||||||
expect(tokenize("'hello ged'")).toEqual([
|
expect(tokenize("'hello ged'")).toEqual([{ type: "VALUE", value: "'hello ged'" }]);
|
||||||
{ type: "VALUE", value: "'hello ged'" }
|
expect(tokenize("'hello \\'ged\\''")).toEqual([{ type: "VALUE", value: "'hello \\'ged\\''" }]);
|
||||||
]);
|
|
||||||
expect(tokenize("'hello \\'ged\\''")).toEqual([
|
|
||||||
{ type: "VALUE", value: "'hello \\'ged\\''" }
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(() => tokenize('"')).toThrow("Invalid expression");
|
expect(() => tokenize('"')).toThrow("Invalid expression");
|
||||||
expect(() => tokenize('"\\"')).toThrow("Invalid expression");
|
expect(() => tokenize('"\\"')).toThrow("Invalid expression");
|
||||||
expect(tokenize('"hello ged"')).toEqual([
|
expect(tokenize('"hello ged"')).toEqual([{ type: "VALUE", value: '"hello ged"' }]);
|
||||||
{ type: "VALUE", value: '"hello ged"' }
|
|
||||||
]);
|
|
||||||
expect(tokenize('"hello ged"}')).toEqual([
|
expect(tokenize('"hello ged"}')).toEqual([
|
||||||
{ type: "VALUE", value: '"hello ged"' },
|
{ type: "VALUE", value: '"hello ged"' },
|
||||||
{ type: "RIGHT_BRACE", value: "}" }
|
{ type: "RIGHT_BRACE", value: "}" }
|
||||||
]);
|
]);
|
||||||
expect(tokenize('"hello \\"ged\\""')).toEqual([
|
expect(tokenize('"hello \\"ged\\""')).toEqual([{ type: "VALUE", value: '"hello \\"ged\\""' }]);
|
||||||
{ type: "VALUE", value: '"hello \\"ged\\""' }
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -111,9 +100,7 @@ describe("expression evaluation", () => {
|
|||||||
test("arrays and objects", () => {
|
test("arrays and objects", () => {
|
||||||
expect(compileExpr("[{b:1}] ", {})).toBe("[{b:1}]");
|
expect(compileExpr("[{b:1}] ", {})).toBe("[{b:1}]");
|
||||||
expect(compileExpr("{a: []} ", {})).toBe("{a:[]}");
|
expect(compileExpr("{a: []} ", {})).toBe("{a:[]}");
|
||||||
expect(compileExpr("[{b:1, c: [1, {d: {e: 3}} ]}] ", {})).toBe(
|
expect(compileExpr("[{b:1, c: [1, {d: {e: 3}} ]}] ", {})).toBe("[{b:1,c:[1,{d:{e:3}}]}]");
|
||||||
"[{b:1,c:[1,{d:{e:3}}]}]"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("dot operator", () => {
|
test("dot operator", () => {
|
||||||
@@ -128,13 +115,9 @@ describe("expression evaluation", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("various binary operators", () => {
|
test("various binary operators", () => {
|
||||||
expect(compileExpr("color == 'black'", {})).toBe(
|
expect(compileExpr("color == 'black'", {})).toBe("context['color']=='black'");
|
||||||
"context['color']=='black'"
|
|
||||||
);
|
|
||||||
expect(compileExpr("a || b", {})).toBe("context['a']||context['b']");
|
expect(compileExpr("a || b", {})).toBe("context['a']||context['b']");
|
||||||
expect(compileExpr("color === 'black'", {})).toBe(
|
expect(compileExpr("color === 'black'", {})).toBe("context['color']==='black'");
|
||||||
"context['color']==='black'"
|
|
||||||
);
|
|
||||||
expect(compileExpr("'li_'+item", {})).toBe("'li_'+context['item']");
|
expect(compileExpr("'li_'+item", {})).toBe("'li_'+context['item']");
|
||||||
expect(compileExpr("state.val > 1", {})).toBe("context['state'].val>1");
|
expect(compileExpr("state.val > 1", {})).toBe("context['state'].val>1");
|
||||||
});
|
});
|
||||||
@@ -162,18 +145,14 @@ describe("expression evaluation", () => {
|
|||||||
expect(compileExpr("a()", {})).toBe("context['a']()");
|
expect(compileExpr("a()", {})).toBe("context['a']()");
|
||||||
expect(compileExpr("a(1)", {})).toBe("context['a'](1)");
|
expect(compileExpr("a(1)", {})).toBe("context['a'](1)");
|
||||||
expect(compileExpr("a(1,2)", {})).toBe("context['a'](1,2)");
|
expect(compileExpr("a(1,2)", {})).toBe("context['a'](1,2)");
|
||||||
expect(compileExpr("a(1,2,{a:[a]})", {})).toBe(
|
expect(compileExpr("a(1,2,{a:[a]})", {})).toBe("context['a'](1,2,{a:[context['a']]})");
|
||||||
"context['a'](1,2,{a:[context['a']]})"
|
|
||||||
);
|
|
||||||
expect(compileExpr("'x'.toUpperCase()", {})).toBe("'x'.toUpperCase()");
|
expect(compileExpr("'x'.toUpperCase()", {})).toBe("'x'.toUpperCase()");
|
||||||
expect(compileExpr("'x'.toUpperCase({a: 3})", {})).toBe(
|
expect(compileExpr("'x'.toUpperCase({a: 3})", {})).toBe("'x'.toUpperCase({a:3})");
|
||||||
"'x'.toUpperCase({a:3})"
|
expect(compileExpr("'x'.toUpperCase(a)", { a: { id: "_v5", expr: "" } })).toBe(
|
||||||
|
"'x'.toUpperCase(_v5)"
|
||||||
|
);
|
||||||
|
expect(compileExpr("'x'.toUpperCase({b: a})", { a: { id: "_v5", expr: "" } })).toBe(
|
||||||
|
"'x'.toUpperCase({b:_v5})"
|
||||||
);
|
);
|
||||||
expect(
|
|
||||||
compileExpr("'x'.toUpperCase(a)", { a: { id: "_v5", expr: "" } })
|
|
||||||
).toBe("'x'.toUpperCase(_v5)");
|
|
||||||
expect(
|
|
||||||
compileExpr("'x'.toUpperCase({b: a})", { a: { id: "_v5", expr: "" } })
|
|
||||||
).toBe("'x'.toUpperCase({b:_v5})");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { Component } from "../../src/component/component";
|
||||||
|
import { Link, LINK_TEMPLATE_NAME } from "../../src/router/Link";
|
||||||
|
import { RouterEnv } from "../../src/router/Router";
|
||||||
|
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
|
||||||
|
import { TestRouter } from "./TestRouter";
|
||||||
|
|
||||||
|
describe("Link component", () => {
|
||||||
|
let fixture: HTMLElement;
|
||||||
|
let env: RouterEnv;
|
||||||
|
let router: TestRouter | null = null;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = makeTestFixture();
|
||||||
|
env = <RouterEnv>makeTestEnv();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fixture.remove();
|
||||||
|
if (router) {
|
||||||
|
router.destroy();
|
||||||
|
}
|
||||||
|
router = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can render simple cases", async () => {
|
||||||
|
env.qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="App">
|
||||||
|
<Link to="'about'">About</Link>
|
||||||
|
</div>
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
class App extends Component<any, any, any> {
|
||||||
|
components = { Link: Link };
|
||||||
|
}
|
||||||
|
|
||||||
|
const routes = [{ name: "about", path: "/about" }, { name: "users", path: "/users" }];
|
||||||
|
|
||||||
|
router = new TestRouter(env, routes, { mode: "history" });
|
||||||
|
router.navigate({ to: "users" });
|
||||||
|
const app = new App(env);
|
||||||
|
await app.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe('<div><a href="/about">About</a></div>');
|
||||||
|
|
||||||
|
expect(window.location.pathname).toBe("/users");
|
||||||
|
fixture.querySelector("a")!.click();
|
||||||
|
await nextTick();
|
||||||
|
expect(window.location.pathname).toBe("/about");
|
||||||
|
expect(fixture.innerHTML).toBe(
|
||||||
|
'<div><a href="/about" class="router-link-active">About</a></div>'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(env.qweb.templates[LINK_TEMPLATE_NAME].fn.toString()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("do not redirect if right clicking", async () => {
|
||||||
|
env.qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="App">
|
||||||
|
<Link to="'about'">About</Link>
|
||||||
|
</div>
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
class App extends Component<any, any, any> {
|
||||||
|
components = { Link: Link };
|
||||||
|
}
|
||||||
|
|
||||||
|
const routes = [{ name: "about", path: "/about" }, { name: "users", path: "/users" }];
|
||||||
|
|
||||||
|
router = new TestRouter(env, routes, { mode: "history" });
|
||||||
|
router.navigate({ to: "users" });
|
||||||
|
const app = new App(env);
|
||||||
|
await app.mount(fixture);
|
||||||
|
|
||||||
|
expect(window.location.pathname).toBe("/users");
|
||||||
|
var evt = new MouseEvent("click", {
|
||||||
|
button: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
fixture.querySelector("a")!.dispatchEvent(evt);
|
||||||
|
await nextTick();
|
||||||
|
expect(window.location.pathname).toBe("/users");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { Router } from "../../src/router/Router";
|
||||||
|
import { QWeb } from "../../src/qweb/index";
|
||||||
|
|
||||||
|
export class TestRouter extends Router {
|
||||||
|
destroy() {
|
||||||
|
window.removeEventListener("popstate", (this as any)._listener);
|
||||||
|
delete QWeb.DIRECTIVE_NAMES.routecomponent;
|
||||||
|
QWeb.DIRECTIVES = QWeb.DIRECTIVES.filter(d => d.name !== "routecomponent");
|
||||||
|
|
||||||
|
// remove component defined inroutes
|
||||||
|
for (let key in QWeb.components) {
|
||||||
|
if (key.startsWith("__component__")) {
|
||||||
|
delete QWeb.components[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Link component can render simple cases 1`] = `
|
||||||
|
"function anonymous(context,extra
|
||||||
|
) {
|
||||||
|
let utils = this.constructor.utils;
|
||||||
|
let owner = context;
|
||||||
|
var h = this.h;
|
||||||
|
let _1 = utils.toObj({'router-link-active':context['isActive']});
|
||||||
|
var _2 = context['href'];
|
||||||
|
let c3 = [], p3 = {key:3,attrs:{href: _2},class:_1,on:{}};
|
||||||
|
var vn3 = h('a', p3, c3);
|
||||||
|
if (!context['navigate']) {
|
||||||
|
throw new Error('Missing handler \\\\'' + 'navigate' + \`\\\\' when evaluating template '__owl__-router-link'\`)
|
||||||
|
}
|
||||||
|
extra.handlers['click' + 3] = extra.handlers['click' + 3] || context['navigate'].bind(owner);
|
||||||
|
p3.on['click'] = extra.handlers['click' + 3];
|
||||||
|
const slot4 = this.slots[context.__owl__.slotId + '_' + 'default'];
|
||||||
|
if (slot4) {
|
||||||
|
slot4(context.__owl__.parent, Object.assign({}, extra, {parentNode: c3, vars: extra.vars, parent: owner}));
|
||||||
|
}
|
||||||
|
return vn3;
|
||||||
|
}"
|
||||||
|
`;
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`router directive t-routecomponent can render parameterized route 1`] = `
|
||||||
|
"function anonymous(context,extra
|
||||||
|
) {
|
||||||
|
let utils = this.constructor.utils;
|
||||||
|
let QWeb = this.constructor;
|
||||||
|
let parent = context;
|
||||||
|
let owner = context;
|
||||||
|
var h = this.h;
|
||||||
|
let c1 = [], p1 = {key:1};
|
||||||
|
var vn1 = h('div', p1, c1);
|
||||||
|
if (context['env'].router.currentRouteName==='book') {
|
||||||
|
//COMPONENT
|
||||||
|
let def3;
|
||||||
|
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
||||||
|
let _2_index = c1.length;
|
||||||
|
c1.push(null);
|
||||||
|
let props4 = {title:context['env'].router.currentParams.title};
|
||||||
|
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
||||||
|
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
||||||
|
def3 = w4.__owl__.renderPromise;
|
||||||
|
} else {
|
||||||
|
w4.destroy();
|
||||||
|
w4 = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!w4) {
|
||||||
|
let componentKey4 = \`__component__book\`;
|
||||||
|
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
|
||||||
|
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
||||||
|
w4 = new W4(parent, props4);
|
||||||
|
parent.__owl__.cmap[4] = w4.__owl__.id;
|
||||||
|
def3 = w4.__prepare();
|
||||||
|
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
||||||
|
} else {
|
||||||
|
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
||||||
|
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
||||||
|
}
|
||||||
|
extra.promises.push(def3);
|
||||||
|
}
|
||||||
|
return vn1;
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`router directive t-routecomponent can render parameterized route with suffixes 1`] = `
|
||||||
|
"function anonymous(context,extra
|
||||||
|
) {
|
||||||
|
let utils = this.constructor.utils;
|
||||||
|
let QWeb = this.constructor;
|
||||||
|
let parent = context;
|
||||||
|
let owner = context;
|
||||||
|
var h = this.h;
|
||||||
|
let c1 = [], p1 = {key:1};
|
||||||
|
var vn1 = h('div', p1, c1);
|
||||||
|
if (context['env'].router.currentRouteName==='book') {
|
||||||
|
//COMPONENT
|
||||||
|
let def3;
|
||||||
|
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
||||||
|
let _2_index = c1.length;
|
||||||
|
c1.push(null);
|
||||||
|
let props4 = {title:context['env'].router.currentParams.title,val:context['env'].router.currentParams.val};
|
||||||
|
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
||||||
|
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
||||||
|
def3 = w4.__owl__.renderPromise;
|
||||||
|
} else {
|
||||||
|
w4.destroy();
|
||||||
|
w4 = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!w4) {
|
||||||
|
let componentKey4 = \`__component__book\`;
|
||||||
|
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
|
||||||
|
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
||||||
|
w4 = new W4(parent, props4);
|
||||||
|
parent.__owl__.cmap[4] = w4.__owl__.id;
|
||||||
|
def3 = w4.__prepare();
|
||||||
|
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
||||||
|
} else {
|
||||||
|
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
||||||
|
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
||||||
|
}
|
||||||
|
extra.promises.push(def3);
|
||||||
|
}
|
||||||
|
return vn1;
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`router directive t-routecomponent can render simple cases 1`] = `
|
||||||
|
"function anonymous(context,extra
|
||||||
|
) {
|
||||||
|
let utils = this.constructor.utils;
|
||||||
|
let QWeb = this.constructor;
|
||||||
|
let parent = context;
|
||||||
|
let owner = context;
|
||||||
|
var h = this.h;
|
||||||
|
let c1 = [], p1 = {key:1};
|
||||||
|
var vn1 = h('div', p1, c1);
|
||||||
|
if (context['env'].router.currentRouteName==='about') {
|
||||||
|
//COMPONENT
|
||||||
|
let def3;
|
||||||
|
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
||||||
|
let _2_index = c1.length;
|
||||||
|
c1.push(null);
|
||||||
|
let props4 = {};
|
||||||
|
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
||||||
|
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
||||||
|
def3 = w4.__owl__.renderPromise;
|
||||||
|
} else {
|
||||||
|
w4.destroy();
|
||||||
|
w4 = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!w4) {
|
||||||
|
let componentKey4 = \`__component__about\`;
|
||||||
|
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
|
||||||
|
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
||||||
|
w4 = new W4(parent, props4);
|
||||||
|
parent.__owl__.cmap[4] = w4.__owl__.id;
|
||||||
|
def3 = w4.__prepare();
|
||||||
|
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
||||||
|
} else {
|
||||||
|
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
||||||
|
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
||||||
|
}
|
||||||
|
extra.promises.push(def3);
|
||||||
|
}
|
||||||
|
else if (context['env'].router.currentRouteName==='users') {
|
||||||
|
//COMPONENT
|
||||||
|
let def6;
|
||||||
|
let w7 = 7 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[7]] : false;
|
||||||
|
let _5_index = c1.length;
|
||||||
|
c1.push(null);
|
||||||
|
let props7 = {};
|
||||||
|
if (w7 && w7.__owl__.renderPromise && !w7.__owl__.vnode) {
|
||||||
|
if (utils.shallowEqual(props7, w7.__owl__.renderProps)) {
|
||||||
|
def6 = w7.__owl__.renderPromise;
|
||||||
|
} else {
|
||||||
|
w7.destroy();
|
||||||
|
w7 = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!w7) {
|
||||||
|
let componentKey7 = \`__component__users\`;
|
||||||
|
let W7 = context.components && context.components[componentKey7] || QWeb.components[componentKey7];
|
||||||
|
if (!W7) {throw new Error('Cannot find the definition of component \\"' + componentKey7 + '\\"')}
|
||||||
|
w7 = new W7(parent, props7);
|
||||||
|
parent.__owl__.cmap[7] = w7.__owl__.id;
|
||||||
|
def6 = w7.__prepare();
|
||||||
|
def6 = def6.then(vnode=>{let pvnode=h(vnode.sel, {key: 7, hook: {insert(vn) {let nvn=w7.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w7.destroy();}}});c1[_5_index]=pvnode;w7.__owl__.pvnode = pvnode;});
|
||||||
|
} else {
|
||||||
|
def6 = def6 || w7.__updateProps(props7, extra.forceUpdate, extra.patchQueue);
|
||||||
|
def6 = def6.then(()=>{if (w7.__owl__.isDestroyed) {return};let pvnode=w7.__owl__.pvnode;c1[_5_index]=pvnode;});
|
||||||
|
}
|
||||||
|
extra.promises.push(def6);
|
||||||
|
}
|
||||||
|
return vn1;
|
||||||
|
}"
|
||||||
|
`;
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import { Component } from "../../src/component/component";
|
||||||
|
import { RouterEnv } from "../../src/router/Router";
|
||||||
|
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
|
||||||
|
import { TestRouter } from "./TestRouter";
|
||||||
|
|
||||||
|
describe("router directive t-routecomponent", () => {
|
||||||
|
let fixture: HTMLElement;
|
||||||
|
let env: RouterEnv;
|
||||||
|
let router: TestRouter | null = null;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = makeTestFixture();
|
||||||
|
env = <RouterEnv>makeTestEnv();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fixture.remove();
|
||||||
|
if (router) {
|
||||||
|
router.destroy();
|
||||||
|
}
|
||||||
|
router = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can render simple cases", async () => {
|
||||||
|
env.qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="App">
|
||||||
|
<t t-routecomponent="1" />
|
||||||
|
</div>
|
||||||
|
<span t-name="About">About</span>
|
||||||
|
<span t-name="Users">Users</span>
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
class About extends Component<any, any, any> {}
|
||||||
|
class Users extends Component<any, any, any> {}
|
||||||
|
class App extends Component<any, any, any> {
|
||||||
|
components = { About, Users };
|
||||||
|
}
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{ name: "about", path: "/about", component: About },
|
||||||
|
{ name: "users", path: "/users", component: Users }
|
||||||
|
];
|
||||||
|
|
||||||
|
router = new TestRouter(env, routes, { mode: "history" });
|
||||||
|
await router.navigate({ to: "about" });
|
||||||
|
const app = new App(env);
|
||||||
|
await app.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>About</span></div>");
|
||||||
|
|
||||||
|
await router.navigate({ to: "users" });
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>Users</span></div>");
|
||||||
|
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can render parameterized route", async () => {
|
||||||
|
env.qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="App">
|
||||||
|
<t t-routecomponent="1" />
|
||||||
|
</div>
|
||||||
|
<span t-name="Book">Book <t t-esc="props.title"/></span>
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
class Book extends Component<any, any, any> {}
|
||||||
|
class App extends Component<any, any, any> {
|
||||||
|
components = { Book };
|
||||||
|
}
|
||||||
|
|
||||||
|
const routes = [{ name: "book", path: "/book/{{title}}", component: Book }];
|
||||||
|
router = new TestRouter(env, routes, { mode: "history" });
|
||||||
|
await router.navigate({ to: "book", params: { title: "1984" } });
|
||||||
|
const app = new App(env);
|
||||||
|
await app.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>Book 1984</span></div>");
|
||||||
|
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can render parameterized route with suffixes", async () => {
|
||||||
|
env.qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="App">
|
||||||
|
<t t-routecomponent="1" />
|
||||||
|
</div>
|
||||||
|
<span t-name="Book">Book <t t-esc="props.title"/>|<t t-esc="incVal"/></span>
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
class Book extends Component<any, any, any> {
|
||||||
|
get incVal() {
|
||||||
|
return this.props.val + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class App extends Component<any, any, any> {
|
||||||
|
components = { Book };
|
||||||
|
}
|
||||||
|
|
||||||
|
const routes = [{ name: "book", path: "/book/{{title}}/{{val.number}}", component: Book }];
|
||||||
|
router = new TestRouter(env, routes, { mode: "history" });
|
||||||
|
await router.navigate({ to: "book", params: { title: "1984", val: "123" } });
|
||||||
|
const app = new App(env);
|
||||||
|
await app.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>Book 1984|124</span></div>");
|
||||||
|
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
import { Destination, Router, RouterEnv, Route } from "../../src/router/Router";
|
||||||
|
import { makeTestEnv } from "../helpers";
|
||||||
|
import { TestRouter } from "./TestRouter";
|
||||||
|
|
||||||
|
let env: RouterEnv;
|
||||||
|
let router: TestRouter | null = null;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
env = <RouterEnv>makeTestEnv();
|
||||||
|
window.history.pushState({}, "/", "/");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (router) {
|
||||||
|
router.destroy();
|
||||||
|
}
|
||||||
|
router = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("router miscellaneous", () => {
|
||||||
|
test("validate routes shape", () => {
|
||||||
|
expect(() => {
|
||||||
|
router = new TestRouter(env, [
|
||||||
|
{ name: "someroute", path: "/some/path", redirect: { abc: "hey" } as Destination }
|
||||||
|
]);
|
||||||
|
}).toThrow(`Invalid destination: {"abc":"hey"}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("routeToPath", () => {
|
||||||
|
const routeToPath = Router.prototype["routeToPath"];
|
||||||
|
test("simple non parameterized path", () => {
|
||||||
|
expect(routeToPath({path: "/abc"} as Route, {})).toBe("/abc");
|
||||||
|
expect(routeToPath({path: "/abc/def"} as Route, {})).toBe("/abc/def");
|
||||||
|
expect(routeToPath({path: "/abc"} as Route, { val: 12 })).toBe("/abc");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simple parameterized path", () => {
|
||||||
|
expect(routeToPath({path: "/abc/{{def}}"} as Route, { def: 34 })).toBe("/abc/34");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("destToPath", () => {
|
||||||
|
test("validate destination shape", () => {
|
||||||
|
router = new TestRouter(env, [{ name: "someroute", path: "/some/path" }]);
|
||||||
|
expect(() => {
|
||||||
|
router!.destToPath({ abc: 123 } as Destination);
|
||||||
|
}).toThrow('Invalid destination: {"abc":123}');
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
router!.destToPath({ to: "someroute" } as Destination);
|
||||||
|
}).not.toThrow();
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
router!.destToPath({ path: "/someroute", to: "otherroute" } as Destination);
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getRouteParams", () => {
|
||||||
|
const getRouteParams = Router.prototype["getRouteParams"];
|
||||||
|
test("properly match simple routes", () => {
|
||||||
|
// simple route
|
||||||
|
expect(getRouteParams({path: "/home"} as Route, "/home")).toEqual({});
|
||||||
|
|
||||||
|
// no match
|
||||||
|
expect(getRouteParams({path: "/home"} as Route, "/otherpath")).toEqual(false);
|
||||||
|
|
||||||
|
// fallback route
|
||||||
|
expect(getRouteParams({path: "*"} as Route, "somepath")).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("match some parameterized routes", () => {
|
||||||
|
expect(getRouteParams({path: "/invoices/{{id}}"} as Route, "/invoices/3")).toEqual({
|
||||||
|
id: "3"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can convert to number if needed", () => {
|
||||||
|
expect(getRouteParams({path: "/invoices/{{id.number}}"} as Route, "/invoices/3")).toEqual({
|
||||||
|
id: 3
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("redirect", () => {
|
||||||
|
test("can redirect to other route", async () => {
|
||||||
|
router = new TestRouter(env, [
|
||||||
|
{ name: "routea", path: "/some/path" },
|
||||||
|
{ name: "routeb", path: "/some/other/path", redirect: { to: "routea" } }
|
||||||
|
]);
|
||||||
|
|
||||||
|
await router.start();
|
||||||
|
expect(window.location.pathname).toBe("/");
|
||||||
|
await router.navigate({ to: "routeb" });
|
||||||
|
expect(window.location.pathname).toBe("/some/path");
|
||||||
|
expect(router.currentRouteName).toBe("routea");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can redirect to other path", async () => {
|
||||||
|
router = new TestRouter(env, [
|
||||||
|
{ name: "routea", path: "/some/path" },
|
||||||
|
{ name: "routeb", path: "/some/other/path", redirect: { path: "/some/path" } }
|
||||||
|
]);
|
||||||
|
router.navigate = jest.fn(router.navigate);
|
||||||
|
|
||||||
|
await router.start();
|
||||||
|
await router.navigate({ to: "routeb" });
|
||||||
|
|
||||||
|
// once because we ask for it, once because it is redirected
|
||||||
|
// expect(router.navigate).toBeCalledTimes(2);
|
||||||
|
expect(window.location.pathname).toBe("/some/path");
|
||||||
|
expect(router.currentRouteName).toBe("routea");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("beforeRouteEnter", () => {
|
||||||
|
test("navigation guard is called and properly handle return true", async () => {
|
||||||
|
expect(window.location.pathname).not.toBe("/some/path");
|
||||||
|
const guard = jest.fn(() => true);
|
||||||
|
router = new TestRouter(env, [{ name: "route", path: "/some/path", beforeRouteEnter: guard }]);
|
||||||
|
|
||||||
|
await router.start();
|
||||||
|
await router.navigate({ to: "route" });
|
||||||
|
expect(guard).toBeCalledTimes(1);
|
||||||
|
expect(window.location.pathname).toBe("/some/path");
|
||||||
|
expect(router.currentRouteName).toBe("route");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("navigation is cancelled if guard return false", async () => {
|
||||||
|
expect(window.location.pathname).toBe("/");
|
||||||
|
const guard = jest.fn(() => false);
|
||||||
|
router = new TestRouter(env, [
|
||||||
|
{ name: "routea", path: "/some/patha"},
|
||||||
|
{ name: "routeb", path: "/some/pathb", beforeRouteEnter: guard }
|
||||||
|
]);
|
||||||
|
|
||||||
|
await router.start();
|
||||||
|
await router.navigate({ to: "routea" });
|
||||||
|
expect(window.location.pathname).toBe("/some/patha");
|
||||||
|
const result = await router.navigate({ to: "routeb" });
|
||||||
|
expect(result).toBe(false);
|
||||||
|
expect(guard).toBeCalledTimes(1);
|
||||||
|
expect(window.location.pathname).toBe("/some/patha");
|
||||||
|
expect(router.currentRouteName).toBe("routea");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("navigation is redirected if guard decides so", async () => {
|
||||||
|
expect(window.location.pathname).toBe("/");
|
||||||
|
const guard = jest.fn(() => {return {to: "routec"}});
|
||||||
|
router = new TestRouter(env, [
|
||||||
|
{ name: "routea", path: "/some/patha"},
|
||||||
|
{ name: "routeb", path: "/some/pathb", beforeRouteEnter: guard },
|
||||||
|
{ name: "routec", path: "/some/pathc"},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await router.start();
|
||||||
|
const result = await router.navigate({ to: "routea" });
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(window.location.pathname).toBe("/some/patha");
|
||||||
|
await router.navigate({ to: "routeb" });
|
||||||
|
expect(guard).toBeCalledTimes(1);
|
||||||
|
expect(window.location.pathname).toBe("/some/pathc");
|
||||||
|
expect(router.currentRouteName).toBe("routec");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("navigation is initially redirected if guard decides so", async () => {
|
||||||
|
expect(window.location.pathname).toBe("/");
|
||||||
|
const guard = jest.fn(() => {return {to: "otherroute"}});
|
||||||
|
router = new TestRouter(env, [
|
||||||
|
{ name: "landing", path: "/", beforeRouteEnter: guard},
|
||||||
|
{ name: "otherroute", path: "/some/other/route"}
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(window.location.pathname).toBe("/");
|
||||||
|
|
||||||
|
await router.start();
|
||||||
|
expect(window.location.pathname).toBe("/some/other/route");
|
||||||
|
});
|
||||||
|
});
|
||||||
-1160
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,912 @@
|
|||||||
|
import { Component, Env } from "../../src/component/component";
|
||||||
|
import { ConnectedComponent } from "../../src/store/connected_component";
|
||||||
|
import { Store } from "../../src/store/store";
|
||||||
|
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
|
||||||
|
|
||||||
|
describe("connecting a component to store", () => {
|
||||||
|
let fixture: HTMLElement;
|
||||||
|
let env: Env;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = makeTestFixture();
|
||||||
|
env = makeTestEnv();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fixture.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("connecting a component works", async () => {
|
||||||
|
env.qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="App">
|
||||||
|
<t t-foreach="props.todos" t-as="todo" >
|
||||||
|
<Todo msg="todo.msg" t-key="todo"/>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
<span t-name="Todo"><t t-esc="props.msg"/></span>
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
class App extends ConnectedComponent<any, any, any> {
|
||||||
|
components = { Todo };
|
||||||
|
static mapStoreToProps(s) {
|
||||||
|
return { todos: s.todos };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Todo extends Component<any, any, any> {}
|
||||||
|
const state = { todos: [] };
|
||||||
|
const mutations = {
|
||||||
|
addTodo({ state }, msg) {
|
||||||
|
state.todos.push({ msg });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const store = new Store({ state, mutations });
|
||||||
|
(<any>env).store = store;
|
||||||
|
const app = new App(env);
|
||||||
|
|
||||||
|
await app.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toMatchSnapshot();
|
||||||
|
|
||||||
|
store.commit("addTodo", "hello");
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("deep and shallow connecting a component", async () => {
|
||||||
|
env.qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="App">
|
||||||
|
<span t-foreach="props.todos" t-as="todo" t-key="todo">
|
||||||
|
<t t-esc="todo.title"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
const state = { todos: [{ title: "Kasteel" }] };
|
||||||
|
const mutations = {
|
||||||
|
edit({ state }, title) {
|
||||||
|
state.todos[0].title = title;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const store = new Store({ state, mutations });
|
||||||
|
|
||||||
|
class App extends ConnectedComponent<any, any, any> {
|
||||||
|
static mapStoreToProps(s) {
|
||||||
|
return { todos: s.todos };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class DeepTodoApp extends App {
|
||||||
|
deep = true;
|
||||||
|
}
|
||||||
|
class ShallowTodoApp extends App {
|
||||||
|
deep = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
(<any>env).store = store;
|
||||||
|
const deepTodoApp = new DeepTodoApp(env);
|
||||||
|
const shallowTodoApp = new ShallowTodoApp(env);
|
||||||
|
|
||||||
|
await deepTodoApp.mount(fixture);
|
||||||
|
|
||||||
|
const shallowFix = makeTestFixture();
|
||||||
|
await shallowTodoApp.mount(shallowFix);
|
||||||
|
|
||||||
|
expect(fixture.innerHTML).toMatchSnapshot();
|
||||||
|
expect(shallowFix.innerHTML).toMatchSnapshot();
|
||||||
|
|
||||||
|
store.commit("edit", "Bertinchamps");
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toMatchSnapshot();
|
||||||
|
expect(shallowFix.innerHTML).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("connecting a component to a local store", async () => {
|
||||||
|
env.qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="App">
|
||||||
|
<t t-foreach="props.todos" t-as="todo">
|
||||||
|
<Todo msg="todo.msg" t-key="todo" />
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
<span t-name="Todo"><t t-esc="props.msg"/></span>
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
class Todo extends Component<any, any, any> {}
|
||||||
|
|
||||||
|
(<any>env).store = new Store({});
|
||||||
|
const store = new Store({
|
||||||
|
state: { todos: [] },
|
||||||
|
mutations: {
|
||||||
|
addTodo({ state }, msg) {
|
||||||
|
state.todos.push({ msg });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
class App extends ConnectedComponent<any, any, any> {
|
||||||
|
components = { Todo };
|
||||||
|
static mapStoreToProps(s) {
|
||||||
|
return { todos: s.todos };
|
||||||
|
}
|
||||||
|
getStore() {
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const app = new App(env);
|
||||||
|
|
||||||
|
await app.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toMatchSnapshot();
|
||||||
|
|
||||||
|
(<any>app.__owl__).store.commit("addTodo", "hello");
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("connected child components with custom hooks", async () => {
|
||||||
|
let steps: any = [];
|
||||||
|
env.qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="Parent">
|
||||||
|
<Child t-if="state.child" />
|
||||||
|
</div>
|
||||||
|
<div t-name="Child"/>
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
class Child extends ConnectedComponent<any, any, any> {
|
||||||
|
static mapStoreToProps(s) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
mounted() {
|
||||||
|
steps.push("child:mounted");
|
||||||
|
}
|
||||||
|
willUnmount() {
|
||||||
|
steps.push("child:willUnmount");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Component<any, any, any> {
|
||||||
|
components = { Child };
|
||||||
|
|
||||||
|
constructor(env: Env) {
|
||||||
|
super(env);
|
||||||
|
this.state = { child: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = new Store({ state: {} });
|
||||||
|
(<any>env).store = store;
|
||||||
|
const parent = new Parent(env);
|
||||||
|
|
||||||
|
await parent.mount(fixture);
|
||||||
|
expect(steps).toEqual(["child:mounted"]);
|
||||||
|
|
||||||
|
parent.state.child = false;
|
||||||
|
await nextTick();
|
||||||
|
expect(steps).toEqual(["child:mounted", "child:willUnmount"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("mapStoreToProps receives ownprops as second argument", async () => {
|
||||||
|
const state = { todos: [{ id: 1, text: "jupiler" }] };
|
||||||
|
let nextId = 2;
|
||||||
|
const mutations = {
|
||||||
|
addTodo({ state }, text) {
|
||||||
|
state.todos.push({ text, id: nextId++ });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const store = new Store({ state, mutations });
|
||||||
|
|
||||||
|
env.qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<span t-name="TodoItem"><t t-esc="props.text"/></span>
|
||||||
|
<div t-name="TodoList">
|
||||||
|
<t t-foreach="props.todos" t-as="todo">
|
||||||
|
<TodoItem id="todo.id" t-key="todo.id"/>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
class TodoItem extends ConnectedComponent<any, any, any> {
|
||||||
|
static mapStoreToProps(state, props) {
|
||||||
|
const todo = state.todos.find(t => t.id === props.id);
|
||||||
|
return todo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TodoList extends ConnectedComponent<any, any, any> {
|
||||||
|
components = { TodoItem };
|
||||||
|
static mapStoreToProps(state) {
|
||||||
|
return { todos: state.todos };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(<any>env).store = store;
|
||||||
|
const app = new TodoList(env);
|
||||||
|
|
||||||
|
await app.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
|
||||||
|
|
||||||
|
store.commit("addTodo", "hoegaarden");
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>jupiler</span><span>hoegaarden</span></div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("mapStoreToProps receives store getters as third argument", async () => {
|
||||||
|
const state = {
|
||||||
|
importantID: 1,
|
||||||
|
todos: [{ id: 1, text: "jupiler" }, { id: 2, text: "bertinchamps" }]
|
||||||
|
};
|
||||||
|
const getters = {
|
||||||
|
importantTodoText({ state }) {
|
||||||
|
return state.todos.find(todo => todo.id === state.importantID).text;
|
||||||
|
},
|
||||||
|
text({ state }, id) {
|
||||||
|
return state.todos.find(todo => todo.id === id).text;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const store = new Store({ state, getters });
|
||||||
|
|
||||||
|
env.qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="TodoItem">
|
||||||
|
<span><t t-esc="props.activeTodoText"/></span>
|
||||||
|
<span><t t-esc="props.importantTodoText"/></span>
|
||||||
|
</div>
|
||||||
|
<div t-name="TodoList">
|
||||||
|
<t t-foreach="props.todos" t-as="todo">
|
||||||
|
<TodoItem id="todo.id" t-key="todo.id"/>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
|
||||||
|
class TodoItem extends ConnectedComponent<any, any, any> {
|
||||||
|
static mapStoreToProps(state, props, getters) {
|
||||||
|
const todo = state.todos.find(t => t.id === props.id);
|
||||||
|
return {
|
||||||
|
activeTodoText: getters.text(todo.id),
|
||||||
|
importantTodoText: getters.importantTodoText()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TodoList extends ConnectedComponent<any, any, any> {
|
||||||
|
components = { TodoItem };
|
||||||
|
static mapStoreToProps(state) {
|
||||||
|
return { todos: state.todos };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(<any>env).store = store;
|
||||||
|
const app = new TodoList(env);
|
||||||
|
|
||||||
|
await app.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe(
|
||||||
|
"<div><div><span>jupiler</span><span>jupiler</span></div><div><span>bertinchamps</span><span>jupiler</span></div></div>"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("connected component is updated when props are updated", async () => {
|
||||||
|
env.qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<span t-name="Beer"><t t-esc="props.name"/></span>
|
||||||
|
<div t-name="App">
|
||||||
|
<Beer id="state.beerId"/>
|
||||||
|
</div>
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
|
||||||
|
class Beer extends ConnectedComponent<any, any, any> {
|
||||||
|
static mapStoreToProps(state, props) {
|
||||||
|
return state.beers[props.id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class App extends Component<any, any, any> {
|
||||||
|
components = { Beer };
|
||||||
|
state = { beerId: 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = { beers: { 1: { name: "jupiler" }, 2: { name: "kwak" } } };
|
||||||
|
const store = new Store({ state });
|
||||||
|
(<any>env).store = store;
|
||||||
|
const app = new App(env);
|
||||||
|
|
||||||
|
await app.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
|
||||||
|
|
||||||
|
app.state.beerId = 2;
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>kwak</span></div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("connected component is updated when store is changed", async () => {
|
||||||
|
env.qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="App">
|
||||||
|
<span t-foreach="props.beers" t-as="beer" t-key="beer.name"><t t-esc="beer.name"/></span>
|
||||||
|
</div>
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
|
||||||
|
class App extends ConnectedComponent<any, any, any> {
|
||||||
|
static mapStoreToProps(state) {
|
||||||
|
return { beers: state.beers, otherKey: 1 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mutations = {
|
||||||
|
addBeer({ state }, name) {
|
||||||
|
state.beers.push({ name });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = { beers: [{ name: "jupiler" }] };
|
||||||
|
const store = new Store({ state, mutations });
|
||||||
|
(<any>env).store = store;
|
||||||
|
|
||||||
|
const app = new App(env);
|
||||||
|
|
||||||
|
await app.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
|
||||||
|
|
||||||
|
store.commit("addBeer", "kwak");
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>jupiler</span><span>kwak</span></div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("connected component with undefined, null and string props", async () => {
|
||||||
|
env.qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="Beer">
|
||||||
|
<span>taster:<t t-esc="props.taster"/></span>
|
||||||
|
<span t-if="props.selected">selected:<t t-esc="props.selected.name"/></span>
|
||||||
|
<span t-if="props.consumed">consumed:<t t-esc="props.consumed.name"/></span>
|
||||||
|
</div>
|
||||||
|
<div t-name="App">
|
||||||
|
<Beer id="state.beerId"/>
|
||||||
|
</div>
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
|
||||||
|
class Beer extends ConnectedComponent<any, any, any> {
|
||||||
|
static mapStoreToProps(state, props) {
|
||||||
|
return {
|
||||||
|
selected: state.beers[props.id],
|
||||||
|
consumed: state.beers[state.consumedID] || null,
|
||||||
|
taster: state.taster
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class App extends Component<any, any, any> {
|
||||||
|
components = { Beer };
|
||||||
|
state = { beerId: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const mutations = {
|
||||||
|
consume({ state }, beerId) {
|
||||||
|
state.consumedID = beerId;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const state = {
|
||||||
|
beers: {
|
||||||
|
1: { name: "jupiler" }
|
||||||
|
},
|
||||||
|
consumedID: null,
|
||||||
|
taster: "aaron"
|
||||||
|
};
|
||||||
|
const store = new Store({ state, mutations });
|
||||||
|
(<any>env).store = store;
|
||||||
|
const app = new App(env);
|
||||||
|
|
||||||
|
await app.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><div><span>taster:aaron</span></div></div>");
|
||||||
|
|
||||||
|
app.state.beerId = 1;
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe(
|
||||||
|
"<div><div><span>taster:aaron</span><span>selected:jupiler</span></div></div>"
|
||||||
|
);
|
||||||
|
|
||||||
|
store.commit("consume", 1);
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe(
|
||||||
|
"<div><div><span>taster:aaron</span><span>selected:jupiler</span><span>consumed:jupiler</span></div></div>"
|
||||||
|
);
|
||||||
|
|
||||||
|
app.state.beerId = 0;
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe(
|
||||||
|
"<div><div><span>taster:aaron</span><span>consumed:jupiler</span></div></div>"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("connected component deeply reactive with undefined, null and string props", async () => {
|
||||||
|
env.qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="Beer">
|
||||||
|
<span>taster:<t t-esc="props.taster"/></span>
|
||||||
|
<span t-if="props.selected">selected:<t t-esc="props.selected.name"/></span>
|
||||||
|
<span t-if="props.consumed">consumed:<t t-esc="props.consumed.name"/></span>
|
||||||
|
</div>
|
||||||
|
<div t-name="App">
|
||||||
|
<Beer id="state.beerId"/>
|
||||||
|
</div>
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
|
||||||
|
class Beer extends ConnectedComponent<any, any, any> {
|
||||||
|
static mapStoreToProps(storeState, props) {
|
||||||
|
return {
|
||||||
|
selected: storeState.beers[props.id],
|
||||||
|
consumed: storeState.beers[storeState.consumedID] || null,
|
||||||
|
taster: storeState.taster
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class App extends Component<any, any, any> {
|
||||||
|
components = { Beer };
|
||||||
|
state = { beerId: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const mutations = {
|
||||||
|
changeTaster({ state }, newTaster) {
|
||||||
|
state.taster = newTaster;
|
||||||
|
},
|
||||||
|
consume({ state }, beerId) {
|
||||||
|
state.consumedID = beerId;
|
||||||
|
},
|
||||||
|
renameBeer({ state }, { beerId, name }) {
|
||||||
|
state.beers[beerId].name = name;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const state = {
|
||||||
|
beers: {
|
||||||
|
1: { name: "jupiler" }
|
||||||
|
},
|
||||||
|
consumedID: null,
|
||||||
|
taster: "aaron"
|
||||||
|
};
|
||||||
|
const store = new Store({ state, mutations });
|
||||||
|
(<any>env).store = store;
|
||||||
|
const app = new App(env);
|
||||||
|
|
||||||
|
await app.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><div><span>taster:aaron</span></div></div>");
|
||||||
|
|
||||||
|
app.state.beerId = 1;
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe(
|
||||||
|
"<div><div><span>taster:aaron</span><span>selected:jupiler</span></div></div>"
|
||||||
|
);
|
||||||
|
|
||||||
|
store.commit("renameBeer", { beerId: 1, name: "kwak" });
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe(
|
||||||
|
"<div><div><span>taster:aaron</span><span>selected:kwak</span></div></div>"
|
||||||
|
);
|
||||||
|
|
||||||
|
store.commit("consume", 1);
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe(
|
||||||
|
"<div><div><span>taster:aaron</span><span>selected:kwak</span><span>consumed:kwak</span></div></div>"
|
||||||
|
);
|
||||||
|
|
||||||
|
app.state.beerId = 0;
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe(
|
||||||
|
"<div><div><span>taster:aaron</span><span>consumed:kwak</span></div></div>"
|
||||||
|
);
|
||||||
|
|
||||||
|
store.commit("renameBeer", { beerId: 1, name: "jupiler" });
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe(
|
||||||
|
"<div><div><span>taster:aaron</span><span>consumed:jupiler</span></div></div>"
|
||||||
|
);
|
||||||
|
|
||||||
|
store.commit("changeTaster", "matthieu");
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe(
|
||||||
|
"<div><div><span>taster:matthieu</span><span>consumed:jupiler</span></div></div>"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("correct update order when parent/children are connected", async () => {
|
||||||
|
const steps: string[] = [];
|
||||||
|
|
||||||
|
env.qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="Parent">
|
||||||
|
<Child key="props.current"/>
|
||||||
|
</div>
|
||||||
|
<span t-name="Child"><t t-esc="props.msg"/></span>
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
|
||||||
|
class Parent extends ConnectedComponent<any, any, any> {
|
||||||
|
components = { Child };
|
||||||
|
static mapStoreToProps(s) {
|
||||||
|
steps.push("parent");
|
||||||
|
return { current: s.current, isvisible: s.isvisible };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Child extends ConnectedComponent<any, any, any> {
|
||||||
|
static mapStoreToProps(s, props) {
|
||||||
|
steps.push("child");
|
||||||
|
return { msg: s.msg[props.key] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = { current: "a", msg: { a: "a", b: "b" } };
|
||||||
|
const mutations = {
|
||||||
|
setCurrent({ state }, c) {
|
||||||
|
state.current = c;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const store = new Store({ state, mutations });
|
||||||
|
(<any>env).store = store;
|
||||||
|
const app = new Parent(env);
|
||||||
|
|
||||||
|
await app.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>a</span></div>");
|
||||||
|
expect(steps).toEqual(["parent", "child"]);
|
||||||
|
|
||||||
|
store.commit("setCurrent", "b");
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>b</span></div>");
|
||||||
|
expect(steps).toEqual(["parent", "child", "parent", "child"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("connected parent/children: no double rendering", async () => {
|
||||||
|
const mutations = {
|
||||||
|
editTodo({ state }) {
|
||||||
|
state.todos[1].title = "abc";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const todos = { 1: { id: 1, title: "kikoou" } };
|
||||||
|
const state = {
|
||||||
|
todos
|
||||||
|
};
|
||||||
|
const store = new Store({
|
||||||
|
state,
|
||||||
|
mutations
|
||||||
|
});
|
||||||
|
|
||||||
|
env.qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="TodoApp" class="todoapp">
|
||||||
|
<t t-foreach="Object.values(props.todos)" t-as="todo">
|
||||||
|
<TodoItem t-key="todo.id" id="todo.id"/>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div t-name="TodoItem" class="todo">
|
||||||
|
<t t-esc="props.todo.title"/>
|
||||||
|
<button class="destroy" t-on-click="editTodo">x</button>
|
||||||
|
</div>
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
|
||||||
|
class TodoApp extends ConnectedComponent<any, any, any> {
|
||||||
|
components = { TodoItem };
|
||||||
|
static mapStoreToProps(state) {
|
||||||
|
return {
|
||||||
|
todos: state.todos
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let renderCount = 0;
|
||||||
|
let fCount = 0;
|
||||||
|
|
||||||
|
class TodoItem extends ConnectedComponent<any, any, any> {
|
||||||
|
state = { isEditing: false };
|
||||||
|
static mapStoreToProps(state, ownProps) {
|
||||||
|
fCount++;
|
||||||
|
return {
|
||||||
|
todo: state.todos[ownProps.id]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
editTodo() {
|
||||||
|
this.env.store.commit("editTodo");
|
||||||
|
}
|
||||||
|
__render(...args) {
|
||||||
|
renderCount++;
|
||||||
|
return super.__render(...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(<any>env).store = store;
|
||||||
|
const app = new TodoApp(env);
|
||||||
|
|
||||||
|
await app.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe(
|
||||||
|
'<div class="todoapp"><div class="todo">kikoou<button class="destroy">x</button></div></div>'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(renderCount).toBe(1);
|
||||||
|
expect(fCount).toBe(1);
|
||||||
|
fixture.querySelector("button")!.click();
|
||||||
|
await nextTick();
|
||||||
|
expect(renderCount).toBe(2);
|
||||||
|
expect(fCount).toBe(2);
|
||||||
|
expect(fixture.innerHTML).toBe(
|
||||||
|
'<div class="todoapp"><div class="todo">abc<button class="destroy">x</button></div></div>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("connected parent/children: no rendering if child is destroyed", async () => {
|
||||||
|
const mutations = {
|
||||||
|
removeTodo({ state }) {
|
||||||
|
delete state.todos[1];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const todos = { 1: { id: 1, title: "kikoou" } };
|
||||||
|
const state = {
|
||||||
|
todos
|
||||||
|
};
|
||||||
|
const store = new Store({
|
||||||
|
state,
|
||||||
|
mutations
|
||||||
|
});
|
||||||
|
|
||||||
|
env.qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="TodoApp" class="todoapp">
|
||||||
|
<t t-foreach="Object.values(props.todos)" t-as="todo">
|
||||||
|
<TodoItem t-key="todo.id" id="todo.id"/>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div t-name="TodoItem" class="todo">
|
||||||
|
<t t-esc="props.todo.title"/>
|
||||||
|
<button class="destroy" t-on-click="removeTodo">x</button>
|
||||||
|
</div>
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
|
||||||
|
class TodoApp extends ConnectedComponent<any, any, any> {
|
||||||
|
components = { TodoItem };
|
||||||
|
static mapStoreToProps(state) {
|
||||||
|
return {
|
||||||
|
todos: state.todos
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let renderCount = 0;
|
||||||
|
let fCount = 0;
|
||||||
|
|
||||||
|
class TodoItem extends ConnectedComponent<any, any, any> {
|
||||||
|
state = { isEditing: false };
|
||||||
|
|
||||||
|
static mapStoreToProps(state, ownProps) {
|
||||||
|
fCount++;
|
||||||
|
return {
|
||||||
|
todo: state.todos[ownProps.id]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
removeTodo() {
|
||||||
|
this.env.store.commit("removeTodo");
|
||||||
|
}
|
||||||
|
__render(...args) {
|
||||||
|
renderCount++;
|
||||||
|
return super.__render(...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(<any>env).store = store;
|
||||||
|
const app = new TodoApp(env);
|
||||||
|
|
||||||
|
await app.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe(
|
||||||
|
'<div class="todoapp"><div class="todo">kikoou<button class="destroy">x</button></div></div>'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(renderCount).toBe(1);
|
||||||
|
expect(fCount).toBe(1);
|
||||||
|
fixture.querySelector("button")!.click();
|
||||||
|
await nextTick();
|
||||||
|
expect(renderCount).toBe(1);
|
||||||
|
expect(fCount).toBe(1);
|
||||||
|
expect(fixture.innerHTML).toBe('<div class="todoapp"></div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test("connected component willpatch/patch hooks are called on store updates", async () => {
|
||||||
|
const steps: string[] = [];
|
||||||
|
|
||||||
|
env.qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="App"><t t-esc="props.msg"/></div>
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
|
||||||
|
class App extends ConnectedComponent<any, any, any> {
|
||||||
|
static mapStoreToProps(s) {
|
||||||
|
return { msg: s.msg };
|
||||||
|
}
|
||||||
|
willPatch() {
|
||||||
|
steps.push("willpatch");
|
||||||
|
}
|
||||||
|
patched() {
|
||||||
|
steps.push("patched");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = { msg: "a" };
|
||||||
|
const mutations = {
|
||||||
|
setMsg({ state }, c) {
|
||||||
|
state.msg = c;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const store = new Store({ state, mutations });
|
||||||
|
(<any>env).store = store;
|
||||||
|
const app = new App(env);
|
||||||
|
|
||||||
|
await app.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div>a</div>");
|
||||||
|
|
||||||
|
store.commit("setMsg", "b");
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("<div>b</div>");
|
||||||
|
expect(steps).toEqual(["willpatch", "patched"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("connected components and default values", () => {
|
||||||
|
let fixture: HTMLElement;
|
||||||
|
let env: Env;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = makeTestFixture();
|
||||||
|
env = makeTestEnv();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fixture.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can set default values", async () => {
|
||||||
|
env.qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="Greeter">Hello, <t t-esc="props.recipient"/></div>
|
||||||
|
<div t-name="App"><Greeter/></div>
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
|
||||||
|
class Greeter extends ConnectedComponent<any, any, any> {
|
||||||
|
static defaultProps = { recipient: "John" };
|
||||||
|
}
|
||||||
|
|
||||||
|
class App extends Component<any, any, any> {
|
||||||
|
components = { Greeter };
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = new Store({ state: {} });
|
||||||
|
(<any>env).store = store;
|
||||||
|
const app = new App(env);
|
||||||
|
|
||||||
|
await app.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><div>Hello, John</div></div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can set default values", async () => {
|
||||||
|
env.qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="Greeter">Hello, <t t-esc="props.recipient"/></div>
|
||||||
|
<div t-name="App"><Greeter recipient="props.initialRecipient"/></div>
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
|
||||||
|
class Greeter extends ConnectedComponent<any, any, any> {
|
||||||
|
static defaultProps = { recipient: "John" };
|
||||||
|
}
|
||||||
|
|
||||||
|
class App extends Component<any, any, any> {
|
||||||
|
components = { Greeter };
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = new Store({ state: {} });
|
||||||
|
(<any>env).store = store;
|
||||||
|
const app = new App(env);
|
||||||
|
|
||||||
|
await app.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><div>Hello, John</div></div>");
|
||||||
|
|
||||||
|
await app.__updateProps({ initialRecipient: "James" }, true);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><div>Hello, James</div></div>");
|
||||||
|
|
||||||
|
await app.__updateProps({ initialRecipient: undefined }, true);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><div>Hello, John</div></div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can set default values (v2)", async () => {
|
||||||
|
env.qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="Message">
|
||||||
|
<t t-if="props.showId"><t t-esc="props.messageId"/></t>
|
||||||
|
<t t-esc="props.message.content"/>
|
||||||
|
</div>
|
||||||
|
<div t-name="Thread">
|
||||||
|
<t t-if="props.showMessages">
|
||||||
|
<Message t-foreach="props.thread.messages" t-as="messageId" messageId="messageId" t-key="messageId"/>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
<div t-name="App"><Thread threadId="props.threadId"/></div>
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
|
||||||
|
class Message extends ConnectedComponent<any, any, any> {
|
||||||
|
static defaultProps = { showId: true };
|
||||||
|
static mapStoreToProps = function(state, ownProps) {
|
||||||
|
return {
|
||||||
|
message: state.messages[ownProps.messageId]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class Thread extends ConnectedComponent<any, any, any> {
|
||||||
|
components = { Message };
|
||||||
|
static defaultProps = { showMessages: true };
|
||||||
|
static mapStoreToProps = function(state, ownProps) {
|
||||||
|
const thread = state.threads[ownProps.threadId];
|
||||||
|
return {
|
||||||
|
thread
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class App extends Component<any, any, any> {
|
||||||
|
components = { Thread };
|
||||||
|
static defaultProps = { threadId: 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
threads: {
|
||||||
|
1: {
|
||||||
|
messages: [100, 101]
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
messages: [200]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
100: {
|
||||||
|
content: "Message100"
|
||||||
|
},
|
||||||
|
101: {
|
||||||
|
content: "Message101"
|
||||||
|
},
|
||||||
|
200: {
|
||||||
|
content: "Message200"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const mutations = {
|
||||||
|
changeMessageContent({ state }, messageId, newContent) {
|
||||||
|
state.messages[messageId].content = newContent;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const store = new Store({ state, mutations });
|
||||||
|
(<any>env).store = store;
|
||||||
|
const app = new App(env);
|
||||||
|
|
||||||
|
await app.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe(
|
||||||
|
"<div><div><div>100Message100</div><div>101Message101</div></div></div>"
|
||||||
|
);
|
||||||
|
|
||||||
|
await app.__updateProps({ threadId: 2 }, true);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><div><div>200Message200</div></div></div>");
|
||||||
|
|
||||||
|
store.commit("changeMessageContent", 200, "UpdatedMessage200");
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("<div><div><div>200UpdatedMessage200</div></div></div>");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,578 @@
|
|||||||
|
import { Env } from "../../src/component/component";
|
||||||
|
import { Store } from "../../src/store/store";
|
||||||
|
import { nextMicroTick, nextTick } from "../helpers";
|
||||||
|
|
||||||
|
|
||||||
|
describe("basic use", () => {
|
||||||
|
test("commit a mutation", () => {
|
||||||
|
const state = { n: 1 };
|
||||||
|
const mutations = {
|
||||||
|
inc({ state }, delta) {
|
||||||
|
state.n += delta;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const store = new Store({ state, mutations });
|
||||||
|
|
||||||
|
expect(store.state.n).toBe(1);
|
||||||
|
store.commit("inc", 14);
|
||||||
|
expect(store.state.n).toBe(15);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("dispatch an action", () => {
|
||||||
|
const state = { n: 1 };
|
||||||
|
const mutations = {
|
||||||
|
inc({ state }, delta) {
|
||||||
|
state.n += delta;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const actions = {
|
||||||
|
inc({ commit }, delta) {
|
||||||
|
commit("inc", delta);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const store = new Store({ state, mutations, actions });
|
||||||
|
|
||||||
|
expect(store.state.n).toBe(1);
|
||||||
|
store.dispatch("inc", 14);
|
||||||
|
expect(store.state.n).toBe(15);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("dispatch an action + commit a mutation with positional arguments", () => {
|
||||||
|
const state = { n1: 1, n2: 1, n3: 1 };
|
||||||
|
const mutations = {
|
||||||
|
batchInc({ state }, delta1, delta2, delta3) {
|
||||||
|
state.n1 += delta1;
|
||||||
|
state.n2 += delta2;
|
||||||
|
state.n3 += delta3;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const actions = {
|
||||||
|
batchInc({ commit }, delta1, delta2, delta3) {
|
||||||
|
commit("batchInc", delta1, delta2, delta3);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const store = new Store({ state, mutations, actions });
|
||||||
|
|
||||||
|
expect(store.state.n1).toBe(1);
|
||||||
|
expect(store.state.n2).toBe(1);
|
||||||
|
expect(store.state.n3).toBe(1);
|
||||||
|
store.dispatch("batchInc", 14, 30, 88);
|
||||||
|
expect(store.state.n1).toBe(15);
|
||||||
|
expect(store.state.n2).toBe(31);
|
||||||
|
expect(store.state.n3).toBe(89);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("modifying state outside of mutations trigger error", () => {
|
||||||
|
const state = { n: 1 };
|
||||||
|
const actions = {
|
||||||
|
inc({ state }) {
|
||||||
|
state.n++;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const store = new Store({ state, mutations: {}, actions });
|
||||||
|
|
||||||
|
expect(() => store.dispatch("inc")).toThrow();
|
||||||
|
expect(() => (store.state.n = 15)).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can dispatch an action in an action", () => {
|
||||||
|
const state = { n: 1 };
|
||||||
|
const mutations = {
|
||||||
|
inc({ state }, delta) {
|
||||||
|
state.n += delta;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const actions = {
|
||||||
|
inc({ commit }, delta) {
|
||||||
|
commit("inc", delta);
|
||||||
|
},
|
||||||
|
inc100({ dispatch }) {
|
||||||
|
dispatch("inc", 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const store = new Store({ state, mutations, actions });
|
||||||
|
|
||||||
|
expect(store.state.n).toBe(1);
|
||||||
|
store.dispatch("inc100");
|
||||||
|
expect(store.state.n).toBe(101);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can commit a mutation in a mutation", () => {
|
||||||
|
const state = { n: 1 };
|
||||||
|
const mutations = {
|
||||||
|
inc({ state }) {
|
||||||
|
state.n++;
|
||||||
|
},
|
||||||
|
inc10({ commit }) {
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
commit("inc");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const store = new Store({ state, mutations });
|
||||||
|
|
||||||
|
expect(store.state.n).toBe(1);
|
||||||
|
store.commit("inc10");
|
||||||
|
expect(store.state.n).toBe(11);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("return data from committing a mutation", () => {
|
||||||
|
const state = { n: 1 };
|
||||||
|
const mutations = {
|
||||||
|
inc({ state }) {
|
||||||
|
return ++state.n;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const store = new Store({ state, mutations });
|
||||||
|
|
||||||
|
expect(store.state.n).toBe(1);
|
||||||
|
const res = store.commit("inc");
|
||||||
|
expect(store.state.n).toBe(2);
|
||||||
|
expect(res).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("dispatch allow synchronizing between actions", async () => {
|
||||||
|
const state = { n: 1 };
|
||||||
|
const mutations = {
|
||||||
|
inc({ state }, delta) {
|
||||||
|
state.n += delta;
|
||||||
|
},
|
||||||
|
setN({ state }, n) {
|
||||||
|
state.n = n;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const actions = {
|
||||||
|
async dosomething({ commit, dispatch }) {
|
||||||
|
await dispatch("setTo10");
|
||||||
|
commit("inc", 3);
|
||||||
|
},
|
||||||
|
async setTo10({ commit }) {
|
||||||
|
await Promise.resolve();
|
||||||
|
commit("setN", 10);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const store = new Store({ state, mutations, actions });
|
||||||
|
|
||||||
|
expect(store.state.n).toBe(1);
|
||||||
|
store.dispatch("dosomething");
|
||||||
|
expect(store.state.n).toBe(1);
|
||||||
|
await nextTick();
|
||||||
|
expect(store.state.n).toBe(13);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("return data from dispatching an action", async () => {
|
||||||
|
const state = { n: 1 };
|
||||||
|
const mutations = {
|
||||||
|
inc({ state }, delta) {
|
||||||
|
state.n += delta;
|
||||||
|
},
|
||||||
|
setN({ state }, n) {
|
||||||
|
state.n = n;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const actions = {
|
||||||
|
async dosomething({ commit, dispatch }) {
|
||||||
|
const val = await dispatch("setTo10");
|
||||||
|
commit("inc", val);
|
||||||
|
},
|
||||||
|
async setTo10({ commit }) {
|
||||||
|
await Promise.resolve();
|
||||||
|
commit("setN", 10);
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const store = new Store({ state, mutations, actions });
|
||||||
|
|
||||||
|
expect(store.state.n).toBe(1);
|
||||||
|
store.dispatch("dosomething");
|
||||||
|
expect(store.state.n).toBe(1);
|
||||||
|
await nextTick();
|
||||||
|
expect(store.state.n).toBe(15);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("env is given to actions", () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
const someEnv = <Env>{};
|
||||||
|
const actions = {
|
||||||
|
someaction({ env }) {
|
||||||
|
expect(env).toBe(someEnv);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const store = new Store({ state: {}, actions, env: someEnv });
|
||||||
|
|
||||||
|
store.dispatch("someaction");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can have getters from store", async () => {
|
||||||
|
const state = {
|
||||||
|
beers: {
|
||||||
|
1: {
|
||||||
|
id: 1,
|
||||||
|
name: "bertinchamps",
|
||||||
|
tasterID: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tasters: {
|
||||||
|
1: {
|
||||||
|
id: 1,
|
||||||
|
name: "aaron"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const getters = {
|
||||||
|
beerTasterName({ state }, beerID) {
|
||||||
|
return state.tasters[state.beers[beerID].tasterID].name;
|
||||||
|
},
|
||||||
|
bestBeerName({ state }) {
|
||||||
|
return state.beers[1].name;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const store = new Store({ state, mutations: {}, actions: {}, getters });
|
||||||
|
expect(store.getters).toBeDefined();
|
||||||
|
expect((<any>store.getters).bestBeerName()).toBe("bertinchamps");
|
||||||
|
expect((<any>store.getters).beerTasterName(1)).toBe("aaron");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getters are memoized", async () => {
|
||||||
|
const state = {
|
||||||
|
beers: {
|
||||||
|
1: {
|
||||||
|
id: 1,
|
||||||
|
name: "bertinchamps",
|
||||||
|
tasterID: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tasters: {
|
||||||
|
1: {
|
||||||
|
id: 1,
|
||||||
|
name: "aaron"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let n = 0;
|
||||||
|
const getters = {
|
||||||
|
beerTasterName({ state }, beerID) {
|
||||||
|
n++;
|
||||||
|
return state.tasters[state.beers[beerID].tasterID].name;
|
||||||
|
},
|
||||||
|
bestBeerName({ state }) {
|
||||||
|
n++;
|
||||||
|
return state.beers[1].name;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const store = new Store({ state, mutations: {}, actions: {}, getters });
|
||||||
|
expect((<any>store.getters).bestBeerName()).toBe("bertinchamps");
|
||||||
|
expect((<any>store.getters).beerTasterName(1)).toBe("aaron");
|
||||||
|
expect(n).toBe(2);
|
||||||
|
expect((<any>store.getters).bestBeerName()).toBe("bertinchamps");
|
||||||
|
expect((<any>store.getters).beerTasterName(1)).toBe("aaron");
|
||||||
|
expect(n).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getters taking Array as argument aren't memoized", async () => {
|
||||||
|
const state = {
|
||||||
|
beers: {
|
||||||
|
1: {
|
||||||
|
id: 1,
|
||||||
|
name: "bertinchamps",
|
||||||
|
tasterID: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let n = 0;
|
||||||
|
const getters = {
|
||||||
|
getBeerNames({ state }, beerIDs) {
|
||||||
|
n++;
|
||||||
|
return beerIDs.map(beerID => {
|
||||||
|
return state.beers[beerID].name;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const store = new Store({ state, mutations: {}, actions: {}, getters });
|
||||||
|
expect((<any>store.getters).getBeerNames([1])).toEqual(["bertinchamps"]);
|
||||||
|
expect(n).toBe(1);
|
||||||
|
expect((<any>store.getters).getBeerNames([1])).toEqual(["bertinchamps"]);
|
||||||
|
expect(n).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getters cache is nuked on store changes", async () => {
|
||||||
|
const state = {
|
||||||
|
beers: {
|
||||||
|
1: {
|
||||||
|
id: 1,
|
||||||
|
name: "bertinchamps",
|
||||||
|
tasterID: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tasters: {
|
||||||
|
1: {
|
||||||
|
id: 1,
|
||||||
|
name: "aaron"
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
id: 2,
|
||||||
|
name: "gery"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const mutations = {
|
||||||
|
changeTaster({ state }, { beerID, tasterID }) {
|
||||||
|
state.beers[beerID].tasterID = tasterID;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let n = 0;
|
||||||
|
const getters = {
|
||||||
|
beerTasterName({ state }, beerID) {
|
||||||
|
n++;
|
||||||
|
return state.tasters[state.beers[beerID].tasterID].name;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const store = new Store({
|
||||||
|
state,
|
||||||
|
mutations: mutations,
|
||||||
|
actions: {},
|
||||||
|
getters
|
||||||
|
});
|
||||||
|
expect((<any>store.getters).beerTasterName(1)).toBe("aaron");
|
||||||
|
expect(n).toBe(1);
|
||||||
|
expect((<any>store.getters).beerTasterName(1)).toBe("aaron");
|
||||||
|
expect(n).toBe(1);
|
||||||
|
|
||||||
|
store.commit("changeTaster", { beerID: 1, tasterID: 2 });
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect((<any>store.getters).beerTasterName(1)).toBe("gery");
|
||||||
|
expect(n).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getters cache is disabled during a mutation", async () => {
|
||||||
|
const state = {
|
||||||
|
beers: {
|
||||||
|
1: {
|
||||||
|
id: 1,
|
||||||
|
name: "bertinchamps"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const mutations = {
|
||||||
|
renameBeer({ state, getters }, beerID) {
|
||||||
|
expect(getters.beerName(beerID)).toBe("bertinchamps");
|
||||||
|
state.beers[1].name = "chouffe";
|
||||||
|
expect(getters.beerName(beerID)).toBe("chouffe");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let n = 0;
|
||||||
|
const getters = {
|
||||||
|
beerName({ state }, beerID) {
|
||||||
|
n++;
|
||||||
|
return state.beers[beerID].name;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const store = new Store({
|
||||||
|
state,
|
||||||
|
mutations: mutations,
|
||||||
|
actions: {},
|
||||||
|
getters
|
||||||
|
});
|
||||||
|
|
||||||
|
store.commit("renameBeer", 1);
|
||||||
|
expect((<any>store.getters).beerName(1)).toBe("chouffe");
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(n).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getters given to actions", async () => {
|
||||||
|
expect.assertions(3);
|
||||||
|
const state = {
|
||||||
|
beers: {
|
||||||
|
1: {
|
||||||
|
id: 1,
|
||||||
|
name: "bertinchamps",
|
||||||
|
tasterID: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tasters: {
|
||||||
|
1: {
|
||||||
|
id: 1,
|
||||||
|
name: "aaron"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const getters = {
|
||||||
|
beerTasterName({ state }, beerID) {
|
||||||
|
return state.tasters[state.beers[beerID].tasterID].name;
|
||||||
|
},
|
||||||
|
bestBeerName({ state }) {
|
||||||
|
return state.beers[1].name;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const actions = {
|
||||||
|
action({ getters }) {
|
||||||
|
expect(getters).toBeDefined();
|
||||||
|
expect(getters.bestBeerName()).toBe("bertinchamps");
|
||||||
|
expect(getters.beerTasterName(1)).toBe("aaron");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const store = new Store({ state, mutations: {}, actions, getters });
|
||||||
|
store.dispatch("action");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getters given to mutations", async () => {
|
||||||
|
expect.assertions(3);
|
||||||
|
const state = {
|
||||||
|
beers: {
|
||||||
|
1: {
|
||||||
|
id: 1,
|
||||||
|
name: "bertinchamps",
|
||||||
|
tasterID: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tasters: {
|
||||||
|
1: {
|
||||||
|
id: 1,
|
||||||
|
name: "aaron"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const getters = {
|
||||||
|
beerTasterName({ state }, beerID) {
|
||||||
|
return state.tasters[state.beers[beerID].tasterID].name;
|
||||||
|
},
|
||||||
|
bestBeerName({ state }) {
|
||||||
|
return state.beers[1].name;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const mutations = {
|
||||||
|
mutation({ getters }) {
|
||||||
|
expect(getters).toBeDefined();
|
||||||
|
expect(getters.bestBeerName()).toBe("bertinchamps");
|
||||||
|
expect(getters.beerTasterName(1)).toBe("aaron");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const store = new Store({ state, mutations, actions: {}, getters });
|
||||||
|
store.commit("mutation");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can use getters inside a getter", () => {
|
||||||
|
const getters = {
|
||||||
|
a({ getters }) {
|
||||||
|
return `${getters.b()}${getters.c(1)}`;
|
||||||
|
},
|
||||||
|
b() {
|
||||||
|
return "b";
|
||||||
|
},
|
||||||
|
c({}, i) {
|
||||||
|
return `c${i}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const store = new Store({ getters });
|
||||||
|
|
||||||
|
expect(store.getters.a()).toBe("bc1");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("advanced state properties", () => {
|
||||||
|
test("state in the store is reference equal after mutation", async () => {
|
||||||
|
const mutations = {
|
||||||
|
donothing() {}
|
||||||
|
};
|
||||||
|
const store = new Store({ state:{}, mutations });
|
||||||
|
const state = store.state;
|
||||||
|
store.commit("donothing");
|
||||||
|
expect(store.state).toBe(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can use array properties in mutations", () => {
|
||||||
|
expect.assertions(3);
|
||||||
|
const state = { a: [1, 2, 3] };
|
||||||
|
const mutations = {
|
||||||
|
m({ state }) {
|
||||||
|
expect(state.a.length).toBe(3);
|
||||||
|
const l = state.a.push(53);
|
||||||
|
expect(l).toBe(4);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const store = new Store({ state, mutations });
|
||||||
|
store.commit("m");
|
||||||
|
expect(store.state.a).toEqual([1, 2, 3, 53]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can use object assign in store", async () => {
|
||||||
|
const mutations = {
|
||||||
|
dosomething({ state }) {
|
||||||
|
Object.assign(state.westmalle, { a: 3, b: 4 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const store = new Store({
|
||||||
|
state: { westmalle: { a: 1, b: 2 } },
|
||||||
|
mutations
|
||||||
|
});
|
||||||
|
store.commit("dosomething");
|
||||||
|
expect(store.state.westmalle).toEqual({ a: 3, b: 4 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("aku reactive store state 1", async () => {
|
||||||
|
const mutations = {
|
||||||
|
inc({ state }) {
|
||||||
|
state.counter++;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const state = { counter: 0 };
|
||||||
|
const store = new Store({ state, mutations });
|
||||||
|
expect(store.state.counter).toBe(0);
|
||||||
|
store.commit("inc", {});
|
||||||
|
expect(store.state.counter).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("updates triggered by the store", () => {
|
||||||
|
test("multiple commits trigger one update", async () => {
|
||||||
|
let updateCounter = 0;
|
||||||
|
const state = { n: 1 };
|
||||||
|
const mutations = {
|
||||||
|
inc({ state }, delta) {
|
||||||
|
state.n += delta;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const store = new Store({ state, mutations });
|
||||||
|
store.on("update", null, () => updateCounter++);
|
||||||
|
|
||||||
|
store.commit("inc", 14);
|
||||||
|
expect(updateCounter).toBe(0);
|
||||||
|
store.commit("inc", 50);
|
||||||
|
expect(updateCounter).toBe(0);
|
||||||
|
await nextMicroTick();
|
||||||
|
expect(updateCounter).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("empty commits do not trigger updates", async () => {
|
||||||
|
let updateCounter = 0;
|
||||||
|
const state = { n: 1 };
|
||||||
|
const mutations = {
|
||||||
|
inc({ state }, delta) {
|
||||||
|
state.n += delta;
|
||||||
|
},
|
||||||
|
noop() {},
|
||||||
|
noop2({ state }) {
|
||||||
|
const val = state.n;
|
||||||
|
state.n = val;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const store = new Store({ state, mutations });
|
||||||
|
store.on("update", null, () => updateCounter++);
|
||||||
|
|
||||||
|
store.commit("noop");
|
||||||
|
await nextMicroTick();
|
||||||
|
expect(updateCounter).toBe(0);
|
||||||
|
|
||||||
|
store.commit("inc", 50);
|
||||||
|
await nextMicroTick();
|
||||||
|
expect(updateCounter).toBe(1);
|
||||||
|
|
||||||
|
store.commit("noop2");
|
||||||
|
await nextMicroTick();
|
||||||
|
expect(updateCounter).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
+30
-144
@@ -1,4 +1,5 @@
|
|||||||
import { h, patch, init } from "../src/vdom";
|
import { h, patch } from "../src/vdom";
|
||||||
|
import { init } from "../src/vdom/vdom";
|
||||||
|
|
||||||
function map(list, fn) {
|
function map(list, fn) {
|
||||||
var ret: any[] = [];
|
var ret: any[] = [];
|
||||||
@@ -59,9 +60,7 @@ describe("attributes", function() {
|
|||||||
test("are set correctly when namespaced", function() {
|
test("are set correctly when namespaced", function() {
|
||||||
const vnode1 = h("div", { attrs: { "xlink:href": "#foo" } });
|
const vnode1 = h("div", { attrs: { "xlink:href": "#foo" } });
|
||||||
elm = patch(vnode0, vnode1).elm;
|
elm = patch(vnode0, vnode1).elm;
|
||||||
expect(elm.getAttributeNS("http://www.w3.org/1999/xlink", "href")).toBe(
|
expect(elm.getAttributeNS("http://www.w3.org/1999/xlink", "href")).toBe("#foo");
|
||||||
"#foo"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should not touch class nor id fields", function() {
|
test("should not touch class nor id fields", function() {
|
||||||
@@ -207,12 +206,8 @@ 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(
|
elm = patch(vnode0, h("svg", [h("foreignObject", [h("div", ["I am HTML embedded in SVG"])])]))
|
||||||
vnode0,
|
.elm;
|
||||||
h("svg", [
|
|
||||||
h("foreignObject", [h("div", ["I am HTML embedded in SVG"])])
|
|
||||||
])
|
|
||||||
).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);
|
||||||
@@ -352,13 +347,7 @@ describe("snabbdom", function() {
|
|||||||
elm = patch(vnode0, vnode1).elm;
|
elm = patch(vnode0, vnode1).elm;
|
||||||
expect(elm.children.length).toBe(2);
|
expect(elm.children.length).toBe(2);
|
||||||
elm = patch(vnode1, vnode2).elm;
|
elm = patch(vnode1, vnode2).elm;
|
||||||
expect(map(elm.children, c => c.innerHTML)).toEqual([
|
expect(map(elm.children, c => c.innerHTML)).toEqual(["1", "2", "3", "4", "5"]);
|
||||||
"1",
|
|
||||||
"2",
|
|
||||||
"3",
|
|
||||||
"4",
|
|
||||||
"5"
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("add elements in the middle", function() {
|
test("add elements in the middle", function() {
|
||||||
@@ -368,13 +357,7 @@ describe("snabbdom", function() {
|
|||||||
expect(elm.children.length).toBe(4);
|
expect(elm.children.length).toBe(4);
|
||||||
expect(elm.children.length).toBe(4);
|
expect(elm.children.length).toBe(4);
|
||||||
elm = patch(vnode1, vnode2).elm;
|
elm = patch(vnode1, vnode2).elm;
|
||||||
expect(map(elm.children, c => c.innerHTML)).toEqual([
|
expect(map(elm.children, c => c.innerHTML)).toEqual(["1", "2", "3", "4", "5"]);
|
||||||
"1",
|
|
||||||
"2",
|
|
||||||
"3",
|
|
||||||
"4",
|
|
||||||
"5"
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("add elements at beginning and end", function() {
|
test("add elements at beginning and end", function() {
|
||||||
@@ -383,13 +366,7 @@ describe("snabbdom", function() {
|
|||||||
elm = patch(vnode0, vnode1).elm;
|
elm = patch(vnode0, vnode1).elm;
|
||||||
expect(elm.children.length).toBe(3);
|
expect(elm.children.length).toBe(3);
|
||||||
elm = patch(vnode1, vnode2).elm;
|
elm = patch(vnode1, vnode2).elm;
|
||||||
expect(map(elm.children, c => c.innerHTML)).toEqual([
|
expect(map(elm.children, c => c.innerHTML)).toEqual(["1", "2", "3", "4", "5"]);
|
||||||
"1",
|
|
||||||
"2",
|
|
||||||
"3",
|
|
||||||
"4",
|
|
||||||
"5"
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("adds children to parent with no children", function() {
|
test("adds children to parent with no children", function() {
|
||||||
@@ -588,35 +565,19 @@ describe("snabbdom", function() {
|
|||||||
elm = patch(vnode0, vnode1).elm;
|
elm = patch(vnode0, vnode1).elm;
|
||||||
expect(elm.children.length).toBe(6);
|
expect(elm.children.length).toBe(6);
|
||||||
elm = patch(vnode1, vnode2).elm;
|
elm = patch(vnode1, vnode2).elm;
|
||||||
expect(map(elm.children, c => c.innerHTML)).toEqual([
|
expect(map(elm.children, c => c.innerHTML)).toEqual(["4", "3", "2", "1", "5", "0"]);
|
||||||
"4",
|
|
||||||
"3",
|
|
||||||
"2",
|
|
||||||
"1",
|
|
||||||
"5",
|
|
||||||
"0"
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("supports null/undefined children", function() {
|
test("supports null/undefined children", function() {
|
||||||
const vnode1 = h("i", [0, 1, 2, 3, 4, 5].map(spanNum));
|
const vnode1 = h("i", [0, 1, 2, 3, 4, 5].map(spanNum));
|
||||||
const vnode2 = h(
|
const vnode2 = h(
|
||||||
"i",
|
"i",
|
||||||
[null, 2, undefined, null, 1, 0, null, 5, 4, null, 3, undefined].map(
|
[null, 2, undefined, null, 1, 0, null, 5, 4, null, 3, undefined].map(spanNum)
|
||||||
spanNum
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
elm = patch(vnode0, vnode1).elm;
|
elm = patch(vnode0, vnode1).elm;
|
||||||
expect(elm.children.length).toBe(6);
|
expect(elm.children.length).toBe(6);
|
||||||
elm = patch(vnode1, vnode2).elm;
|
elm = patch(vnode1, vnode2).elm;
|
||||||
expect(map(elm.children, c => c.innerHTML)).toEqual([
|
expect(map(elm.children, c => c.innerHTML)).toEqual(["2", "1", "0", "5", "4", "3"]);
|
||||||
"2",
|
|
||||||
"1",
|
|
||||||
"0",
|
|
||||||
"5",
|
|
||||||
"4",
|
|
||||||
"3"
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("supports all null/undefined children", function() {
|
test("supports all null/undefined children", function() {
|
||||||
@@ -627,14 +588,7 @@ describe("snabbdom", function() {
|
|||||||
elm = patch(vnode1, vnode2).elm;
|
elm = patch(vnode1, vnode2).elm;
|
||||||
expect(elm.children.length).toBe(0);
|
expect(elm.children.length).toBe(0);
|
||||||
elm = patch(vnode2, vnode3).elm;
|
elm = patch(vnode2, vnode3).elm;
|
||||||
expect(map(elm.children, c => c.innerHTML)).toEqual([
|
expect(map(elm.children, c => c.innerHTML)).toEqual(["5", "4", "3", "2", "1", "0"]);
|
||||||
"5",
|
|
||||||
"4",
|
|
||||||
"3",
|
|
||||||
"2",
|
|
||||||
"1",
|
|
||||||
"0"
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -714,18 +668,10 @@ describe("snabbdom", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("removes elements", function() {
|
test("removes elements", function() {
|
||||||
const vnode1 = h("div", [
|
const vnode1 = h("div", [h("span", "One"), h("span", "Two"), h("span", "Three")]);
|
||||||
h("span", "One"),
|
|
||||||
h("span", "Two"),
|
|
||||||
h("span", "Three")
|
|
||||||
]);
|
|
||||||
const vnode2 = h("div", [h("span", "One"), h("span", "Three")]);
|
const vnode2 = h("div", [h("span", "One"), h("span", "Three")]);
|
||||||
elm = patch(vnode0, vnode1).elm;
|
elm = patch(vnode0, vnode1).elm;
|
||||||
expect(map(elm.children, c => c.innerHTML)).toEqual([
|
expect(map(elm.children, c => c.innerHTML)).toEqual(["One", "Two", "Three"]);
|
||||||
"One",
|
|
||||||
"Two",
|
|
||||||
"Three"
|
|
||||||
]);
|
|
||||||
elm = patch(vnode1, vnode2).elm;
|
elm = patch(vnode1, vnode2).elm;
|
||||||
expect(map(elm.children, c => c.innerHTML)).toEqual(["One", "Three"]);
|
expect(map(elm.children, c => c.innerHTML)).toEqual(["One", "Three"]);
|
||||||
});
|
});
|
||||||
@@ -760,49 +706,19 @@ describe("snabbdom", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("reorders elements", function() {
|
test("reorders elements", function() {
|
||||||
const vnode1 = h("div", [
|
const vnode1 = h("div", [h("span", "One"), h("div", "Two"), h("b", "Three")]);
|
||||||
h("span", "One"),
|
const vnode2 = h("div", [h("b", "Three"), h("span", "One"), h("div", "Two")]);
|
||||||
h("div", "Two"),
|
|
||||||
h("b", "Three")
|
|
||||||
]);
|
|
||||||
const vnode2 = h("div", [
|
|
||||||
h("b", "Three"),
|
|
||||||
h("span", "One"),
|
|
||||||
h("div", "Two")
|
|
||||||
]);
|
|
||||||
elm = patch(vnode0, vnode1).elm;
|
elm = patch(vnode0, vnode1).elm;
|
||||||
expect(map(elm.children, c => c.innerHTML)).toEqual([
|
expect(map(elm.children, c => c.innerHTML)).toEqual(["One", "Two", "Three"]);
|
||||||
"One",
|
|
||||||
"Two",
|
|
||||||
"Three"
|
|
||||||
]);
|
|
||||||
elm = patch(vnode1, vnode2).elm;
|
elm = patch(vnode1, vnode2).elm;
|
||||||
expect(map(elm.children, c => c.tagName)).toEqual(["B", "SPAN", "DIV"]);
|
expect(map(elm.children, c => c.tagName)).toEqual(["B", "SPAN", "DIV"]);
|
||||||
expect(map(elm.children, c => c.innerHTML)).toEqual([
|
expect(map(elm.children, c => c.innerHTML)).toEqual(["Three", "One", "Two"]);
|
||||||
"Three",
|
|
||||||
"One",
|
|
||||||
"Two"
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("supports null/undefined children", function() {
|
test("supports null/undefined children", function() {
|
||||||
const vnode1 = h("i", [null, h("i", "1"), h("i", "2"), null]);
|
const vnode1 = h("i", [null, h("i", "1"), h("i", "2"), null]);
|
||||||
const vnode2 = h("i", [
|
const vnode2 = h("i", [h("i", "2"), undefined, undefined, h("i", "1"), undefined]);
|
||||||
h("i", "2"),
|
const vnode3 = h("i", [null, h("i", "1"), undefined, null, h("i", "2"), undefined, null]);
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
h("i", "1"),
|
|
||||||
undefined
|
|
||||||
]);
|
|
||||||
const vnode3 = h("i", [
|
|
||||||
null,
|
|
||||||
h("i", "1"),
|
|
||||||
undefined,
|
|
||||||
null,
|
|
||||||
h("i", "2"),
|
|
||||||
undefined,
|
|
||||||
null
|
|
||||||
]);
|
|
||||||
elm = patch(vnode0, vnode1).elm;
|
elm = patch(vnode0, vnode1).elm;
|
||||||
expect(map(elm.children, c => c.innerHTML)).toEqual(["1", "2"]);
|
expect(map(elm.children, c => c.innerHTML)).toEqual(["1", "2"]);
|
||||||
elm = patch(vnode1, vnode2).elm;
|
elm = patch(vnode1, vnode2).elm;
|
||||||
@@ -835,10 +751,7 @@ describe("snabbdom", function() {
|
|||||||
}
|
}
|
||||||
const vnode1 = h("div", [
|
const vnode1 = h("div", [
|
||||||
h("span", "First sibling"),
|
h("span", "First sibling"),
|
||||||
h("div", { hook: { create: cb } }, [
|
h("div", { hook: { create: cb } }, [h("span", "Child 1"), h("span", "Child 2")]),
|
||||||
h("span", "Child 1"),
|
|
||||||
h("span", "Child 2")
|
|
||||||
]),
|
|
||||||
h("span", "Can't touch me")
|
h("span", "Can't touch me")
|
||||||
]);
|
]);
|
||||||
patch(vnode0, vnode1);
|
patch(vnode0, vnode1);
|
||||||
@@ -855,10 +768,7 @@ describe("snabbdom", function() {
|
|||||||
}
|
}
|
||||||
const vnode1 = h("div", [
|
const vnode1 = h("div", [
|
||||||
h("span", "First sibling"),
|
h("span", "First sibling"),
|
||||||
h("div", { hook: { insert: cb } }, [
|
h("div", { hook: { insert: cb } }, [h("span", "Child 1"), h("span", "Child 2")]),
|
||||||
h("span", "Child 1"),
|
|
||||||
h("span", "Child 2")
|
|
||||||
]),
|
|
||||||
h("span", "Can touch me")
|
h("span", "Can touch me")
|
||||||
]);
|
]);
|
||||||
patch(vnode0, vnode1);
|
patch(vnode0, vnode1);
|
||||||
@@ -874,17 +784,11 @@ describe("snabbdom", function() {
|
|||||||
}
|
}
|
||||||
const vnode1 = h("div", [
|
const vnode1 = h("div", [
|
||||||
h("span", "First sibling"),
|
h("span", "First sibling"),
|
||||||
h("div", { hook: { prepatch: cb } }, [
|
h("div", { hook: { prepatch: cb } }, [h("span", "Child 1"), h("span", "Child 2")])
|
||||||
h("span", "Child 1"),
|
|
||||||
h("span", "Child 2")
|
|
||||||
])
|
|
||||||
]);
|
]);
|
||||||
const vnode2 = h("div", [
|
const vnode2 = h("div", [
|
||||||
h("span", "First sibling"),
|
h("span", "First sibling"),
|
||||||
h("div", { hook: { prepatch: cb } }, [
|
h("div", { hook: { prepatch: cb } }, [h("span", "Child 1"), h("span", "Child 2")])
|
||||||
h("span", "Child 1"),
|
|
||||||
h("span", "Child 2")
|
|
||||||
])
|
|
||||||
]);
|
]);
|
||||||
patch(vnode0, vnode1);
|
patch(vnode0, vnode1);
|
||||||
patch(vnode1, vnode2);
|
patch(vnode1, vnode2);
|
||||||
@@ -965,10 +869,7 @@ describe("snabbdom", function() {
|
|||||||
}
|
}
|
||||||
const vnode1 = h("div", [
|
const vnode1 = h("div", [
|
||||||
h("span", "First sibling"),
|
h("span", "First sibling"),
|
||||||
h("div", { hook: { remove: cb } }, [
|
h("div", { hook: { remove: cb } }, [h("span", "Child 1"), h("span", "Child 2")])
|
||||||
h("span", "Child 1"),
|
|
||||||
h("span", "Child 2")
|
|
||||||
])
|
|
||||||
]);
|
]);
|
||||||
const vnode2 = h("div", [h("span", "First sibling")]);
|
const vnode2 = h("div", [h("span", "First sibling")]);
|
||||||
patch(vnode0, vnode1);
|
patch(vnode0, vnode1);
|
||||||
@@ -981,9 +882,7 @@ describe("snabbdom", function() {
|
|||||||
function cb(vnode) {
|
function cb(vnode) {
|
||||||
calls++;
|
calls++;
|
||||||
}
|
}
|
||||||
const vnode1 = h("div", [
|
const vnode1 = h("div", [h("div", { hook: { destroy: cb } }, [h("span", "Child 1")])]);
|
||||||
h("div", { hook: { destroy: cb } }, [h("span", "Child 1")])
|
|
||||||
]);
|
|
||||||
const vnode2 = h("div", "Text node");
|
const vnode2 = h("div", "Text node");
|
||||||
patch(vnode0, vnode1);
|
patch(vnode0, vnode1);
|
||||||
patch(vnode1, vnode2);
|
patch(vnode1, vnode2);
|
||||||
@@ -1053,10 +952,7 @@ describe("snabbdom", function() {
|
|||||||
result.push(vnode);
|
result.push(vnode);
|
||||||
rm();
|
rm();
|
||||||
}
|
}
|
||||||
const vnode1 = h("div", { hook: { remove: cb } }, [
|
const vnode1 = h("div", { hook: { remove: cb } }, [h("b", "Child 1"), h("i", "Child 2")]);
|
||||||
h("b", "Child 1"),
|
|
||||||
h("i", "Child 2")
|
|
||||||
]);
|
|
||||||
const vnode2 = h("span", [h("b", "Child 1"), h("i", "Child 2")]);
|
const vnode2 = h("span", [h("b", "Child 1"), h("i", "Child 2")]);
|
||||||
patch(vnode0, vnode1);
|
patch(vnode0, vnode1);
|
||||||
patch(vnode1, vnode2);
|
patch(vnode1, vnode2);
|
||||||
@@ -1091,10 +987,7 @@ describe("snabbdom", function() {
|
|||||||
}
|
}
|
||||||
const vnode1 = h("div", [
|
const vnode1 = h("div", [
|
||||||
h("span", "First sibling"),
|
h("span", "First sibling"),
|
||||||
h("div", [
|
h("div", [h("span", { hook: { destroy: cb } }, "Child 1"), h("span", "Child 2")])
|
||||||
h("span", { hook: { destroy: cb } }, "Child 1"),
|
|
||||||
h("span", "Child 2")
|
|
||||||
])
|
|
||||||
]);
|
]);
|
||||||
const vnode2 = h("div");
|
const vnode2 = h("div");
|
||||||
patch(vnode0, vnode1);
|
patch(vnode0, vnode1);
|
||||||
@@ -1150,11 +1043,7 @@ describe("snabbdom", function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
const vnode1 = h("div", [
|
const vnode1 = h("div", [h("span", "First child"), "", h("span", "Third child")]);
|
||||||
h("span", "First child"),
|
|
||||||
"",
|
|
||||||
h("span", "Third child")
|
|
||||||
]);
|
|
||||||
const vnode2 = h("div");
|
const vnode2 = h("div");
|
||||||
patch(vnode0, vnode1);
|
patch(vnode0, vnode1);
|
||||||
patch(vnode1, vnode2);
|
patch(vnode1, vnode2);
|
||||||
@@ -1196,10 +1085,7 @@ describe("snabbdom", function() {
|
|||||||
function cb(vnode) {
|
function cb(vnode) {
|
||||||
result.push(vnode);
|
result.push(vnode);
|
||||||
}
|
}
|
||||||
const vnode1 = h("div", [
|
const vnode1 = h("div", [h("span", { hook: { update: cb } }, "Hello"), h("span", "there")]);
|
||||||
h("span", { hook: { update: cb } }, "Hello"),
|
|
||||||
h("span", "there")
|
|
||||||
]);
|
|
||||||
patch(vnode0, vnode1);
|
patch(vnode0, vnode1);
|
||||||
patch(vnode1, vnode1);
|
patch(vnode1, vnode1);
|
||||||
expect(result).toHaveLength(0);
|
expect(result).toHaveLength(0);
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
import {
|
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
|
||||||
buildData,
|
|
||||||
startMeasure,
|
|
||||||
stopMeasure,
|
|
||||||
formatNumber
|
|
||||||
} from "../shared/utils.js";
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Likes Counter Widget
|
// Likes Counter Widget
|
||||||
@@ -40,11 +35,7 @@ class App extends owl.Component {
|
|||||||
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
|
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.log(
|
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
|
||||||
`Benchmarking Owl v${owl.__info__.version} (build date: ${
|
|
||||||
owl.__info__.date
|
|
||||||
})`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
benchmark(message, fn, callback) {
|
benchmark(message, fn, callback) {
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
import {
|
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
|
||||||
buildData,
|
|
||||||
startMeasure,
|
|
||||||
stopMeasure,
|
|
||||||
formatNumber
|
|
||||||
} from "../shared/utils.js";
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Likes Counter Widget
|
// Likes Counter Widget
|
||||||
@@ -40,11 +35,7 @@ class App extends owl.Component {
|
|||||||
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
|
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.log(
|
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
|
||||||
`Benchmarking Owl v${owl.__info__.version} (build date: ${
|
|
||||||
owl.__info__.date
|
|
||||||
})`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
benchmark(message, fn, callback) {
|
benchmark(message, fn, callback) {
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
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 {
|
||||||
|
widgets = { 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 {
|
||||||
|
widgets = { 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 < this.state.messages.length; i += 10) {
|
||||||
|
const msg = Object.assign({}, messages[i]);
|
||||||
|
msg.author += "!!!";
|
||||||
|
this.set(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 = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleMultiple() {
|
||||||
|
this.state.multipleFlag = !this.state.multipleFlag;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleClear() {
|
||||||
|
this.state.clearAfterFlag = !this.state.clearAfterFlag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// 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,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>OWL 0.15.0 Benchmark</title>
|
||||||
|
<link href="../shared/main.css" rel="stylesheet"/>
|
||||||
|
<script src='owl.js'></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id='main'></div>
|
||||||
|
<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-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
|
||||||
|
<label for="multipleflag">Do it 20x</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.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">
|
||||||
|
<t t-widget="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>
|
||||||
|
<t t-widget="Counter"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div t-name="Counter">
|
||||||
|
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
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 < this.state.messages.length; i += 10) {
|
||||||
|
const msg = Object.assign({}, messages[i]);
|
||||||
|
msg.author += "!!!";
|
||||||
|
this.set(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 = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleMultiple() {
|
||||||
|
this.state.multipleFlag = !this.state.multipleFlag;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleClear() {
|
||||||
|
this.state.clearAfterFlag = !this.state.clearAfterFlag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// 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,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>OWL 0.16.0 Benchmark</title>
|
||||||
|
<link href="../shared/main.css" rel="stylesheet"/>
|
||||||
|
<script src='owl.js'></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id='main'></div>
|
||||||
|
<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-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
|
||||||
|
<label for="multipleflag">Do it 20x</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
|
||||||
|
<label for="clearFlag">Clear after</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
|
||||||
|
<hr/>
|
||||||
|
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
|
||||||
|
<div class="log">
|
||||||
|
<div class="log-content" t-ref="log"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="right-thing">
|
||||||
|
<div class="content" t-on-remove-message="removeMessage">
|
||||||
|
<t t-foreach="state.messages" t-as="message">
|
||||||
|
<Message t-key="message.id" message="message"/>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div t-name="Message" class="message">
|
||||||
|
<span class="author"><t t-esc="props.message.author"/></span>
|
||||||
|
<span class="msg"><t t-esc="props.message.msg"/></span>
|
||||||
|
<button class="remove" t-on-click="removeMessage">Remove</button>
|
||||||
|
<Counter/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div t-name="Counter">
|
||||||
|
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
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 < this.state.messages.length; i += 10) {
|
||||||
|
const msg = Object.assign({}, messages[i]);
|
||||||
|
msg.author += "!!!";
|
||||||
|
this.set(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 = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleMultiple() {
|
||||||
|
this.state.multipleFlag = !this.state.multipleFlag;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleClear() {
|
||||||
|
this.state.clearAfterFlag = !this.state.clearAfterFlag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// 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,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>OWL 0.17.0 Benchmark</title>
|
||||||
|
<link href="../shared/main.css" rel="stylesheet"/>
|
||||||
|
<script src='owl.js'></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id='main'></div>
|
||||||
|
<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-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
|
||||||
|
<label for="multipleflag">Do it 20x</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.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,9 +1,4 @@
|
|||||||
import {
|
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
|
||||||
buildData,
|
|
||||||
startMeasure,
|
|
||||||
stopMeasure,
|
|
||||||
formatNumber
|
|
||||||
} from "../shared/utils.js";
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Likes Counter Widget
|
// Likes Counter Widget
|
||||||
@@ -20,7 +15,7 @@ class Counter extends owl.Component {
|
|||||||
// Message Widget
|
// Message Widget
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
class Message extends owl.Component {
|
class Message extends owl.Component {
|
||||||
widgets = { Counter };
|
components = { Counter };
|
||||||
|
|
||||||
shouldUpdate(nextProps) {
|
shouldUpdate(nextProps) {
|
||||||
return nextProps.message !== this.props.message;
|
return nextProps.message !== this.props.message;
|
||||||
@@ -36,15 +31,11 @@ class Message extends owl.Component {
|
|||||||
// Root Widget
|
// Root Widget
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
class App extends owl.Component {
|
class App extends owl.Component {
|
||||||
widgets = { Message };
|
components = { Message };
|
||||||
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
|
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.log(
|
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
|
||||||
`Benchmarking Owl v${owl.__info__.version} (build date: ${
|
|
||||||
owl.__info__.date
|
|
||||||
})`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
benchmark(message, fn, callback) {
|
benchmark(message, fn, callback) {
|
||||||
@@ -116,21 +107,18 @@ class App extends owl.Component {
|
|||||||
|
|
||||||
updateSomeMessages() {
|
updateSomeMessages() {
|
||||||
this.benchmark("update every 10th", () => {
|
this.benchmark("update every 10th", () => {
|
||||||
const setState = owl.Observer.set;
|
|
||||||
const messages = this.state.messages;
|
const messages = this.state.messages;
|
||||||
for (let i = 0; i < this.state.messages.length; i += 10) {
|
for (let i = 0; i < messages.length; i += 10) {
|
||||||
const msg = Object.assign({}, messages[i]);
|
const msg = Object.assign({}, messages[i]);
|
||||||
msg.author += "!!!";
|
msg.author += "!!!";
|
||||||
setState(messages, i, msg);
|
messages[i] = msg;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
removeMessage(event) {
|
removeMessage(event) {
|
||||||
this.benchmark("remove message", () => {
|
this.benchmark("remove message", () => {
|
||||||
const index = this.state.messages.findIndex(
|
const index = this.state.messages.findIndex(m => m.id === event.detail.id);
|
||||||
m => m.id === event.detail.id
|
|
||||||
);
|
|
||||||
this.state.messages.splice(index, 1);
|
this.state.messages.splice(index, 1);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
<div class="right-thing">
|
<div class="right-thing">
|
||||||
<div class="content" t-on-remove-message="removeMessage">
|
<div class="content" t-on-remove-message="removeMessage">
|
||||||
<t t-foreach="state.messages" t-as="message">
|
<t t-foreach="state.messages" t-as="message">
|
||||||
<t t-widget="Message" t-key="message.id" message="message"/>
|
<Message t-key="message.id" message="message"/>
|
||||||
</t>
|
</t>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
<span class="author"><t t-esc="props.message.author"/></span>
|
<span class="author"><t t-esc="props.message.author"/></span>
|
||||||
<span class="msg"><t t-esc="props.message.msg"/></span>
|
<span class="msg"><t t-esc="props.message.msg"/></span>
|
||||||
<button class="remove" t-on-click="removeMessage">Remove</button>
|
<button class="remove" t-on-click="removeMessage">Remove</button>
|
||||||
<t t-widget="Counter"/>
|
<Counter/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div t-name="Counter">
|
<div t-name="Counter">
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
|
font-family: Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||||
Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial,
|
|
||||||
sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|||||||
@@ -35,6 +35,9 @@
|
|||||||
<li><a href="benchmarks/owl-0.12.0">OWL 0.12.0</a></li>
|
<li><a href="benchmarks/owl-0.12.0">OWL 0.12.0</a></li>
|
||||||
<li><a href="benchmarks/owl-0.13.0">OWL 0.13.0</a></li>
|
<li><a href="benchmarks/owl-0.13.0">OWL 0.13.0</a></li>
|
||||||
<li><a href="benchmarks/owl-0.14.0">OWL 0.14.0</a></li>
|
<li><a href="benchmarks/owl-0.14.0">OWL 0.14.0</a></li>
|
||||||
|
<li><a href="benchmarks/owl-0.15.0">OWL 0.15.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-master">OWL Master</a></li>
|
<li><a href="benchmarks/owl-master">OWL Master</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul>
|
<ul>
|
||||||
|
|||||||
+1
-3
@@ -1,9 +1,7 @@
|
|||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
|
font-family: Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||||
Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial,
|
|
||||||
sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.title,
|
.title,
|
||||||
|
|||||||
+28
-15
@@ -97,10 +97,7 @@ function makeCodeIframe(js, css, xml, errorHandler) {
|
|||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
if (iframe.contentWindow) {
|
if (iframe.contentWindow) {
|
||||||
iframe.contentWindow.removeEventListener("error", errorHandler);
|
iframe.contentWindow.removeEventListener("error", errorHandler);
|
||||||
iframe.contentWindow.removeEventListener(
|
iframe.contentWindow.removeEventListener("unhandledrejection", errorHandler);
|
||||||
"unhandledrejection",
|
|
||||||
errorHandler
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}, 200);
|
}, 200);
|
||||||
doc.body.appendChild(script);
|
doc.body.appendChild(script);
|
||||||
@@ -161,7 +158,7 @@ class App extends owl.Component {
|
|||||||
super(...args);
|
super(...args);
|
||||||
this.version = owl.__info__.version;
|
this.version = owl.__info__.version;
|
||||||
this.SAMPLES = SAMPLES;
|
this.SAMPLES = SAMPLES;
|
||||||
this.widgets = { TabbedEditor };
|
this.components = { TabbedEditor };
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
js: SAMPLES[0].code,
|
js: SAMPLES[0].code,
|
||||||
@@ -200,6 +197,8 @@ class App extends owl.Component {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
//probably problem with the templates
|
//probably problem with the templates
|
||||||
error = e;
|
error = e;
|
||||||
|
// we still log the error, always useful to have it available
|
||||||
|
console.error(e);
|
||||||
}
|
}
|
||||||
if (error) {
|
if (error) {
|
||||||
this.displayError(error.message);
|
this.displayError(error.message);
|
||||||
@@ -284,15 +283,7 @@ class TabbedEditor extends owl.Component {
|
|||||||
this.setTab = owl.utils.debounce(this.setTab, 250, true);
|
this.setTab = owl.utils.debounce(this.setTab, 250, true);
|
||||||
|
|
||||||
this.sessions = {};
|
this.sessions = {};
|
||||||
for (let tab of ["js", "xml", "css"]) {
|
this._setupSessions(props);
|
||||||
if (props[tab]) {
|
|
||||||
this.sessions[tab] = new ace.EditSession(props[tab], MODES[tab]);
|
|
||||||
this.sessions[tab].setOption("useWorker", false);
|
|
||||||
const tabSize = tab === "xml" ? 2 : 4;
|
|
||||||
this.sessions[tab].setOption("tabSize", tabSize);
|
|
||||||
this.sessions[tab].setUndoManager(new ace.UndoManager());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.editor = null;
|
this.editor = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,9 +308,19 @@ class TabbedEditor extends owl.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
willUpdateProps(nextProps) {
|
||||||
|
this._setupSessions(nextProps);
|
||||||
|
}
|
||||||
|
|
||||||
patched() {
|
patched() {
|
||||||
const session = this.sessions[this.state.currentTab];
|
const session = this.sessions[this.state.currentTab];
|
||||||
session.setValue(this.props[this.state.currentTab], -1);
|
let content = this.props[this.state.currentTab];
|
||||||
|
if (content === false) {
|
||||||
|
const tab = this.props.js ? "js" : this.props.xml ? "xml" : "css";
|
||||||
|
content = this.props[tab];
|
||||||
|
this.state.currentTab = tab;
|
||||||
|
}
|
||||||
|
session.setValue(content, -1);
|
||||||
this.editor.setSession(session);
|
this.editor.setSession(session);
|
||||||
this.editor.resize();
|
this.editor.resize();
|
||||||
}
|
}
|
||||||
@@ -347,6 +348,18 @@ class TabbedEditor extends owl.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_setupSessions(props) {
|
||||||
|
for (let tab of ["js", "xml", "css"]) {
|
||||||
|
if (props[tab] && !this.sessions[tab]) {
|
||||||
|
this.sessions[tab] = new ace.EditSession(props[tab], MODES[tab]);
|
||||||
|
this.sessions[tab].setOption("useWorker", false);
|
||||||
|
const tabSize = tab === "xml" ? 2 : 4;
|
||||||
|
this.sessions[tab].setOption("tabSize", tabSize);
|
||||||
|
this.sessions[tab].setUndoManager(new ace.UndoManager());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -2,9 +2,7 @@
|
|||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
|
font-family: Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||||
Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial,
|
|
||||||
sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -181,10 +179,14 @@ body {
|
|||||||
|
|
||||||
.right-pane .error {
|
.right-pane .error {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding-top: 40%;
|
width: 90%;
|
||||||
font-size: 30px;
|
padding-top: 30%;
|
||||||
|
font-size: 18px;
|
||||||
color: darkred;
|
color: darkred;
|
||||||
text-align: center;
|
margin-left: 5%;
|
||||||
padding-left: 30px;
|
}
|
||||||
padding-right: 30px;
|
|
||||||
|
.right-pane .error pre {
|
||||||
|
overflow: auto;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
+99
-93
@@ -1,16 +1,17 @@
|
|||||||
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.
|
||||||
|
|
||||||
class Counter extends owl.Component {
|
class Greeter extends owl.Component {
|
||||||
state = { value: 0 };
|
state = { word: 'Hello' };
|
||||||
|
|
||||||
increment() {
|
toggle() {
|
||||||
this.state.value++;
|
this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main root widget
|
// Main root component
|
||||||
class App extends owl.Component {
|
class App extends owl.Component {
|
||||||
widgets = { Counter };
|
components = { Greeter };
|
||||||
|
state = { name: 'World'};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Application setup
|
// Application setup
|
||||||
@@ -21,20 +22,25 @@ app.mount(document.body);
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const COMPONENTS_XML = `<templates>
|
const COMPONENTS_XML = `<templates>
|
||||||
<button t-name="Counter" t-on-click="increment">
|
<div t-name="Greeter" class="greeter" t-on-click="toggle">
|
||||||
Click Me! [<t t-esc="state.value"/>]
|
<t t-esc="state.word"/>, <t t-esc="props.name"/>
|
||||||
</button>
|
</div>
|
||||||
|
|
||||||
<div t-name="App">
|
<div t-name="App">
|
||||||
<t t-widget="Counter"/>
|
<Greeter name="state.name"/>
|
||||||
<t t-widget="Counter"/>
|
|
||||||
</div>
|
</div>
|
||||||
</templates>`;
|
</templates>
|
||||||
|
`;
|
||||||
|
|
||||||
const COMPONENTS_CSS = `button {
|
const COMPONENTS_CSS = `.greeter {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
width: 220px;
|
width: 300px;
|
||||||
|
height: 100px;
|
||||||
margin: 5px;
|
margin: 5px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 100px;
|
||||||
|
background-color: #eeeeee;
|
||||||
|
user-select: none;
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
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
|
||||||
@@ -49,8 +55,8 @@ class Counter extends owl.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class App extends owl.Component {
|
class App extends owl.Component {
|
||||||
state = { flag: false, widgetFlag: false, numbers: [] };
|
state = { flag: false, componentFlag: false, numbers: [] };
|
||||||
widgets = { Counter };
|
components = { Counter };
|
||||||
|
|
||||||
toggle(key) {
|
toggle(key) {
|
||||||
this.state[key] = !this.state[key];
|
this.state[key] = !this.state[key];
|
||||||
@@ -83,12 +89,12 @@ const ANIMATION_XML = `<templates>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>Transition on sub widgets</h2>
|
<h2>Transition on sub components</h2>
|
||||||
|
|
||||||
<div class="demo">
|
<div class="demo">
|
||||||
<button t-on-click="toggle('widgetFlag')">Toggle widget</button>
|
<button t-on-click="toggle('componentFlag')">Toggle component</button>
|
||||||
<div>
|
<div>
|
||||||
<t t-widget="Counter" t-if="state.widgetFlag" t-transition="fade"/>
|
<Counter t-if="state.componentFlag" t-transition="fade"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -98,7 +104,7 @@ const ANIMATION_XML = `<templates>
|
|||||||
<button t-on-click="addNumber">Add a number</button>
|
<button t-on-click="addNumber">Add a number</button>
|
||||||
<div>
|
<div>
|
||||||
<t t-foreach="state.numbers" t-as="n">
|
<t t-foreach="state.numbers" t-as="n">
|
||||||
<span t-transition="fade" class="numberspan"><t t-esc="n"/></span>
|
<span t-transition="fade" class="numberspan" t-key="n"><t t-esc="n"/></span>
|
||||||
</t>
|
</t>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -173,12 +179,12 @@ const ANIMATION_CSS = `button {
|
|||||||
|
|
||||||
const LIFECYCLE_DEMO = `// This example shows all the possible lifecycle hooks
|
const LIFECYCLE_DEMO = `// This example shows all the possible lifecycle hooks
|
||||||
//
|
//
|
||||||
// The root widget controls a sub widget (DemoWidget). It logs all its lifecycle
|
// The root component controls a sub component (DemoComponent). It logs all its lifecycle
|
||||||
// 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.
|
||||||
|
|
||||||
class DemoWidget extends owl.Component {
|
class DemoComponent extends owl.Component {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(...arguments);
|
super(...arguments);
|
||||||
this.state = { n: 0 };
|
this.state = { n: 0 };
|
||||||
@@ -208,14 +214,14 @@ class DemoWidget extends owl.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class App extends owl.Component {
|
class App extends owl.Component {
|
||||||
widgets = { DemoWidget };
|
components = { DemoComponent };
|
||||||
state = { n: 0, flag: true };
|
state = { n: 0, flag: true };
|
||||||
|
|
||||||
increment() {
|
increment() {
|
||||||
this.state.n++;
|
this.state.n++;
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleSubWidget() {
|
toggleSubComponent() {
|
||||||
this.state.flag = !this.state.flag;
|
this.state.flag = !this.state.flag;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,17 +232,17 @@ app.mount(document.body);
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const LIFECYCLE_DEMO_XML = `<templates>
|
const LIFECYCLE_DEMO_XML = `<templates>
|
||||||
<div t-name="DemoWidget" t-on-click="increment" class="demo">
|
<div t-name="DemoComponent" t-on-click="increment" class="demo">
|
||||||
<div>Demo Sub Widget</div>
|
<div>Demo Sub Component</div>
|
||||||
<div>(click on me to update me)</div>
|
<div>(click on me to update me)</div>
|
||||||
<div>Props: <t t-esc="props.n"/>, State: <t t-esc="state.n"/>. </div>
|
<div>Props: <t t-esc="props.n"/>, State: <t t-esc="state.n"/>. </div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div t-name="App">
|
<div t-name="App">
|
||||||
<button t-on-click="increment">Increment Parent State</button>
|
<button t-on-click="increment">Increment Parent State</button>
|
||||||
<button t-on-click="toggleSubWidget">Toggle SubWidget</button>
|
<button t-on-click="toggleSubComponent">Toggle SubComponent</button>
|
||||||
<div t-if="state.flag">
|
<div t-if="state.flag">
|
||||||
<t t-widget="DemoWidget" n="state.n"/>
|
<DemoComponent n="state.n"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</templates>`;
|
</templates>`;
|
||||||
@@ -332,7 +338,7 @@ function makeStore() {
|
|||||||
todos,
|
todos,
|
||||||
nextId
|
nextId
|
||||||
};
|
};
|
||||||
const store = new owl.Store({
|
const store = new owl.store.Store({
|
||||||
state,
|
state,
|
||||||
actions,
|
actions,
|
||||||
mutations
|
mutations
|
||||||
@@ -399,16 +405,15 @@ class TodoItem extends owl.Component {
|
|||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// TodoApp
|
// TodoApp
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
function mapStoreToProps(state) {
|
class TodoApp extends owl.store.ConnectedComponent {
|
||||||
|
components = { TodoItem };
|
||||||
|
state = { filter: "all" };
|
||||||
|
|
||||||
|
static mapStoreToProps(state) {
|
||||||
return {
|
return {
|
||||||
todos: state.todos
|
todos: state.todos
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
class TodoApp extends owl.Component {
|
|
||||||
widgets = { TodoItem };
|
|
||||||
state = { filter: "all" };
|
|
||||||
|
|
||||||
get visibleTodos() {
|
get visibleTodos() {
|
||||||
let todos = this.props.todos;
|
let todos = this.props.todos;
|
||||||
if (this.state.filter === "active") {
|
if (this.state.filter === "active") {
|
||||||
@@ -456,8 +461,6 @@ class TodoApp extends owl.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConnectedTodoApp = owl.connect(TodoApp, mapStoreToProps);
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// App Initialization
|
// App Initialization
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
@@ -468,7 +471,7 @@ const env = {
|
|||||||
store,
|
store,
|
||||||
dispatch: store.dispatch.bind(store),
|
dispatch: store.dispatch.bind(store),
|
||||||
};
|
};
|
||||||
const app = new ConnectedTodoApp(env);
|
const app = new TodoApp(env);
|
||||||
app.mount(document.body);
|
app.mount(document.body);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -483,7 +486,7 @@ const TODO_APP_STORE_XML = `<templates>
|
|||||||
<label for="toggle-all"></label>
|
<label for="toggle-all"></label>
|
||||||
<ul class="todo-list">
|
<ul class="todo-list">
|
||||||
<t t-foreach="visibleTodos" t-as="todo">
|
<t t-foreach="visibleTodos" t-as="todo">
|
||||||
<t t-widget="TodoItem" t-key="todo.id" id="todo.id" completed="todo.completed" title="todo.title"/>
|
<TodoItem t-key="todo.id" id="todo.id" completed="todo.completed" title="todo.title"/>
|
||||||
</t>
|
</t>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
@@ -910,7 +913,7 @@ const RESPONSIVE = `// In this example, we show how we can modify keys in the gl
|
|||||||
//
|
//
|
||||||
// The main idea is to have a "isMobile" key in the environment, then listen
|
// The main idea is to have a "isMobile" key in the environment, then listen
|
||||||
// to resize events and update the env if needed. Then, the whole interface
|
// to resize events and update the env if needed. Then, the whole interface
|
||||||
// will be updated, creating and destroying widgets as needed.
|
// will be updated, creating and destroying components as needed.
|
||||||
//
|
//
|
||||||
// To see this in action, try resizing the window. The application will switch
|
// To see this in action, try resizing the window. The application will switch
|
||||||
// to mobile mode whenever it has less than 768px.
|
// to mobile mode whenever it has less than 768px.
|
||||||
@@ -921,50 +924,51 @@ const RESPONSIVE = `// In this example, we show how we can modify keys in the gl
|
|||||||
class Navbar extends owl.Component {}
|
class Navbar extends owl.Component {}
|
||||||
|
|
||||||
class ControlPanel extends owl.Component {
|
class ControlPanel extends owl.Component {
|
||||||
widgets = { MobileSearchView };
|
components = { MobileSearchView };
|
||||||
}
|
}
|
||||||
|
|
||||||
class FormView extends owl.Component {
|
class FormView extends owl.Component {
|
||||||
widgets = { AdvancedWidget };
|
components = { AdvancedComponent };
|
||||||
}
|
}
|
||||||
|
|
||||||
class AdvancedWidget extends owl.Component {}
|
class AdvancedComponent extends owl.Component {}
|
||||||
|
|
||||||
class Chatter extends owl.Component {}
|
class Chatter extends owl.Component {
|
||||||
|
messages = Array.from(Array(100).keys());
|
||||||
|
}
|
||||||
|
|
||||||
class MobileSearchView extends owl.Component {}
|
class MobileSearchView extends owl.Component {}
|
||||||
|
|
||||||
|
|
||||||
class App extends owl.Component {
|
class App extends owl.Component {
|
||||||
widgets = { Navbar, ControlPanel, FormView, Chatter };
|
components = { Navbar, ControlPanel, FormView, Chatter };
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Responsive plugin
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
function setupResponsivePlugin(env) {
|
||||||
|
const isMobile = () => window.innerWidth <= 768;
|
||||||
|
env.isMobile = isMobile();
|
||||||
|
const updateEnv = owl.utils.debounce(() => {
|
||||||
|
if (env.isMobile !== isMobile()) {
|
||||||
|
env.isMobile = !env.isMobile;
|
||||||
|
env.qweb.forceUpdate();
|
||||||
|
}
|
||||||
|
}, 15);
|
||||||
|
window.addEventListener("resize", updateEnv);
|
||||||
}
|
}
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Application Startup
|
// Application Startup
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
function isMobile() {
|
|
||||||
return window.innerWidth <= 768;
|
|
||||||
}
|
|
||||||
|
|
||||||
const env = {
|
const env = {
|
||||||
qweb: new owl.QWeb(TEMPLATES),
|
qweb: new owl.QWeb(TEMPLATES),
|
||||||
isMobile: isMobile()
|
|
||||||
};
|
};
|
||||||
|
setupResponsivePlugin(env);
|
||||||
|
|
||||||
const app = new App(env);
|
const app = new App(env);
|
||||||
app.mount(document.body);
|
app.mount(document.body);
|
||||||
|
|
||||||
function updateEnv() {
|
|
||||||
const _isMobile = isMobile();
|
|
||||||
if (_isMobile !== env.isMobile) {
|
|
||||||
app.updateEnv({
|
|
||||||
isMobile: _isMobile
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("resize", owl.utils.debounce(updateEnv, 20));
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const RESPONSIVE_XML = `<templates>
|
const RESPONSIVE_XML = `<templates>
|
||||||
@@ -972,38 +976,40 @@ const RESPONSIVE_XML = `<templates>
|
|||||||
|
|
||||||
<div t-name="ControlPanel" class="controlpanel">
|
<div t-name="ControlPanel" class="controlpanel">
|
||||||
<h2>Control Panel</h2>
|
<h2>Control Panel</h2>
|
||||||
<t t-if="env.isMobile" t-widget="MobileSearchView"/>
|
<MobileSearchView t-if="env.isMobile" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div t-name="FormView" class="formview">
|
<div t-name="FormView" class="formview">
|
||||||
<h2>Form View</h2>
|
<h2>Form View</h2>
|
||||||
<t t-if="!env.isMobile" t-widget="AdvancedWidget"/>
|
<AdvancedComponent t-if="!env.isMobile" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div t-name="Chatter" class="chatter">
|
<div t-name="Chatter" class="chatter">
|
||||||
<h2>Chatter</h2>
|
<h2>Chatter</h2>
|
||||||
<t t-foreach="100" t-as="item"><div>Message <t t-esc="item"/></div></t>
|
<t t-foreach="messages" t-as="item"><div>Message <t t-esc="item"/></div></t>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div t-name="MobileSearchView">Mobile searchview</div>
|
<div t-name="MobileSearchView">Mobile searchview</div>
|
||||||
|
|
||||||
<div t-name="AdvancedWidget">
|
<div t-name="AdvancedComponent">
|
||||||
This widget is only created in desktop mode.
|
This component is only created in desktop mode.
|
||||||
<button>Button!</button>
|
<button>Button!</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div t-name="App" class="app" t-att-class="{mobile: env.isMobile, desktop: !env.isMobile}">
|
<div t-name="App" class="app" t-att-class="{mobile: env.isMobile, desktop: !env.isMobile}">
|
||||||
<t t-widget="Navbar"/>
|
<t t-set="maincontent">
|
||||||
<t t-widget="ControlPanel"/>
|
<FormView />
|
||||||
|
<Chatter />
|
||||||
|
</t>
|
||||||
|
<Navbar/>
|
||||||
|
<ControlPanel/>
|
||||||
<div class="content-wrapper" t-if="!env.isMobile">
|
<div class="content-wrapper" t-if="!env.isMobile">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<t t-widget="FormView"/>
|
<t t-raw="maincontent"/>
|
||||||
<t t-widget="Chatter"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<t t-else="1">
|
<t t-else="1">
|
||||||
<t t-widget="FormView"/>
|
<t t-raw="maincontent"/>
|
||||||
<t t-widget="Chatter"/>
|
|
||||||
</t>
|
</t>
|
||||||
</div>
|
</div>
|
||||||
</templates>
|
</templates>
|
||||||
@@ -1070,7 +1076,7 @@ const RESPONSIVE_CSS = `body {
|
|||||||
const SLOTS = `// We show here how slots can be used to create generic components.
|
const SLOTS = `// We show here how slots can be used to create generic components.
|
||||||
// In this example, the Card component is basically only a container. It is not
|
// In this example, the Card component is basically only a container. It is not
|
||||||
// aware of its content. It just knows where it should be (with t-slot).
|
// aware of its content. It just knows where it should be (with t-slot).
|
||||||
// The parent widget define the content with t-set.
|
// The parent component define the content with t-set.
|
||||||
//
|
//
|
||||||
// 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
|
||||||
@@ -1091,9 +1097,9 @@ class Counter extends owl.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main root widget
|
// Main root component
|
||||||
class App extends owl.Component {
|
class App extends owl.Component {
|
||||||
widgets = {Card, Counter};
|
components = {Card, Counter};
|
||||||
state = {a: 1, b: 3};
|
state = {a: 1, b: 3};
|
||||||
|
|
||||||
inc(key, delta) {
|
inc(key, delta) {
|
||||||
@@ -1126,17 +1132,17 @@ const SLOTS_XML = `<templates>
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div t-name="App" class="main">
|
<div t-name="App" class="main">
|
||||||
<t t-widget="Card" title="'Title card A'">
|
<Card title="'Title card A'">
|
||||||
<t t-set="content">Content of card 1... [<t t-esc="state.a"/>]</t>
|
<t t-set="content">Content of card 1... [<t t-esc="state.a"/>]</t>
|
||||||
<t t-set="footer"><button t-on-click="inc('a', 1)">Increment A</button></t>
|
<t t-set="footer"><button t-on-click="inc('a', 1)">Increment A</button></t>
|
||||||
</t>
|
</Card>
|
||||||
<t t-widget="Card" title="'Title card B'">
|
<Card title="'Title card B'">
|
||||||
<div t-set="content">
|
<t t-set="content">
|
||||||
<div>Card 2... [<t t-esc="state.b"/>]</div>
|
<div>Card 2... [<t t-esc="state.b"/>]</div>
|
||||||
<t t-widget="Counter"/>
|
<Counter />
|
||||||
</div>
|
|
||||||
<t t-set="footer"><button t-on-click="inc('b', -1)">Decrement B</button></t>
|
|
||||||
</t>
|
</t>
|
||||||
|
<t t-set="footer"><button t-on-click="inc('b', -1)">Decrement B</button></t>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</templates>`;
|
</templates>`;
|
||||||
|
|
||||||
@@ -1186,13 +1192,13 @@ const SLOTS_CSS = `.main {
|
|||||||
|
|
||||||
const ASYNC_COMPONENTS = `// This example will not work if your browser does not support ESNext class fields
|
const ASYNC_COMPONENTS = `// This example will not work if your browser does not support ESNext class fields
|
||||||
|
|
||||||
// In this example, we have 2 sub widgets, one of them being async (slow).
|
// In this example, we have 2 sub components, one of them being async (slow).
|
||||||
// However, we don't want renderings of the other sub widget to be delayed
|
// However, we don't want renderings of the other sub component to be delayed
|
||||||
// because of the slow widget. 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.
|
||||||
|
|
||||||
class App extends owl.Component {
|
class App extends owl.Component {
|
||||||
widgets = {SlowWidget, NotificationList};
|
components = {SlowComponent, NotificationList};
|
||||||
state = { value: 0, notifs: [] };
|
state = { value: 0, notifs: [] };
|
||||||
|
|
||||||
increment() {
|
increment() {
|
||||||
@@ -1206,9 +1212,9 @@ class App extends owl.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SlowWidget extends owl.Component {
|
class SlowComponent extends owl.Component {
|
||||||
willUpdateProps() {
|
willUpdateProps() {
|
||||||
// simulate a widget 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
|
||||||
return new Promise(resolve => setTimeout(resolve, 1500));
|
return new Promise(resolve => setTimeout(resolve, 1500));
|
||||||
}
|
}
|
||||||
@@ -1224,11 +1230,11 @@ app.mount(document.body);
|
|||||||
const ASYNC_COMPONENTS_XML = `<templates>
|
const ASYNC_COMPONENTS_XML = `<templates>
|
||||||
<div t-name="App" class="app">
|
<div t-name="App" class="app">
|
||||||
<button t-on-click="increment">Increment</button>
|
<button t-on-click="increment">Increment</button>
|
||||||
<t t-widget="SlowWidget" value="state.value"/>
|
<SlowComponent value="state.value"/>
|
||||||
<t t-widget="NotificationList" t-asyncroot="1" notifications="state.notifs"/>
|
<NotificationList t-asyncroot="1" notifications="state.notifs"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div t-name="SlowWidget" class="value" >
|
<div t-name="SlowComponent" class="value" >
|
||||||
Current value: <t t-esc="props.value"/>
|
Current value: <t t-esc="props.value"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1350,7 +1356,7 @@ export const SAMPLES = [
|
|||||||
description: "Lifecycle demo",
|
description: "Lifecycle demo",
|
||||||
code: LIFECYCLE_DEMO,
|
code: LIFECYCLE_DEMO,
|
||||||
xml: LIFECYCLE_DEMO_XML,
|
xml: LIFECYCLE_DEMO_XML,
|
||||||
css: LIFECYCLE_CSS,
|
css: LIFECYCLE_CSS
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "Todo List App (with store)",
|
description: "Todo List App (with store)",
|
||||||
|
|||||||
@@ -24,14 +24,14 @@
|
|||||||
<a class="btn flash" t-on-click="downloadCode" title="Download a Zip with this Code"><i class="fas fa-download"></i></a>
|
<a class="btn flash" t-on-click="downloadCode" title="Download a Zip with this Code"><i class="fas fa-download"></i></a>
|
||||||
<a class="layout-selector flash" t-on-click="toggleLayout" title="Toggle Layout"><i class="fas" t-att-class="state.splitLayout ? 'fa-toggle-on' : 'fa-toggle-off'"></i></a>
|
<a class="layout-selector flash" t-on-click="toggleLayout" title="Toggle Layout"><i class="fas" t-att-class="state.splitLayout ? 'fa-toggle-on' : 'fa-toggle-off'"></i></a>
|
||||||
</div>
|
</div>
|
||||||
<t t-widget="TabbedEditor"
|
<TabbedEditor
|
||||||
js="state.js"
|
js="state.js"
|
||||||
css="!state.splitLayout and state.css"
|
css="!state.splitLayout and state.css"
|
||||||
xml="!state.splitLayout and state.js"
|
xml="!state.splitLayout and state.xml"
|
||||||
t-att-style="topEditorStyle"/>
|
t-att-style="topEditorStyle"/>
|
||||||
<t t-if="state.splitLayout">
|
<t t-if="state.splitLayout">
|
||||||
<div class="separator horizontal"/>
|
<div class="separator horizontal"/>
|
||||||
<t t-widget="TabbedEditor" t-keepalive="1"
|
<TabbedEditor t-keepalive="1"
|
||||||
js="false"
|
js="false"
|
||||||
css="state.css"
|
css="state.css"
|
||||||
xml="state.xml"
|
xml="state.xml"
|
||||||
@@ -48,7 +48,8 @@
|
|||||||
<div class="note">Note: these examples require a recent browser to work without a transpilation step. </div>
|
<div class="note">Note: these examples require a recent browser to work without a transpilation step. </div>
|
||||||
</div>
|
</div>
|
||||||
<div t-if="state.error" class="error">
|
<div t-if="state.error" class="error">
|
||||||
<t t-esc="state.error"/>
|
<h3>Error</h3>
|
||||||
|
<pre t-esc="state.error"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="content" t-ref="content"/>
|
<div class="content" t-ref="content"/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+2
-2
@@ -5,7 +5,7 @@
|
|||||||
"noImplicitThis": true,
|
"noImplicitThis": true,
|
||||||
"removeComments": false,
|
"removeComments": false,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"target": "es5",
|
"target": "esnext",
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"alwaysStrict": true,
|
"alwaysStrict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
@@ -15,5 +15,5 @@
|
|||||||
"strictPropertyInitialization": true,
|
"strictPropertyInitialization": true,
|
||||||
"strictNullChecks": true
|
"strictNullChecks": true
|
||||||
},
|
},
|
||||||
"include": ["src/*.ts"]
|
"include": ["src/**/*.ts","src/*.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user