mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2701a7d861 | |||
| 3ebe985c3c | |||
| 1c0d4b5832 | |||
| 2840794fa2 | |||
| 710f42d4e4 | |||
| 45a2b0122d | |||
| 4c41f62364 | |||
| c2ab9774fb | |||
| 22e48e3be0 | |||
| 1ff3c32fe4 | |||
| 9a3e1ed99e | |||
| 438e2fbc2e | |||
| b3db085745 | |||
| b948c59c6d | |||
| 6426fe98f9 | |||
| 4dd9de349e | |||
| e1bcea77b5 | |||
| 2a19aeb6a3 | |||
| b53a34b7d1 | |||
| f881640d3d | |||
| 54c3dd76e4 | |||
| 7b887447da | |||
| 438b21df3f | |||
| d1094f647a | |||
| 49baf0de6e | |||
| b14d069ee7 | |||
| e14a4fe338 | |||
| 1f9ec46236 | |||
| 1464a3631b | |||
| 7d3c374b78 | |||
| 1dd257f33c | |||
| 1206afe6be | |||
| 5842ed51b0 | |||
| d22219f084 | |||
| 0be1a54e03 | |||
| 32921ecc20 | |||
| 1c290c1172 | |||
| 2f9d7ea58f | |||
| fe23e76341 | |||
| 73023d4869 | |||
| ff51443b8e | |||
| f8e07f6d30 | |||
| 00da778753 | |||
| 187eb922c0 | |||
| 466c12a0e6 | |||
| 8dc3ec94bf | |||
| a554436d03 | |||
| 34b5049dee | |||
| 0eccfd08fe | |||
| 98b063fd70 | |||
| e9ba94ae55 | |||
| 4d72a4f240 | |||
| a08deb895d | |||
| 60cea14b01 | |||
| f60904b740 | |||
| 376f801541 | |||
| 26f14180e2 | |||
| bced9f3d04 | |||
| 94208d9cbb | |||
| 681983142d | |||
| a525b42537 | |||
| a4cc085b6d | |||
| 837dacf1cb | |||
| 7cdd1aa9a1 | |||
| 24c569e6d5 | |||
| 050694414c | |||
| 639908366a | |||
| 59f401303a | |||
| 649e20b956 | |||
| c2280ff1bb | |||
| 8a32d823cd | |||
| 13b31458da | |||
| 92b3ab32e2 | |||
| 4ca0a157e3 | |||
| 1dec8df715 | |||
| 8f29cfc996 | |||
| da275e74fd | |||
| 3d9f4e028c | |||
| 13577997bc |
+6
-1
@@ -1,5 +1,6 @@
|
||||
/node_modules
|
||||
/dist
|
||||
|
||||
npm-debug.log
|
||||
|
||||
# misc
|
||||
@@ -14,8 +15,12 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
package-lock.json
|
||||
|
||||
#ide's
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
node_modules
|
||||
|
||||
# Extras temp file
|
||||
/extras/owl.js
|
||||
/tools/owl.js
|
||||
@@ -59,8 +59,8 @@ More interesting examples can be found on the [playground](https://odoo.github.i
|
||||
|
||||
If you want to use a simple `<script>` tag, the last release can be downloaded here:
|
||||
|
||||
- [owl-0.11.0.js](https://github.com/odoo/owl/releases/download/v0.11.0/owl.js)
|
||||
- [owl-0.11.0.min.js](https://github.com/odoo/owl/releases/download/v0.11.0/owl.min.js)
|
||||
- [owl-0.14.0.js](https://github.com/odoo/owl/releases/download/v0.14.0/owl.js)
|
||||
- [owl-0.14.0.min.js](https://github.com/odoo/owl/releases/download/v0.14.0/owl.min.js)
|
||||
|
||||
Some npm scripts are available:
|
||||
|
||||
@@ -71,8 +71,8 @@ Some npm scripts are available:
|
||||
| `npm run minify` | minify the prebuilt owl.js file |
|
||||
| `npm run test` | run all tests |
|
||||
| `npm run test:watch` | run all tests, and keep a watcher |
|
||||
| `npm run extras` | build extras applications, start a static server (see [here](extras/readme.md)) |
|
||||
| `npm run extras:watch` | same as `extras`, but with a watcher to rebuild owl |
|
||||
| `npm run tools` | build tools applications, start a static server (see [here](doc/tooling.md)) |
|
||||
| `npm run tools:watch` | same as `tools`, but with a watcher to rebuild owl |
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -82,6 +82,9 @@ The complete documentation can be found [here](doc/readme.md). The most importan
|
||||
- [Component](doc/component.md)
|
||||
- [QWeb](doc/qweb.md)
|
||||
|
||||
Found an issue in the documentation? A broken link? Some outdated information?
|
||||
Submit a PR!
|
||||
|
||||
## License
|
||||
|
||||
OWL is [GPL licensed](./LICENSE).
|
||||
|
||||
+65
-8
@@ -1,23 +1,25 @@
|
||||
# 🦉 Animations 🦉
|
||||
|
||||
|
||||
Animation is a complex topic. There are many different use cases, and many
|
||||
solutions and technologies.
|
||||
solutions and technologies. Owl only supports some basic use cases.
|
||||
|
||||
## Simple CSS effects
|
||||
|
||||
Sometimes, using pure CSS is enough. For these use cases, Owl is not really
|
||||
Sometimes, using pure CSS is enough. For these use cases, Owl is not really
|
||||
necessary: it just needs to render a DOM element with a specific class. For
|
||||
example:
|
||||
|
||||
|
||||
```xml
|
||||
<button class="flash" t-on-click="doSomething">Click</button>
|
||||
<a class="btn flash" t-on-click="doSomething">Click</a>
|
||||
```
|
||||
|
||||
with the following CSS:
|
||||
|
||||
```css
|
||||
btn {
|
||||
background-color: gray;
|
||||
}
|
||||
|
||||
.flash {
|
||||
transition: background 0.5s;
|
||||
}
|
||||
@@ -31,9 +33,64 @@ with the following CSS:
|
||||
will produce a nice flash effect whenever the user click (or activate with the
|
||||
keyboard) the button.
|
||||
|
||||
## CSS Transitions (single element)
|
||||
## CSS Transitions
|
||||
|
||||
A more complex situation occurs when we want to transition an element in or out
|
||||
of the page. For example, we may want a fade-in and fade-out effect.
|
||||
of the page. For example, we may want a fade-in and fade-out effect.
|
||||
|
||||
The `t-transition` directive is here to help us (see [QWeb documentation](doc/qweb.md#t-transition-directive)).
|
||||
The `t-transition` directive is here to help us. It works on html elements and
|
||||
on components, by adding and removing some css classes.
|
||||
|
||||
To perform useful transition effects, whenever an element appears or disappears,
|
||||
it is necessary to add/remove some css style or class at some precise moment in
|
||||
the lifetime of a node. Since this is not easy to do by hand, Owl `t-transition`
|
||||
directive is there to help.
|
||||
|
||||
Whenever a node has a `t-transition` directive, with a `name` value, the following
|
||||
will happen:
|
||||
|
||||
At node insertion:
|
||||
|
||||
- the css classes `name-enter` and `name-enter-active` will be added directly
|
||||
when the node is inserted into the DOM,
|
||||
- on the next animation frame: the css class `name-enter` will be removed and the
|
||||
class `name-enter-to` will be added (so they can be used to trigger css
|
||||
transition effects),
|
||||
- the css class `name-enter-active` will be removed whenever a css transition
|
||||
ends.
|
||||
|
||||
At node destruction:
|
||||
|
||||
- the css classes `name-leave` and `name-leave-active` will be added before the
|
||||
node is removed to the DOM,
|
||||
- the css class `name-leave` will be removed on the next animation frame (so it
|
||||
can be used to trigger css transition effects),
|
||||
- the css class `name-leave-active` will be removed whenever a css transition
|
||||
ends. Only then will the element be removed from the DOM.
|
||||
|
||||
For example, a simple fade in/out effect can be done with this:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<div t-if="state.flag" class="square" t-transition="fade">Hello</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
```css
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
```
|
||||
|
||||
The `t-transition` directive can be combined with `t-widget`.
|
||||
|
||||
Notes:
|
||||
|
||||
- more information on animations are available [here](animations.md).
|
||||
- Owl does not support more than one transition on a single node, so the
|
||||
`t-transition` expression must be a single value (i.e. no space allowed)
|
||||
|
||||
+439
-62
@@ -4,13 +4,19 @@
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Example](#example)
|
||||
- [Composition](#composition)
|
||||
- [Reference](#reference)
|
||||
- [Properties](#properties)
|
||||
- [Static Properties](#static-properties)
|
||||
- [Methods](#methods)
|
||||
- [Lifecycle](#lifecycle)
|
||||
- [Semantics](#semantics)
|
||||
- [Asynchronous rendering](#asynchronous-rendering)
|
||||
- [Composition](#composition)
|
||||
- [Event Handling](#event-handling)
|
||||
- [`t-key` directive](#t-key-directive)
|
||||
- [`t-mounted` directive](#t-mounted-directive)
|
||||
- [Props Validation](#props-validation)
|
||||
- [Keeping References](#keeping-references)
|
||||
- [Slots](#slots)
|
||||
- [Asynchronous rendering](#asynchronous-rendering)
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -67,55 +73,6 @@ 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
|
||||
change to it will cause a rerendering.
|
||||
|
||||
## Composition
|
||||
|
||||
The example above shows a QWeb template with a `t-on-click` directive. Widget
|
||||
templates are standard [QWeb](qweb.md) templates, but with an extra directive:
|
||||
`t-widget`. With the `t-widget` directive, widget templates can declare sub
|
||||
widgets:
|
||||
|
||||
```xml
|
||||
<div t-name="ParentWidget">
|
||||
<span>some text</span>
|
||||
<t t-widget="MyWidget" t-props="{info: 13}"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
class ParentWidget extends owl.Component {
|
||||
widgets = { MyWidget: MyWidget};
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
In this example, the `ParentWidget`'s template creates a widget `MyWidget` just
|
||||
after the span. See the [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
|
||||
that the template can access `state`, `props`, `env`, or any methods defined in the widget.
|
||||
|
||||
**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
|
||||
root widget element.
|
||||
|
||||
```xml
|
||||
<div t-name="ParentWidget">
|
||||
<t t-widget="MyWidget" class="someClass" style="font-weight:bold;" t-props="{info: 13}"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
Warning: there is a small caveat with dynamic class attributes: since Owl needs
|
||||
to be able to add/remove proper classes whenever necessary, it needs to be aware
|
||||
of the possible classes. Otherwise, it will not be able to make the difference
|
||||
between a valid css class added by the component, or some custom code, and a
|
||||
class that need to be removed. This is why we only support the explicit syntax
|
||||
with a class object:
|
||||
|
||||
```xml
|
||||
<t t-widget="MyWidget" t-att-class="{a: state.flagA, b: state.flagB}" />
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
An Owl component is a small class which represent a widget or some UI element.
|
||||
@@ -144,11 +101,22 @@ find a template with the component name (or one of its ancestor).
|
||||
- **`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,
|
||||
in some case. Note that `props` are owned by the parent, not by the component.
|
||||
As such, it should not ever be modified by the component.
|
||||
As such, it should not ever be modified by the component!!
|
||||
|
||||
- **`refs`** (Object): the `refs` object contains all references to sub DOM nodes
|
||||
or sub widgets defined by a `t-ref` directive in the component's template.
|
||||
|
||||
### Static Properties
|
||||
|
||||
- **`props`** (Object, optional): if given, this is an object that describes the
|
||||
type and shape of the (actual) props given to the component. If Owl mode is
|
||||
`dev`, this will be used to validate the props each time the component is
|
||||
created/updated. See [Props Validation](#props-validation) for more information.
|
||||
- **`defaultProps`** (Object, optional): if given, this object define default
|
||||
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
|
||||
change the initial object, a new object will be created instead.
|
||||
|
||||
### Methods
|
||||
|
||||
- **`mount(target)`** (async): this is the main way a component's hierarchy is added to the
|
||||
@@ -179,11 +147,6 @@ find a template with the component name (or one of its ancestor).
|
||||
if we have a `isMobile` key in the environment, to decide if we want a mobile
|
||||
interface or a destkop one.
|
||||
|
||||
- **`set(target, key, value)`**. This method is necessary in some cases when we
|
||||
need to modify the state of the component in a way that is not visible to the
|
||||
observer (see [observer's technical limitations](observer.md#technical-limitations)).
|
||||
For example, if we need to add a key to the state.
|
||||
|
||||
- **`destroy()`**. As its name suggests, this method will remove the component,
|
||||
and perform all necessary cleanup, such as unmounting the component, its children,
|
||||
removing the parent/children relationship. This method should almost never be
|
||||
@@ -198,13 +161,13 @@ a owl component:
|
||||
|
||||
| Method | Description |
|
||||
| ------------------------------------------------ | ----------------------------------------------------- |
|
||||
| **[constructor](#constructor)** | constructor |
|
||||
| **[willStart](#willStart)** | async, before first rendering |
|
||||
| **[constructor](#constructorparent-props)** | constructor |
|
||||
| **[willStart](#willstart)** | async, before first rendering |
|
||||
| **[mounted](#mounted)** | just after component is rendered and added to the DOM |
|
||||
| **[willUpdateProps](#willupdatepropsnextprops)** | async, before props update |
|
||||
| **[willPatch](#willpatch)** | just before 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 |
|
||||
|
||||
Notes:
|
||||
|
||||
@@ -345,6 +308,233 @@ the DOM. This is a good place to remove some listeners, for example.
|
||||
|
||||
This is the opposite method of `mounted`.
|
||||
|
||||
## Composition
|
||||
|
||||
The example above shows a QWeb template with a `t-on-click` directive. Widget
|
||||
templates are standard [QWeb](qweb.md) templates, but with an extra directive:
|
||||
`t-widget`. With the `t-widget` directive, widget templates can declare sub
|
||||
widgets:
|
||||
|
||||
```xml
|
||||
<div t-name="ParentWidget">
|
||||
<span>some text</span>
|
||||
<t t-widget="MyWidget" info="13"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
class ParentWidget extends owl.Component {
|
||||
widgets = { MyWidget: MyWidget};
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
In this example, the `ParentWidget`'s template creates a widget `MyWidget` just
|
||||
after the span. The `info` key will be added to the subwidget's props. Each
|
||||
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:
|
||||
`someString="'somevalue'"`. See the
|
||||
[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
|
||||
that the template can access `state`, `props`, `env`, or any methods defined in the widget.
|
||||
|
||||
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
|
||||
<div t-name="ParentWidget">
|
||||
<t t-widget="ChildWidget" count="state.val"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
class ParentWidget {
|
||||
widgets = { ChildWidget };
|
||||
state = { val: 4 };
|
||||
}
|
||||
```
|
||||
|
||||
Whenever the template is rendered, it will automatically create the subwidget
|
||||
`ChildWidget` at the correct place. It needs to find the reference to the
|
||||
actual component class in the special `widgets` key, or the class registered in
|
||||
QWeb's global registry (see `register` function of QWeb). It first looks inside
|
||||
the local `widgets` key, then fallbacks on the global registry.
|
||||
|
||||
_Props_: In this example, the child widget will receive the object `{count: 4}` in its
|
||||
constructor. This will be assigned to the `props` variable, which can be accessed
|
||||
on the widget (and also, in the template). Whenever the state is updated, then
|
||||
the subwidget will also be updated automatically.
|
||||
|
||||
Note that there are some restrictions on prop names: `class`, `style` and any
|
||||
string which starts with `t-` are not allowed.
|
||||
|
||||
The `t-widget` directive also accepts dynamic values with string interpolation
|
||||
(like the [`t-attf-`](qweb.md#dynamic-attributes) directive):
|
||||
|
||||
```xml
|
||||
<div t-name="ParentWidget">
|
||||
<t t-widget="ChildWidget{{id}}"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
class ParentWidget {
|
||||
widgets = { ChildWidget1, ChildWidget2 };
|
||||
state = { id: 1 };
|
||||
}
|
||||
```
|
||||
|
||||
**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
|
||||
root widget element.
|
||||
|
||||
```xml
|
||||
<div t-name="ParentWidget">
|
||||
<t t-widget="MyWidget" class="someClass" style="font-weight:bold;" info="13"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
Warning: there is a small caveat with dynamic class attributes: since Owl needs
|
||||
to be able to add/remove proper classes whenever necessary, it needs to be aware
|
||||
of the possible classes. Otherwise, it will not be able to make the difference
|
||||
between a valid css class added by the component, or some custom code, and a
|
||||
class that need to be removed. This is why we only support the explicit syntax
|
||||
with a class object:
|
||||
|
||||
```xml
|
||||
<t t-widget="MyWidget" t-att-class="{a: state.flagA, b: state.flagB}" />
|
||||
```
|
||||
|
||||
### Event handling
|
||||
|
||||
In a component's template, it is useful to be able to register handlers on some
|
||||
elements to some specific events. This is what makes a template _alive_. There
|
||||
are four different use cases.
|
||||
|
||||
1. Register an event handler on a DOM node (_pure_ DOM event)
|
||||
2. Register an event handler on a component (_pure_ DOM event)
|
||||
3. Register an event handler on a DOM node (_business_ DOM event)
|
||||
4. Register an event handler on a component (_business_ DOM event)
|
||||
|
||||
A _pure_ DOM event is directly triggered by a user interaction (e.g. a `click`).
|
||||
|
||||
```xml
|
||||
<button t-on-click="someMethod">Do something</button>
|
||||
```
|
||||
|
||||
This will be roughly translated in javascript like this:
|
||||
|
||||
```js
|
||||
button.addEventListener("click", widget.someMethod.bind(widget));
|
||||
```
|
||||
|
||||
The suffix (`click` in this example) is simply the name of the actual DOM
|
||||
event.
|
||||
|
||||
A _business_ DOM event is triggered by a call to `trigger` on a component.
|
||||
|
||||
```xml
|
||||
<t t-widget="MyWidget" t-on-menu-loaded="someMethod"/>
|
||||
```
|
||||
|
||||
```js
|
||||
class MyWidget {
|
||||
someWhere() {
|
||||
const payload = ...;
|
||||
this.trigger('menu-loaded', payload);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
(`this.el`). The event bubbles and is cancelable. The parent widget listening
|
||||
to event `menu-loaded` will receive the payload in its `someMethod` handler
|
||||
(in the `detail` property of the event), whenever the event is triggered.
|
||||
|
||||
```js
|
||||
class ParentWidget {
|
||||
someMethod(ev) {
|
||||
const payload = ev.detail;
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
By convention, we use KebabCase for the name of _business_ events.
|
||||
|
||||
In order to remove the DOM event details from the event handlers (like calls to
|
||||
`event.preventDefault`) and let them focus on data logic, _modifiers_ can be
|
||||
specified as additional suffixes of the `t-on` directive.
|
||||
|
||||
| Modifier | Description |
|
||||
| ---------- | ----------------------------------------------------------------- |
|
||||
| `.stop` | calls `event.stopPropagation()` before calling the method |
|
||||
| `.prevent` | calls `event.preventDefault()` before calling the method |
|
||||
| `.self` | calls the method only if the `event.target` is the element itself |
|
||||
|
||||
```xml
|
||||
<button t-on-click.stop="someMethod">Do something</button>
|
||||
```
|
||||
|
||||
Note that modifiers can be combined (ex: `t-on-click.stop.prevent`), and that
|
||||
the order may matter. For instance `t-on-click.prevent.self` will prevent all
|
||||
clicks while `t-on-click.self.prevent` will only prevent clicks on the element
|
||||
itself.
|
||||
|
||||
The `t-on` directive also allows to prebind some arguments. For example,
|
||||
|
||||
```xml
|
||||
<button t-on-click="someMethod(expr)">Do something</button>
|
||||
```
|
||||
|
||||
Here, `expr` is a valid Owl expression, so it could be `true` or some variable
|
||||
from the rendering context.
|
||||
|
||||
### `t-key` directive
|
||||
|
||||
Even though Owl tries to be as declarative as possible, some DOM state is still
|
||||
locked inside the DOM: for example, the scrolling state, the current user selection,
|
||||
the focused element or the state of an input. This is why we use a virtual dom
|
||||
algorithm to keep the actual DOM node as much as possible. However, this is
|
||||
sometimes not enough, and we need to help Owl decide if an element is actually
|
||||
the same, or is different. The `t-key` directive is used to give an identity to an element.
|
||||
|
||||
There are three main use cases:
|
||||
|
||||
- _elements in a list_:
|
||||
|
||||
```xml
|
||||
<span t-foreach="todos" t-as="todo" t-key="todo.id">
|
||||
<t t-esc="todo.text"/>
|
||||
</span>
|
||||
```
|
||||
|
||||
- _`t-if`/`t-else`_
|
||||
|
||||
- _animations_: give a different identity to a component. Ex: thread id with
|
||||
animations on add/remove message.
|
||||
|
||||
### `t-mounted` directive
|
||||
|
||||
The `t-mounted` directive allows to register a callback to execute whenever the node
|
||||
is inserted into the DOM.
|
||||
|
||||
```xml
|
||||
<div><input t-ref="someInput" t-mounted="focusMe"/></div>
|
||||
```
|
||||
|
||||
```js
|
||||
class MyWidget extends owl.Component {
|
||||
...
|
||||
focusMe() {
|
||||
this.refs.someInput.focus();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Semantics
|
||||
|
||||
We give here an informal description of the way components are created/updated
|
||||
@@ -432,7 +622,194 @@ Here is what Owl will do:
|
||||
|
||||
6. `patched` hooks are called on `D`, `C`
|
||||
|
||||
## Asynchronous rendering
|
||||
### Props Validation
|
||||
|
||||
As an application becomes complex, it may be quite unsafe to define props in an informal way. This leads to two issues:
|
||||
|
||||
- hard to tell how a component should be used, by looking at its code.
|
||||
- unsafe, it is easy to send wrong props into a component, either by refactoring a component, or one of its parent.
|
||||
|
||||
A props type system would solve both issues, by describing the types and shapes
|
||||
of the props. Here is how it works in Owl:
|
||||
|
||||
- `props` key is a static key (so, different from `this.props` in a component instance)
|
||||
- it is optional: it is ok for a component to not define a `props` key.
|
||||
- props are validated whenever a component is created/updated
|
||||
- props are only validated in `dev` mode (see [tooling page](tooling.md#development-mode))
|
||||
- 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.
|
||||
|
||||
For example:
|
||||
|
||||
```js
|
||||
class ComponentA extends owl.Component {
|
||||
static props = ['id', 'url'];
|
||||
|
||||
...
|
||||
}
|
||||
|
||||
class ComponentB extends owl.Component {
|
||||
static props = {
|
||||
count: {type: Number},
|
||||
messages: {
|
||||
type: Array,
|
||||
element: {type: Object, shape: {id: Boolean, text: 'string' }
|
||||
},
|
||||
date: Date,
|
||||
combinedVal: [Number, Boolean]
|
||||
};
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
- it is an object or a list of strings
|
||||
- a list of strings is a simplified props definition, which only lists the name
|
||||
of the props.
|
||||
- all props are by default required, unless they are defined with `optional: true`
|
||||
(in that case, validation is only done if there is a value)
|
||||
- valid types are: `Number, String, Boolean, Object, Array, Date, Function`, and all
|
||||
constructor functions (so, if you have a `Person` class, it can be used as a type)
|
||||
- arrays are homogeneous (all elements have the same type/shape)
|
||||
|
||||
For each key, a `prop` definition is either a constructor, a list of constructors, or an object:
|
||||
|
||||
- a constructor: this should describe the type, for example: `id: Number` describe
|
||||
the props `id` as a number
|
||||
- a list of constructors. In that case, this means that we allow more than one
|
||||
type. For example, `id: [Number, String]` means that `id` can be either a string
|
||||
or a number.
|
||||
- an object. This makes it possible to have more expressive definition. The following sub keys are then allowed:
|
||||
- `type`: the main type of the prop being validated
|
||||
- `element`: if the type was `Array`, then the `element` key describes the type of each element in the array. It is optional (not set means that we only validate the array, not its elements),
|
||||
- `shape`: if the type was `Object`, then the `shape` key describes the interface of the object. It is optional (not set means that we only validate the object, not its elements)
|
||||
|
||||
Examples:
|
||||
|
||||
```js
|
||||
// only the existence of those 3 keys is documented
|
||||
static props = ['message', 'id', 'date'];
|
||||
```
|
||||
|
||||
```js
|
||||
static props = {
|
||||
messageIds: {type: Array, element: Number}, // list of number
|
||||
otherArr: {type: Array}, // just array. no validation is made on sub elements
|
||||
otherArr2: Array, // same as otherArr
|
||||
someObj: {type: Object}, // just an object, no internal validation
|
||||
someObj2: {
|
||||
type: Object,
|
||||
shape: {
|
||||
id: Number,
|
||||
name: {type: String, optional: true},
|
||||
url: String
|
||||
]}, // object, with keys id (number), name (string, optional) and url (string)
|
||||
someFlag: Boolean, // a boolean, mandatory (even if `false`)
|
||||
someVal: [Boolean, Date] // either a boolean or a date
|
||||
};
|
||||
```
|
||||
|
||||
### Keeping references
|
||||
|
||||
The `t-ref` directive helps a component keep reference to some inside part of it.
|
||||
Like the `t-on` directive, it can work either on a DOM node, or on a component:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<div t-ref="someDiv"/>
|
||||
<t t-widget="SubWidget" t-ref="someWidget"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
In this example, the widget will be able to access the `div` and the component
|
||||
inside the special `refs` variable:
|
||||
|
||||
```js
|
||||
this.refs.someDiv;
|
||||
this.refs.someWidget;
|
||||
```
|
||||
|
||||
This is useful for various usecases: for example, integrating with an external
|
||||
library that needs to render itself inside an actual DOM node. Or for calling
|
||||
some method on a sub widget.
|
||||
|
||||
Note: if used on a component, the reference will be set in the `refs`
|
||||
variable between `willPatch` and `patched`.
|
||||
|
||||
The `t-ref` directive also accepts dynamic values with string interpolation
|
||||
(like the [`t-attf-`](qweb.md#dynamic-attributes) and
|
||||
`t-widget` directives). For example, if we have
|
||||
`id` set to 44 in the rendering context,
|
||||
|
||||
```xml
|
||||
<div t-ref="widget_{{id}}"/>
|
||||
```
|
||||
|
||||
```js
|
||||
this.refs.widget_44;
|
||||
```
|
||||
|
||||
### Slots
|
||||
|
||||
To make generic components, it is useful to be able for a parent widget to *inject*
|
||||
some sub template, but still be the owner. For example, a generic dialog widget
|
||||
will need to render some content, some footer, but with the parent as the
|
||||
rendering context.
|
||||
|
||||
This is what *slots* are for.
|
||||
|
||||
```xml
|
||||
<div t-name="Dialog" class="modal">
|
||||
<div class="modal-title"><t t-esc="props.title"/></div>
|
||||
<div class="modal-content">
|
||||
<t t-slot="content"/>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<t t-slot="footer"/>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
Slots are defined by the caller, with the `t-set` directive:
|
||||
|
||||
```xml
|
||||
<div t-name="SomeWidget">
|
||||
<div>some widget</div>
|
||||
<t t-widget="Dialog" title="Some Dialog">
|
||||
<t t-set="content">
|
||||
<div>hey</div>
|
||||
</t>
|
||||
<t t-set="footer">
|
||||
<button t-on-click="doSomething">ok</button>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
```
|
||||
|
||||
In this example, the widget `Dialog` will render the slots `content` and `footer`
|
||||
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.
|
||||
|
||||
Warning! Slots have a technical constraint: the result of the slot rendering
|
||||
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>
|
||||
```
|
||||
|
||||
### Asynchronous rendering
|
||||
|
||||
Working with asynchronous code always adds a lot of complexity to a system. Whenever
|
||||
different parts of a system are active at the same time, one needs to think
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# 🦉 Event Bus 🦉
|
||||
|
||||
It is sometimes useful to use a `Bus` to communicate informations between various
|
||||
parts of the code. Owl has a very simple bus class, which manages subscriptions,
|
||||
triggering events, and callbacks.
|
||||
|
||||
```js
|
||||
const bus = new owl.EventBus();
|
||||
|
||||
bus.on("some-event", null, function(...args) {
|
||||
console.log(...args);
|
||||
});
|
||||
|
||||
bus.trigger("some-event", 1, 2, 3);
|
||||
// [1,2,3] will be logged to the console
|
||||
```
|
||||
|
||||
Its API is:
|
||||
|
||||
| Method | Description |
|
||||
| -------------------------------- | --------------------------------- |
|
||||
| `on(eventType, owner, callback)` | add a listener |
|
||||
| `off(eventType, owner)` | remove all listeners for an owner |
|
||||
| `trigger(eventType, ...args)` | trigger an event |
|
||||
| `clear` | remove all subscriptions |
|
||||
|
||||
Note that the [`Store`](store.md) is an example of an `EventBus`.
|
||||
+17
-21
@@ -20,35 +20,31 @@ obj.a.b = 2;
|
||||
## Technical Limitations
|
||||
|
||||
Since the observer uses getters and setters, it is actually unable to react to
|
||||
changes in two situations:
|
||||
changes in three situations:
|
||||
|
||||
- adding a key to an object:
|
||||
- adding a key to an object
|
||||
- deleting a key from an object
|
||||
- modifying an array by setting a new value at a given index
|
||||
|
||||
In those situations, we need a way to tell the observer that something happened.
|
||||
This can be done by using the `set` and `delete` (only for objects) static
|
||||
methods of the `Observer`.
|
||||
|
||||
```javascript
|
||||
const observer = new owl.Observer();
|
||||
const obj = { a: 1 };
|
||||
observer.observe(obj);
|
||||
obj.b = 2; // will do nothing
|
||||
obj.b = 2; // won't notify the change
|
||||
owl.Observer.set(obj, "b", 2); // will notify the change
|
||||
|
||||
delete obj.b; // won't notify the change
|
||||
owl.Observer.delete(obj, "b"); // will notify the change
|
||||
```
|
||||
|
||||
In that case, we need a way to tell the observer that something happened.
|
||||
This can be done by using the `set` method:
|
||||
|
||||
```javascript
|
||||
observer.set(obj, "b", 2);
|
||||
```
|
||||
|
||||
- modifying an array by setting a new value at a given index:
|
||||
|
||||
```javascript
|
||||
const observer = new owl.Observer();
|
||||
const obj = { todos: [{ id: 1, text: "todo" }] };
|
||||
observer.observe(obj);
|
||||
obj[0] = { id: 2, text: "othertodo" }; // will do nothing, and obj[0] is not observed
|
||||
```
|
||||
|
||||
In that case, the solution is the same, we can simply use the `set` method:
|
||||
|
||||
```javascript
|
||||
observer.set(obj, 0, { id: 2, text: "othertodo" });
|
||||
const arr = ["a"];
|
||||
observer.observe(arr);
|
||||
arr[0] = "b"; // won't notify the change
|
||||
owl.Observer.set(arr, 0, "b"); // will notify the change
|
||||
```
|
||||
|
||||
+231
-319
@@ -3,45 +3,77 @@
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Directives](#directives)
|
||||
- [QWeb Engine](#qweb-engine)
|
||||
- [QWeb Specification](#qweb-specification)
|
||||
|
||||
- [Static html nodes](#static-html-nodes)
|
||||
- [`t-esc` directive](#t-esc-directive)
|
||||
- [`t-raw` directive](#t-raw-directive)
|
||||
- [`t-set` directive](#t-set-directive)
|
||||
- [`t-if` directive](#t-if-directive)
|
||||
- [Expression evaluation](#expression-evaluation)
|
||||
- [Dynamic attributes (`t-att` and `t-attf` directives)](#dynamic-attributes-t-att-and-t-attf-directives)
|
||||
- [`t-attf` directive (dynamic attributes)](#t-att-directive-dynamic-attributes)
|
||||
- [`t-call` directive (sub templates)](#t-call-directive-sub-templates)
|
||||
|
||||
- [JS/OWL Specific Extensions](#jsowl-specific-extensions)
|
||||
|
||||
- [`t-on` directive](#t-on-directive)
|
||||
- [Component: `t-widget`, `t-props`](#component-t-widget-t-props)
|
||||
- [`t-ref` directive](#t-ref-directive)
|
||||
- [`t-key` directive](#t-key-directive)
|
||||
- [`t-transition` directive](#t-transition-directive)
|
||||
- [`t-mounted` directive](#t-mounted-directive)
|
||||
- [Debugging (`t-debug` and `t-log`)](#debugging-t-debug-and-t-log)
|
||||
- [Reference](#reference)
|
||||
- [White spaces](#white-spaces)
|
||||
- [Root nodes](#root-nodes)
|
||||
- [Expression evaluation](#expression-evaluation)
|
||||
- [Static html nodes](#static-html-nodes)
|
||||
- [Outputting data](#outputting-data)
|
||||
- [Setting Variables](#setting-variables)
|
||||
- [Conditionals](#conditionals)
|
||||
- [Dynamic attributes](#dynamic-attributes)
|
||||
- [Loops](#loops)
|
||||
- [Rendering Sub Templates](#rendering-sub-templates)
|
||||
- [Debugging](#debugging)
|
||||
|
||||
## Overview
|
||||
|
||||
[QWeb](https://www.odoo.com/documentation/12.0/reference/qweb.html) is the primary templating engine used by Odoo. It is based on the XML format, and used
|
||||
mostly to generate html. In OWL, QWeb templates are compiled into functions that
|
||||
generate a virtual dom representation of the html.
|
||||
mostly to generate HTML. In OWL, QWeb templates are compiled into functions that
|
||||
generate a virtual dom representation of the HTML.
|
||||
|
||||
Template directives are specified as XML attributes prefixed with `t-`, for instance `t-if` for conditionals, with elements and other attributes being rendered directly.
|
||||
|
||||
To avoid element rendering, a placeholder element `<t>` is also available, which executes its directive but doesn’t generate any output in and of itself.
|
||||
|
||||
The QWeb implementation in the OWL project is slightly different. It compiles
|
||||
templates into functions that output a virtual DOM instead of a string. This is
|
||||
necessary for the component system. In addition, it has a few extra directives
|
||||
(see [OWL Specific Extensions](#owlspecificextensions))
|
||||
```xml
|
||||
<div>
|
||||
<span t-if="somecondition">Some string</span>
|
||||
<ul t-else="1">
|
||||
<li t-foreach="messages" t-as="message">
|
||||
<t t-esc="message">
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
```
|
||||
|
||||
The QWeb class in the OWL project is an implementation of that specification
|
||||
with a few interesting points:
|
||||
|
||||
- it compiles templates into functions that output a virtual DOM instead of a
|
||||
string. This is necessary for the component system.
|
||||
- it has a few extra directives: `t-widget`, `t-on`, ...
|
||||
|
||||
## Directives
|
||||
|
||||
We present here a list of all standard QWeb directives:
|
||||
|
||||
| Name | Description |
|
||||
| ------------------------------ | ------------------------------------------------------------ |
|
||||
| `t-esc` | [Outputting safely a value](#outputting-data) |
|
||||
| `t-raw` | [Outputting value, without escaping](#outputting-data) |
|
||||
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
|
||||
| `t-if`, `t-elif`, `t-else`, | [conditionally rendering](#conditionals) |
|
||||
| `t-foreach`, `t-as` | [Loops](#loops) |
|
||||
| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
|
||||
| `t-call` | [Rendering sub templates](#rendering-sub-templates) |
|
||||
| `t-debug`, `t-log` | [Debugging](#debugging) |
|
||||
| `t-name` | [Defining a template (not really a directive)](#qweb-engine) |
|
||||
|
||||
The component system in Owl requires additional directives, to express various
|
||||
needs. Here is a list of all Owl specific directives:
|
||||
|
||||
| Name | Description |
|
||||
| ------------------------- | --------------------------------------------------------------------------------------- |
|
||||
| `t-widget`, `t-keepalive` | [Defining a sub component](component.md#composition) |
|
||||
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#keeping-references) |
|
||||
| `t-key` | [Defining a key (to help virtual dom reconciliation)](component.md#t-key-directive) |
|
||||
| `t-on-*` | [Event handling](component.md#event-handling) |
|
||||
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
|
||||
| `t-mounted` | [Callback when a node or component is mounted](component.md#t-mounted-directive) |
|
||||
| `t-slot` | [Rendering a slot](component.md#slots) |
|
||||
|
||||
## QWeb Engine
|
||||
|
||||
@@ -87,12 +119,113 @@ It's API is quite simple:
|
||||
const vnode = qweb.render("App", widget);
|
||||
```
|
||||
|
||||
## QWeb Specification
|
||||
- **`register(name, Component)`**: static function to register an OWL Component
|
||||
to QWeb's global registry. Globally registered Components can be used in
|
||||
templates (see the `t-widget` directive). This is useful for commonly used
|
||||
components accross the application.
|
||||
|
||||
```js
|
||||
class Dialog extends owl.Component { ... }
|
||||
QWeb.register("Dialog", Dialog);
|
||||
|
||||
...
|
||||
|
||||
class ParentWidget extends owl.Component { ... }
|
||||
qweb.addTemplate("ParentWidget", "<div><t t-widget='Dialog'/></div>");
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
We define in this section the specification of how `QWeb` templates should be
|
||||
rendered.
|
||||
rendered. Note that we only document here the standard QWeb specification. Owl
|
||||
specific extensions are documented in various other parts of the documentation.
|
||||
|
||||
### Static html nodes
|
||||
### White spaces
|
||||
|
||||
White spaces in a templates are handled in a special way:
|
||||
|
||||
- consecutive whitespaces are always condensed to a single whitespace
|
||||
- if a whitespace-only text node contains a linebreak, it is ignored
|
||||
- the previous rules do not apply if we are in a `<pre>` tag
|
||||
|
||||
### Root nodes
|
||||
|
||||
For many reasons, Owl QWeb templates should have a single root node. More
|
||||
precisely, the result of a template rendering should have a single root node:
|
||||
|
||||
```xml
|
||||
<!–– not ok: two root nodes ––>
|
||||
<t>
|
||||
<div>foo</div>
|
||||
<div>bar</div>
|
||||
</t>
|
||||
|
||||
<!–– ok: result has one single root node ––>
|
||||
<t>
|
||||
<div t-if="someCondition">foo</div>
|
||||
<span t-else="1">bar</span>
|
||||
</t>
|
||||
```
|
||||
|
||||
Extra root nodes will actually be ignored (even though they will be rendered
|
||||
in memory).
|
||||
|
||||
Note: this does not apply to subtemplates (see the `t-call` directive). In that
|
||||
case, they will be inlined in the main template, and can actually have many
|
||||
root nodes.
|
||||
|
||||
### Expression evaluation
|
||||
|
||||
QWeb expressions are strings that will be processed at compile time. Each variable in
|
||||
the javascript expression will be replaced by a lookup in the context (so, the
|
||||
widget). For example, `a + b.c(d)` will be converted into:
|
||||
|
||||
```js
|
||||
context["a"] + context["b"].c(context["d"]);
|
||||
```
|
||||
|
||||
It is useful to explain the various rules that applies on these expressions:
|
||||
|
||||
1. it should be a simple expression which returns a value. It cannot be a statement.
|
||||
|
||||
```xml
|
||||
<div><p t-if="1 + 2 === 3">ok</p></div>
|
||||
```
|
||||
|
||||
is valid, but the following is not valid:
|
||||
|
||||
```xml
|
||||
<div><p t-if="console.log(1)">NOT valid</p></div>
|
||||
```
|
||||
|
||||
2. it can use anything in the rendering context (typically, the component):
|
||||
|
||||
```xml
|
||||
<p t-if="user.birthday === today()">Happy bithday!</p>
|
||||
```
|
||||
|
||||
is valid, and will read the `user` object from the context, and call the
|
||||
`today` function.
|
||||
|
||||
3. it can use a few special operators to avoid using symbols such as `<`, `>`,
|
||||
`&` or `|`. This is useful to make sure that we still write valid XML.
|
||||
|
||||
| Word | will be replaced by |
|
||||
| ----- | ------------------- |
|
||||
| `and` | `&&` |
|
||||
| `or` | `\|\|` |
|
||||
| `gt` | `>` |
|
||||
| `gte` | `>=` |
|
||||
| `lt` | `<` |
|
||||
| `lte` | `<=` |
|
||||
|
||||
So, one can write this:
|
||||
|
||||
```xml
|
||||
<div><p t-if="10 + 2 gt 5">ok</p></div>
|
||||
```
|
||||
|
||||
### Static Html Nodes
|
||||
|
||||
Normal, regular html nodes are rendered into themselves:
|
||||
|
||||
@@ -100,7 +233,7 @@ Normal, regular html nodes are rendered into themselves:
|
||||
<div>hello</div> <!–– rendered as itself ––>
|
||||
```
|
||||
|
||||
### `t-esc` directive
|
||||
### Outputting Data
|
||||
|
||||
The `t-esc` directive is necessary whenever you want to add a dynamic text
|
||||
expression in a template. The text is escaped to avoid security issues.
|
||||
@@ -115,14 +248,12 @@ rendered with the value `value` set to `42` in the rendering context yields:
|
||||
<p>42</p>
|
||||
```
|
||||
|
||||
### `t-raw` directive
|
||||
|
||||
The `t-raw` directive is almost the same as `t-esc`, but without the escaping.
|
||||
This is mostly useful to inject a raw html string somewhere. Obviously, this
|
||||
is unsafe to do in general, and should only be used for strings known to be safe.
|
||||
|
||||
```xml
|
||||
<p><t t-esc="value"/></p>
|
||||
<p><t t-raw="value"/></p>
|
||||
```
|
||||
|
||||
rendered with the value `value` set to `<span>foo</span>` in the rendering context yields:
|
||||
@@ -131,7 +262,7 @@ rendered with the value `value` set to `<span>foo</span>` in the rendering conte
|
||||
<p><span>foo</span></p>
|
||||
```
|
||||
|
||||
### `t-set` directive
|
||||
### Setting Variables
|
||||
|
||||
QWeb allows creating variables from within the template, to memoize a computation (to use it multiple times), give a piece of data a clearer name, ...
|
||||
|
||||
@@ -163,7 +294,7 @@ This is done via the `t-set` directive, which takes the name of the variable to
|
||||
The `t-set` directive acts like a regular variable in most programming language.
|
||||
It is lexically scoped (inner nodes are sub scopes), can be shadowed, ...
|
||||
|
||||
### `t-if` directive
|
||||
### Conditionals
|
||||
|
||||
The `t-if` directive is useful to conditionally render something. It evaluates
|
||||
the expression given as attribute value, and then acts accordingly.
|
||||
@@ -213,52 +344,7 @@ Extra conditional branching directives `t-elif` and `t-else` are also available:
|
||||
</div>
|
||||
```
|
||||
|
||||
### Expression evaluation
|
||||
|
||||
It is useful to explain the various rules that applies on QWeb expressions. These
|
||||
expressions are strings that will be converted to a javascript expression at
|
||||
compile time.
|
||||
|
||||
1. it should be a simple expression which returns a value. It cannot be a statement.
|
||||
|
||||
```xml
|
||||
<div><p t-if="1 + 2 === 3">ok</p></div>
|
||||
```
|
||||
|
||||
is valid, but the following is not valid:
|
||||
|
||||
```xml
|
||||
<div><p t-if="console.log(1)">NOT valid</p></div>
|
||||
```
|
||||
|
||||
2. it can use anything in the rendering context:
|
||||
|
||||
```xml
|
||||
<p t-if="user.birthday == today()">Happy bithday!</p>
|
||||
```
|
||||
|
||||
is valid, and will read the `user` object from the context, and call the
|
||||
`today` function.
|
||||
|
||||
3. it can use a few special operators to avoid using symbols such as `<`, `>`,
|
||||
`&` or `|`. This is useful to make sure that we still write valid XML.
|
||||
|
||||
| Word | will be replaced by |
|
||||
| ----- | ------------------- |
|
||||
| `and` | `&&` |
|
||||
| `or` | `\|\|` |
|
||||
| `gt` | `>` |
|
||||
| `gte` | `>=` |
|
||||
| `lt` | `<` |
|
||||
| `lte` | `<=` |
|
||||
|
||||
So, one can write this:
|
||||
|
||||
```xml
|
||||
<div><p t-if="10 + 2 gt 5">ok</p></div>
|
||||
```
|
||||
|
||||
### Dynamic attributes (`t-att` and `t-attf` directives)
|
||||
### Dynamic attributes
|
||||
|
||||
One can use the `t-att-` directive to add dynamic attributes. Its main use is to
|
||||
evaluate an expression (at rendering time) and bind an attribute to its result:
|
||||
@@ -283,14 +369,72 @@ it, you get string interpolation:
|
||||
<!-- result if values are set to 1,2 and 3: <div foo="a 0 is 1 of 2 ]"></div> -->
|
||||
```
|
||||
|
||||
For historical reason, there is an alternate form of string interpolation:
|
||||
### Loops
|
||||
|
||||
QWeb has an iteration directive `t-foreach` which take an expression returning the
|
||||
collection to iterate on, and a second parameter `t-as` providing the name to use
|
||||
for the current item of the iteration:
|
||||
|
||||
```xml
|
||||
<div t-attf-foo="a #{value1} is #{value2} of #{value3} ]"/>
|
||||
<!-- result if values are set to 1,2 and 3: <div foo="a 0 is 1 of 2 ]"></div> -->
|
||||
<t t-foreach="[1, 2, 3]" t-as="i">
|
||||
<p><t t-esc="i"/></p>
|
||||
</t>
|
||||
```
|
||||
|
||||
### `t-call` directive (sub templates)
|
||||
will be rendered as:
|
||||
|
||||
```xml
|
||||
<p>1</p>
|
||||
<p>2</p>
|
||||
<p>3</p>
|
||||
```
|
||||
|
||||
Like conditions, `t-foreach` applies to the element bearing the directive’s attribute, and
|
||||
|
||||
```xml
|
||||
<p t-foreach="[1, 2, 3]" t-as="i">
|
||||
<t t-esc="i"/>
|
||||
</p>
|
||||
```
|
||||
|
||||
is equivalent to the previous example.
|
||||
|
||||
`t-foreach` can iterate on an array (the current item will be the current value)
|
||||
or an object (the current item will be the current key).
|
||||
|
||||
In addition to the name passed via t-as, `t-foreach` provides a few other
|
||||
variables for various data points (note: `$as` will be replaced by the name
|
||||
passed to `t-as`):
|
||||
|
||||
- `$as_value`: the current iteration value, identical to `$as` for lists and
|
||||
integers, but for objects, it provides the value (where `$as` provides the key)
|
||||
- `$as_index`: the current iteration index (the first item of the iteration has index 0)
|
||||
- `$as_first`: whether the current item is the first of the iteration
|
||||
(equivalent to `$as_index == 0`)
|
||||
- `$as_last`: whether the current item is the last of the iteration
|
||||
(equivalent to `$as_index + 1 == $as_size`), requires the iteratee’s size be
|
||||
available
|
||||
|
||||
These extra variables provided and all new variables created into the `t-foreach`
|
||||
are only available in the scope of the `t-foreach`. If the variable exists outside
|
||||
the context of the `t-foreach`, the value is copied at the end of the foreach
|
||||
into the global context.
|
||||
|
||||
```xml
|
||||
<t t-set="existing_variable" t-value="False"/>
|
||||
<!-- existing_variable now False -->
|
||||
|
||||
<p t-foreach="Array(3)" t-as="i">
|
||||
<t t-set="existing_variable" t-value="True"/>
|
||||
<t t-set="new_variable" t-value="True"/>
|
||||
<!-- existing_variable and new_variable now True -->
|
||||
</p>
|
||||
|
||||
<!-- existing_variable always True -->
|
||||
<!-- new_variable undefined -->
|
||||
```
|
||||
|
||||
### Rendering Sub Templates
|
||||
|
||||
QWeb templates can be used for top level rendering, but they can also be used
|
||||
from within another template (to avoid duplication or give names to parts of
|
||||
@@ -338,205 +482,7 @@ will result in :
|
||||
</div>
|
||||
```
|
||||
|
||||
## JS/OWL Specific Extensions
|
||||
|
||||
### `t-on` directive
|
||||
|
||||
In a component's template, it is useful to be able to register handlers on some
|
||||
elements to some specific events. This
|
||||
is what makes a template _alive_. There are two different use cases.
|
||||
|
||||
1. Register an event handler on a DOM node
|
||||
|
||||
```xml
|
||||
<button t-on-click="someMethod">Do something</button>
|
||||
```
|
||||
|
||||
This will be roughly translated in javascript like this:
|
||||
|
||||
```js
|
||||
button.addEventListener("click", widget.someMethod.bind(widget));
|
||||
```
|
||||
|
||||
The suffix (`click` in this example) is simply the name of the actual DOM
|
||||
event.
|
||||
|
||||
2. Register an event handler on a component. This will not capture a DOM event,
|
||||
but rather a _business_ event:
|
||||
|
||||
```xml
|
||||
<t t-widget="MyWidget" t-on-menuLoaded="someMethod"/>
|
||||
```
|
||||
|
||||
```js
|
||||
class MyWidget {
|
||||
someWhere() {
|
||||
const payload = ...;
|
||||
this.trigger('menuLoaded', payload);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Here, the parent widget will receive the payload in its `someMethod` handler,
|
||||
whenever the event is triggered.
|
||||
|
||||
The `t-on` directive also allows to prebind some arguments. For example,
|
||||
|
||||
```xml
|
||||
<button t-on-click="someMethod(expr)">Do something</button>
|
||||
```
|
||||
|
||||
Here, `expr` is a valid Owl expression, so it could be `true` or some variable
|
||||
from the rendering context.
|
||||
|
||||
### Component: `t-widget`, `t-props`
|
||||
|
||||
The `t-widget` and the `t-props` directives are the key to a declarative component
|
||||
system. They allow a template to define where and how a sub widget is created
|
||||
and/or updated. For example:
|
||||
|
||||
```xml
|
||||
<div t-name="ParentWidget">
|
||||
<t t-widget="ChildWidget" t-props="{count: state.val}"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
class ParentWidget {
|
||||
widgets = { ChildWidget };
|
||||
state = { val: 4 };
|
||||
}
|
||||
```
|
||||
|
||||
Whenever the template is rendered, it will automatically create the subwidget
|
||||
`ChildWidget` at the correct place. It needs to find the reference to the
|
||||
actual component class in the special `widgets` key.
|
||||
|
||||
In this example, the child widget will receive the object `{count: 4}` in its
|
||||
constructor. This will be assigned to the `props` variable, which can be accessed
|
||||
on the widget (and also, in the template). Whenever the state is updated, then
|
||||
the subwidget will also be updated automatically.
|
||||
|
||||
### `t-ref` directive
|
||||
|
||||
The `t-ref` directive helps a component keep reference to some inside part of it.
|
||||
Like the `t-on` directive, it can work either on a DOM node, or on a component:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<div t-ref="someDiv"/>
|
||||
<t t-widget="SubWidget" t-ref="someWidget"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
In this example, the widget will be able to access the `div` and the component
|
||||
inside the special `refs` variable:
|
||||
|
||||
```js
|
||||
this.refs.someDiv;
|
||||
this.refs.someWidget;
|
||||
```
|
||||
|
||||
This is useful for various usecases: for example, integrating with an external
|
||||
library that needs to render itself inside an actual DOM node. Or for calling
|
||||
some method on a sub widget.
|
||||
|
||||
Note: if used on a component, the reference will be set in the `refs`
|
||||
variable between `willPatch` and `patched`.
|
||||
|
||||
### `t-key` directive
|
||||
|
||||
Even though Owl tries to be as declarative as possible, some DOM state is still
|
||||
locked inside the DOM: for example, the scrolling state, the current user selection,
|
||||
the focused element or the state of an input. This is why we use a virtual dom
|
||||
algorithm to keep the actual DOM node as much as possible. However, this is
|
||||
sometimes not enough, and we need to help Owl decide if an element is actually
|
||||
the same, or is different. The `t-key` directive is used to give an identity to an element.
|
||||
|
||||
There are three main use cases:
|
||||
|
||||
- _elements in a list_:
|
||||
|
||||
```xml
|
||||
<span t-foreach="todos" t-as="todo" t-key="todo.id">
|
||||
<t t-esc="todo.text"/>
|
||||
</span>
|
||||
```
|
||||
|
||||
- _`t-if`/`t-else`_
|
||||
|
||||
- _animations_: give a different identity to a component. Ex: thread id with
|
||||
animations on add/remove message.
|
||||
|
||||
### `t-transition` directive
|
||||
|
||||
To perform useful transition effects, whenever an element appears or disappears,
|
||||
it is necessary to add/remove some css style or class at some precise moment in
|
||||
the lifetime of a node. Since this is not easy to do by hand, Owl `t-transition`
|
||||
directive is there to help.
|
||||
|
||||
Whenever a node has a `t-transition` directive, with a `name` value, the following
|
||||
will happen:
|
||||
|
||||
At node creation:
|
||||
|
||||
- the css classes `name-enter` and `name-enter-active` will be added before the
|
||||
node is added to the DOM,
|
||||
- on the next animation frame: the css class `name-enter` will be removed and the
|
||||
class `name-enter-to` will be added (so they can be used to trigger css
|
||||
transition effects),
|
||||
- the css class `name-enter-active` will be removed whenever a css transition
|
||||
ends.
|
||||
|
||||
At node destruction:
|
||||
|
||||
- the css classes `name-leave` and `name-leave-active` will be added before the
|
||||
node is removed to the DOM,
|
||||
- the css class `name-leave` will be removed on the next animation frame (so it
|
||||
can be used to trigger css transition effects),
|
||||
- the css class `name-leave-active` will be removed whenever a css transition
|
||||
ends. Only then will the element be removed from the DOM.
|
||||
|
||||
For example, a simple fade in/out effect can be done with this:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<div t-if="state.flag" class="square" t-transition="fade">Hello</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
```css
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
```
|
||||
|
||||
Note: more information on animations are available [here](doc/animations.md).
|
||||
|
||||
### `t-mounted` directive
|
||||
|
||||
The `t-mounted` directive allows to register a callback to execute whenever the node
|
||||
is inserted into the DOM.
|
||||
|
||||
```xml
|
||||
<div><input t-ref="someInput" t-mounted="focusMe"/></div>
|
||||
```
|
||||
|
||||
```js
|
||||
class MyWidget extends owl.Component {
|
||||
...
|
||||
focusMe() {
|
||||
this.refs.someInput.focus();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Debugging (`t-debug` and `t-log`)
|
||||
### Debugging
|
||||
|
||||
The javascript QWeb implementation provides two useful debugging directives:
|
||||
|
||||
@@ -558,37 +504,3 @@ will stop execution if the browser dev tools are open.
|
||||
```
|
||||
|
||||
will print 42 to the console
|
||||
|
||||
### White spaces
|
||||
|
||||
White spaces in a templates are handled in a special way:
|
||||
|
||||
- consecutive whitespaces are always condensed to a single whitespace
|
||||
- if a whitespace-only text node contains a linebreak, it is ignored
|
||||
- the previous rules do not apply if we are in a `<pre>` tag
|
||||
|
||||
### Root nodes
|
||||
|
||||
For many reasons, Owl QWeb templates should have a single root node. More
|
||||
precisely, the result of a template rendering should have a single root node:
|
||||
|
||||
```xml
|
||||
<!–– not ok: two root nodes ––>
|
||||
<t>
|
||||
<div>foo</div>
|
||||
<div>bar</div>
|
||||
</t>
|
||||
|
||||
<!–– ok: result has one single root node ––>
|
||||
<t>
|
||||
<div t-if="someCondition">foo</div>
|
||||
<span t-else="1">bar</span>
|
||||
</t>
|
||||
```
|
||||
|
||||
Extra root nodes will actually be ignored (even though they will be rendered
|
||||
in memory).
|
||||
|
||||
Note: this does not apply to subtemplates (see the `t-call` directive). In that
|
||||
case, they will be inlined in the main template, and can actually have many
|
||||
root nodes.
|
||||
|
||||
+10
-4
@@ -3,14 +3,20 @@
|
||||
|
||||
## Reference
|
||||
|
||||
- [Animations](animations.md)
|
||||
- [Component](component.md)
|
||||
- [Event Bus](event_bus.md)
|
||||
- [Observer](observer.md)
|
||||
- [QWeb](qweb.md)
|
||||
- [Store](store.md)
|
||||
- [Observer](observer.md)
|
||||
- [Virtual DOM](vdom.md)
|
||||
- [Utils](utils.md)
|
||||
- [Virtual DOM](vdom.md)
|
||||
|
||||
## Learning resources
|
||||
|
||||
- [Quick Start](quick_start.md)
|
||||
|
||||
## Miscellaneous
|
||||
- [Quick Start](quick_start.md)
|
||||
- [Animations](animations.md)
|
||||
|
||||
- [Comparison with React/Vue](comparison.md)
|
||||
- [Tooling](tooling.md)
|
||||
|
||||
+58
-4
@@ -28,7 +28,7 @@ Note: Owl's store is inspired by React Redux and VueX.
|
||||
|
||||
## Example
|
||||
|
||||
Here is what a simple store look like:
|
||||
Here is what a simple store looks like:
|
||||
|
||||
```js
|
||||
const actions = {
|
||||
@@ -63,12 +63,12 @@ store.dispatch('addTodo', 'fix all bugs');
|
||||
|
||||
## Reference
|
||||
|
||||
The store is a simple `owl.EventBus` that triggers `update` events whenever its
|
||||
The store is a simple [`owl.EventBus`](event_bus.md) that triggers `update` events whenever its
|
||||
state is changed. Note that these events are triggered only after a microtask
|
||||
tick, so only one event will be triggered for any number of state changes in a
|
||||
call stack.
|
||||
|
||||
Also, it is important to mention that the state is observed (with a `owl.Observer`),
|
||||
Also, it is important to mention that the state is observed (with an `owl.Observer`),
|
||||
which is the reason why it is able to know if it was changed. This implies that
|
||||
state changes need to be done carefully in some cases (adding a new key to an
|
||||
object, or modifying an array with the `arr[i] = newValue` syntax). See the
|
||||
@@ -146,4 +146,58 @@ const post = store.getters.getPost(id);
|
||||
|
||||
### Connecting a component
|
||||
|
||||
Todo
|
||||
By default, an Owl `Component` is not connected to any store. The `connect`
|
||||
function is there to create sub Components that are connected versions of
|
||||
Components.
|
||||
|
||||
```javascript
|
||||
const actions = {
|
||||
increment({commit}) {
|
||||
commit('increment', 1);
|
||||
}
|
||||
};
|
||||
const mutations = {
|
||||
increment({state}, val) {
|
||||
state.counter += val;
|
||||
}
|
||||
};
|
||||
const state = {
|
||||
counter: 0,
|
||||
};
|
||||
const store = new owl.Store({state, actions, mutations});
|
||||
|
||||
class Counter extends owl.Component {
|
||||
increment() {
|
||||
this.env.store.dispatch('increment');
|
||||
}
|
||||
}
|
||||
function mapStoreToProps(state) {
|
||||
return {
|
||||
value: state.counter
|
||||
};
|
||||
}
|
||||
const ConnectedCounter = owl.connect(Counter, mapStoreToProps);
|
||||
|
||||
const counter = new ConnectedCounter({ store, qweb });
|
||||
```
|
||||
```xml
|
||||
<button t-name="Counter" t-on-click="increment">
|
||||
Click Me! [<t t-esc="props.value"/>]
|
||||
</button>
|
||||
```
|
||||
|
||||
The arguments of `connect` are:
|
||||
- `Counter`: an owl `Component` to connect
|
||||
- `mapStoreToProps`: a function that extracts the `props` of the Component
|
||||
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
|
||||
instance of `Store` to connect to (if not given, connects to `env.store`)
|
||||
- `hashFunction`: the function to use to detect changes in the state (if not
|
||||
given, generates a function that uses revision numbers, incremented at
|
||||
each state change)
|
||||
- `deep`: [only useful if no hashFunction is given] if false, only watch
|
||||
for top level state changes (true by default)
|
||||
|
||||
The `connect` function returns a sub class of the given `Component` which is
|
||||
connected to the `store`.
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# 🦉 Tooling 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Development Mode](#development-mode)
|
||||
- [Playground](#playground)
|
||||
- [Benchmarks](#benchmarks)
|
||||
|
||||
## Overview
|
||||
|
||||
To help work with/improve/learn OWL, there are a few extras tools/settings.
|
||||
|
||||
- development mode: enable better error reporting for the developer
|
||||
- a playground application: a space to experiment and learn Owl.
|
||||
- a benchmarks application: allow comparison with a few common frameworks
|
||||
|
||||
The two applications are available in the `tools/` folder, and can be accessed
|
||||
by using a static http server. A simple python
|
||||
server is available in `server.py`. There is also a npm script to start it:
|
||||
`npm run tools` (and its version with a watcher: `npm run tools:watch`).
|
||||
|
||||
## Development Mode
|
||||
|
||||
By default, Owl is in _production_ mode, this means that it will try to do its
|
||||
job fast, and skip some expensive operations. However, in some cases, it is
|
||||
convenient to have better information on what is going on, this is the purpose
|
||||
of the dev mode.
|
||||
|
||||
Owl has a mode flag, in `owl.__info__.mode`. Its default value is `prod`, but
|
||||
it can be set to `dev`:
|
||||
|
||||
```js
|
||||
owl.__info__.mode = "dev";
|
||||
```
|
||||
|
||||
Note that templates compiled with the `prod` settings will not be recompiled.
|
||||
So, changing this setting is best done at startup.
|
||||
|
||||
## Playground
|
||||
|
||||
The playground is an important application designed to help learning and
|
||||
experimenting with Owl. The last published version of Owl can be tested [online](https://odoo.github.io/owl/playground/).
|
||||
|
||||
It is an application similar to `jsFiddle`, but specialized for Owl: there are
|
||||
three tabs (`js`, `css` and `xml`), and a simple button `Run` to execute that
|
||||
code in an iframe.
|
||||
|
||||
## Benchmarks
|
||||
|
||||
Note: This is more an internal tool, useful for people working on Owl.
|
||||
|
||||
The benchmarks application is a very small application, implemented in different
|
||||
frameworks, and in different versions of Owl. This is a simple internal tool,
|
||||
useful to compare various performance metrics on some tasks.
|
||||
+40
-16
@@ -5,37 +5,61 @@ functions are all available in the `owl.utils` namespace.
|
||||
|
||||
## Content
|
||||
|
||||
- [`whenReady`](#whenready)
|
||||
- [`loadJS`](#loadjs)
|
||||
- [`loadTemplates`](#loadtemplates)
|
||||
- [`escape`](#escape)
|
||||
- [`debounce`](#debounce)
|
||||
- [`patch` and `unpatch`](#patchandunpatch)
|
||||
- [`whenReady`](#whenready): executing code when DOM is ready
|
||||
- [`loadJS`](#loadjs): loading script files
|
||||
- [`loadTemplates`](#loadtemplates): loading xml files
|
||||
- [`escape`](#escape): sanitizing strings
|
||||
- [`debounce`](#debounce): limiting rate of function calls
|
||||
|
||||
## `whenReady`
|
||||
|
||||
The function `whenReady` is useful to register some code that need to be executed
|
||||
as soon as the document (page) is ready:
|
||||
The function `whenReady` returns a `Promise` resolved when the DOM is ready (if
|
||||
not ready yet, resolved directly otherwise). If called with a callback as
|
||||
argument, it executes it as soon as the DOM ready (or directly).
|
||||
|
||||
```js
|
||||
owl.utils.whenReady(function () {
|
||||
Promise.all([loadTemplates(), owl.utils.whenReady()]).then(function ([templates]) {
|
||||
const qweb = new owl.QWeb(templates);
|
||||
const app = new App({ qweb });
|
||||
app.mount(document.body);
|
||||
});
|
||||
```
|
||||
|
||||
```js
|
||||
owl.utils.whenReady(function() {
|
||||
const qweb = new owl.QWeb();
|
||||
const app = new App({ qweb });
|
||||
app.mount(document.body);
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
## `loadJS`
|
||||
|
||||
`loadJS` takes a url (string) for a javascript resource, and loads it. It returns
|
||||
a promise, so the caller can properly react when it is ready. Also, it is smart:
|
||||
it maintains a list of urls previously loaded (or currently being loaded), and
|
||||
prevent doing twice the work.
|
||||
|
||||
```js
|
||||
class MyComponent extends owl.Component {
|
||||
willStart() {
|
||||
return owl.utils.loadJS('/static/libs/someLib.js');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## `loadTemplates`
|
||||
|
||||
```js
|
||||
async function makeEnv() {
|
||||
const templates = await owl.utils.loadTemplates('templates.xml');
|
||||
const qweb = new owl.QWeb(templates);
|
||||
return { qweb };
|
||||
}
|
||||
```
|
||||
|
||||
## `escape`
|
||||
|
||||
|
||||
## `debounce`
|
||||
|
||||
## `patch` and `unpatch`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+19
-1
@@ -1,3 +1,21 @@
|
||||
# 🦉 VDom 🦉
|
||||
|
||||
Owl's virtual dom is a fork of [snabbdom](https://github.com/snabbdom/snabbdom).
|
||||
Owl is a declarative component system: we declare the structure of the component
|
||||
tree, and Owl will translate that to a list of imperative operations. This
|
||||
translation is done by a virtual dom. This is the low level layer of Owl, most
|
||||
developer will not need to call directly the virtual dom functions.
|
||||
|
||||
|
||||
The main idea behind a virtual dom is to keep a in-memory representation of the
|
||||
DOM (called a virtual node), and whenever some change is needed, to regenerate
|
||||
a new representation, compute the difference between the old and the new, then
|
||||
apply the changes.
|
||||
|
||||
`vdom` exports two functions:
|
||||
|
||||
- `h`: create a new virtual node
|
||||
- `patch`: compare two virtual nodes, and apply the difference.
|
||||
|
||||
Note: Owl's virtual dom is a fork of [snabbdom](https://github.com/snabbdom/snabbdom).
|
||||
|
||||
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
<templates>
|
||||
<div class="main" t-name="root">
|
||||
<div class="left-thing">
|
||||
<div class="message_count">
|
||||
Number of msg: 0
|
||||
</div>
|
||||
<button class="o_btn_msg 100">Add 100 messages</button>
|
||||
<button class="o_btn_msg 1000">Add 1000 messages</button>
|
||||
<button class="o_btn_msg 10000">Add 10000 messages</button>
|
||||
<button class="o_btn_msg 50000">Add 50000 messages</button>
|
||||
<button class="updateSomeMessages">Update every 10th message</button>
|
||||
<button class="clear">Clear</button>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="message" class="message">
|
||||
<span class="author"><t t-esc="widget.author"/></span>
|
||||
<span class="msg"><t t-esc="widget.msg"/></span>
|
||||
<button class="remove" >Remove</button>
|
||||
</div>
|
||||
|
||||
<div t-name="counter">
|
||||
<button class="o_increment"></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -1,30 +0,0 @@
|
||||
<templates>
|
||||
<div class="main" t-name="root">
|
||||
<div class="left-thing">
|
||||
<div class="message_count">
|
||||
Number of msg: 0
|
||||
</div>
|
||||
<button class="o_btn_msg 100">Add 100 messages</button>
|
||||
<button class="o_btn_msg 1000">Add 1000 messages</button>
|
||||
<button class="o_btn_msg 10000">Add 10000 messages</button>
|
||||
<button class="o_btn_msg 50000">Add 50000 messages</button>
|
||||
<button class="updateSomeMessages">Update every 10th message</button>
|
||||
<button class="clear">Clear</button>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="message" class="message">
|
||||
<span class="author"><t t-esc="widget.author"/></span>
|
||||
<span class="msg"><t t-esc="widget.msg"/></span>
|
||||
<button class="remove" >Remove</button>
|
||||
</div>
|
||||
|
||||
<div t-name="counter">
|
||||
<button class="o_increment"></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -1,84 +0,0 @@
|
||||
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Likes Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Counter extends owl.Component {
|
||||
template = "counter";
|
||||
state = { counter: 0 };
|
||||
|
||||
increment() {
|
||||
this.state.counter++;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Message extends owl.Component {
|
||||
template = "message";
|
||||
widgets = { Counter };
|
||||
|
||||
shouldUpdate(nextProps) {
|
||||
return nextProps !== this.props;
|
||||
}
|
||||
removeMessage() {
|
||||
this.trigger("remove_message", {
|
||||
id: this.props.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class App extends owl.Component {
|
||||
template = "root";
|
||||
widgets = { Message };
|
||||
state = { messages: [] };
|
||||
|
||||
addMessages(n) {
|
||||
startMeasure("add " + n);
|
||||
const newMessages = buildData(n);
|
||||
this.state.messages.push.apply(this.state.messages, newMessages);
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
clear() {
|
||||
startMeasure("clear");
|
||||
this.state.messages = [];
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
updateSomeMessages() {
|
||||
startMeasure("update every 10th");
|
||||
const messages = this.state.messages;
|
||||
for (let i = 0; i < this.state.messages.length; i += 10) {
|
||||
const msg = Object.assign({}, messages[i]);
|
||||
msg.author += '!!!';
|
||||
this.set(messages, i, msg);
|
||||
}
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
removeMessage(data) {
|
||||
startMeasure("remove message");
|
||||
const index = this.state.messages.findIndex(m => m.id === data.id);
|
||||
this.state.messages.splice(index, 1);
|
||||
stopMeasure();
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Application initialization
|
||||
//------------------------------------------------------------------------------
|
||||
async function start() {
|
||||
const templates = await owl.utils.loadTemplates("templates.xml");
|
||||
const env = {
|
||||
qweb: new owl.QWeb(templates)
|
||||
};
|
||||
const app = new App(env);
|
||||
app.mount(document.body);
|
||||
}
|
||||
|
||||
start();
|
||||
@@ -1,32 +0,0 @@
|
||||
<templates>
|
||||
<div t-name="root" class="main">
|
||||
<div class="left-thing">
|
||||
<div>Number of msg: <t t-esc="state.messages.length"/></div>
|
||||
<button t-on-click="addMessages(100)">Add 100 messages</button>
|
||||
<button t-on-click="addMessages(1000)">Add 1000 messages</button>
|
||||
<button t-on-click="addMessages(10000)">Add 10000 messages</button>
|
||||
<button t-on-click="addMessages(50000)">Add 50000 messages</button>
|
||||
<button t-on-click="updateSomeMessages">Update every 10th message</button>
|
||||
<button t-on-click="clear">Clear</button>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content">
|
||||
<t t-foreach="state.messages" t-as="message">
|
||||
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="message" class="message">
|
||||
<span class="author"><t t-esc="props.author"/></span>
|
||||
<span class="msg"><t t-esc="props.msg"/></span>
|
||||
<button class="remove" t-on-click="removeMessage">Remove</button>
|
||||
<t t-widget="Counter"/>
|
||||
</div>
|
||||
|
||||
<div t-name="counter">
|
||||
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -1,85 +0,0 @@
|
||||
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Likes Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Counter extends owl.Component {
|
||||
template = "counter";
|
||||
state = { counter: 0 };
|
||||
|
||||
increment() {
|
||||
this.state.counter++;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Message extends owl.Component {
|
||||
template = "message";
|
||||
widgets = { Counter };
|
||||
|
||||
shouldUpdate(nextProps) {
|
||||
return nextProps !== this.props;
|
||||
}
|
||||
|
||||
removeMessage() {
|
||||
this.trigger("remove_message", {
|
||||
id: this.props.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class App extends owl.Component {
|
||||
template = "root";
|
||||
widgets = { Message };
|
||||
state = { messages: [] };
|
||||
|
||||
addMessages(n) {
|
||||
startMeasure("add " + n);
|
||||
const newMessages = buildData(n);
|
||||
this.state.messages.push.apply(this.state.messages, newMessages);
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
clear() {
|
||||
startMeasure("clear");
|
||||
this.state.messages = [];
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
updateSomeMessages() {
|
||||
startMeasure("update every 10th");
|
||||
const messages = this.state.messages;
|
||||
for (let i = 0; i < this.state.messages.length; i += 10) {
|
||||
const msg = Object.assign({}, messages[i]);
|
||||
msg.author += '!!!';
|
||||
this.set(messages, i, msg);
|
||||
}
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
removeMessage(data) {
|
||||
startMeasure('remove message');
|
||||
const index = this.state.messages.findIndex(m => m.id === data.id);
|
||||
this.state.messages.splice(index, 1);
|
||||
stopMeasure();
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Application initialization
|
||||
//------------------------------------------------------------------------------
|
||||
async function start() {
|
||||
const templates = await owl.utils.loadTemplates("templates.xml");
|
||||
const env = {
|
||||
qweb: new owl.QWeb(templates)
|
||||
};
|
||||
const app = new App(env);
|
||||
app.mount(document.body);
|
||||
}
|
||||
|
||||
start();
|
||||
@@ -1,32 +0,0 @@
|
||||
<templates>
|
||||
<div t-name="root" class="main">
|
||||
<div class="left-thing">
|
||||
<div>Number of msg: <t t-esc="state.messages.length"/></div>
|
||||
<button t-on-click="addMessages(100)">Add 100 messages</button>
|
||||
<button t-on-click="addMessages(1000)">Add 1000 messages</button>
|
||||
<button t-on-click="addMessages(10000)">Add 10000 messages</button>
|
||||
<button t-on-click="addMessages(50000)">Add 50000 messages</button>
|
||||
<button t-on-click="updateSomeMessages">Update every 10th message</button>
|
||||
<button t-on-click="clear">Clear</button>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content">
|
||||
<t t-foreach="state.messages" t-as="message">
|
||||
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="message" class="message">
|
||||
<span class="author"><t t-esc="props.author"/></span>
|
||||
<span class="msg"><t t-esc="props.msg"/></span>
|
||||
<button class="remove" t-on-click="removeMessage">Remove</button>
|
||||
<t t-widget="Counter"/>
|
||||
</div>
|
||||
|
||||
<div t-name="counter">
|
||||
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -1,84 +0,0 @@
|
||||
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Likes Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Counter extends owl.Component {
|
||||
template = "counter";
|
||||
state = { counter: 0 };
|
||||
|
||||
increment() {
|
||||
this.state.counter++;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Message extends owl.Component {
|
||||
template = "message";
|
||||
widgets = { Counter };
|
||||
|
||||
shouldUpdate(nextProps) {
|
||||
return nextProps !== this.props;
|
||||
}
|
||||
removeMessage() {
|
||||
this.trigger("remove_message", {
|
||||
id: this.props.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class App extends owl.Component {
|
||||
template = "root";
|
||||
widgets = { Message };
|
||||
state = { messages: [] };
|
||||
|
||||
addMessages(n) {
|
||||
startMeasure("add " + n);
|
||||
const newMessages = buildData(n);
|
||||
this.state.messages.push.apply(this.state.messages, newMessages);
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
clear() {
|
||||
startMeasure("clear");
|
||||
this.state.messages = [];
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
updateSomeMessages() {
|
||||
startMeasure("update every 10th");
|
||||
const messages = this.state.messages;
|
||||
for (let i = 0; i < this.state.messages.length; i += 10) {
|
||||
const msg = Object.assign({}, messages[i]);
|
||||
msg.author += '!!!';
|
||||
this.set(messages, i, msg);
|
||||
}
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
removeMessage(data) {
|
||||
startMeasure("remove message");
|
||||
const index = this.state.messages.findIndex(m => m.id === data.id);
|
||||
this.state.messages.splice(index, 1);
|
||||
stopMeasure();
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Application initialization
|
||||
//------------------------------------------------------------------------------
|
||||
async function start() {
|
||||
const templates = await owl.utils.loadTemplates("templates.xml");
|
||||
const env = {
|
||||
qweb: new owl.QWeb(templates)
|
||||
};
|
||||
const app = new App(env);
|
||||
app.mount(document.body);
|
||||
}
|
||||
|
||||
start();
|
||||
@@ -1,32 +0,0 @@
|
||||
<templates>
|
||||
<div t-name="root" class="main">
|
||||
<div class="left-thing">
|
||||
<div>Number of msg: <t t-esc="state.messages.length"/></div>
|
||||
<button t-on-click="addMessages(100)">Add 100 messages</button>
|
||||
<button t-on-click="addMessages(1000)">Add 1000 messages</button>
|
||||
<button t-on-click="addMessages(10000)">Add 10000 messages</button>
|
||||
<button t-on-click="addMessages(50000)">Add 50000 messages</button>
|
||||
<button t-on-click="updateSomeMessages">Update every 10th message</button>
|
||||
<button t-on-click="clear">Clear</button>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content">
|
||||
<t t-foreach="state.messages" t-as="message">
|
||||
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="message" class="message">
|
||||
<span class="author"><t t-esc="props.author"/></span>
|
||||
<span class="msg"><t t-esc="props.msg"/></span>
|
||||
<button class="remove" t-on-click="removeMessage">Remove</button>
|
||||
<t t-widget="Counter"/>
|
||||
</div>
|
||||
|
||||
<div t-name="counter">
|
||||
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -1,84 +0,0 @@
|
||||
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Likes Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Counter extends owl.Component {
|
||||
template = "counter";
|
||||
state = { counter: 0 };
|
||||
|
||||
increment() {
|
||||
this.state.counter++;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Message extends owl.Component {
|
||||
template = "message";
|
||||
widgets = { Counter };
|
||||
|
||||
shouldUpdate(nextProps) {
|
||||
return nextProps !== this.props;
|
||||
}
|
||||
removeMessage() {
|
||||
this.trigger("remove_message", {
|
||||
id: this.props.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class App extends owl.Component {
|
||||
template = "root";
|
||||
widgets = { Message };
|
||||
state = { messages: [] };
|
||||
|
||||
addMessages(n) {
|
||||
startMeasure("add " + n);
|
||||
const newMessages = buildData(n);
|
||||
this.state.messages.push.apply(this.state.messages, newMessages);
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
clear() {
|
||||
startMeasure("clear");
|
||||
this.state.messages = [];
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
updateSomeMessages() {
|
||||
startMeasure("update every 10th");
|
||||
const messages = this.state.messages;
|
||||
for (let i = 0; i < this.state.messages.length; i += 10) {
|
||||
const msg = Object.assign({}, messages[i]);
|
||||
msg.author += '!!!';
|
||||
this.set(messages, i, msg);
|
||||
}
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
removeMessage(data) {
|
||||
startMeasure("remove message");
|
||||
const index = this.state.messages.findIndex(m => m.id === data.id);
|
||||
this.state.messages.splice(index, 1);
|
||||
stopMeasure();
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Application initialization
|
||||
//------------------------------------------------------------------------------
|
||||
async function start() {
|
||||
const templates = await owl.utils.loadTemplates("templates.xml");
|
||||
const env = {
|
||||
qweb: new owl.QWeb(templates)
|
||||
};
|
||||
const app = new App(env);
|
||||
app.mount(document.body);
|
||||
}
|
||||
|
||||
start();
|
||||
@@ -1,32 +0,0 @@
|
||||
<templates>
|
||||
<div t-name="root" class="main">
|
||||
<div class="left-thing">
|
||||
<div>Number of msg: <t t-esc="state.messages.length"/></div>
|
||||
<button t-on-click="addMessages(100)">Add 100 messages</button>
|
||||
<button t-on-click="addMessages(1000)">Add 1000 messages</button>
|
||||
<button t-on-click="addMessages(10000)">Add 10000 messages</button>
|
||||
<button t-on-click="addMessages(50000)">Add 50000 messages</button>
|
||||
<button t-on-click="updateSomeMessages">Update every 10th message</button>
|
||||
<button t-on-click="clear">Clear</button>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content">
|
||||
<t t-foreach="state.messages" t-as="message">
|
||||
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="message" class="message">
|
||||
<span class="author"><t t-esc="props.author"/></span>
|
||||
<span class="msg"><t t-esc="props.msg"/></span>
|
||||
<button class="remove" t-on-click="removeMessage">Remove</button>
|
||||
<t t-widget="Counter"/>
|
||||
</div>
|
||||
|
||||
<div t-name="counter">
|
||||
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -1,81 +0,0 @@
|
||||
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Likes Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Counter extends owl.Component {
|
||||
state = { counter: 0 };
|
||||
|
||||
increment() {
|
||||
this.state.counter++;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Message extends owl.Component {
|
||||
widgets = { Counter };
|
||||
|
||||
shouldUpdate(nextProps) {
|
||||
return nextProps !== this.props;
|
||||
}
|
||||
removeMessage() {
|
||||
this.trigger("remove_message", {
|
||||
id: this.props.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class App extends owl.Component {
|
||||
widgets = { Message };
|
||||
state = { messages: [] };
|
||||
|
||||
addMessages(n) {
|
||||
startMeasure("add " + n);
|
||||
const newMessages = buildData(n);
|
||||
this.state.messages.push.apply(this.state.messages, newMessages);
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
clear() {
|
||||
startMeasure("clear");
|
||||
this.state.messages = [];
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
updateSomeMessages() {
|
||||
startMeasure("update every 10th");
|
||||
const messages = this.state.messages;
|
||||
for (let i = 0; i < this.state.messages.length; i += 10) {
|
||||
const msg = Object.assign({}, messages[i]);
|
||||
msg.author += '!!!';
|
||||
this.set(messages, i, msg);
|
||||
}
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
removeMessage(data) {
|
||||
startMeasure("remove message");
|
||||
const index = this.state.messages.findIndex(m => m.id === data.id);
|
||||
this.state.messages.splice(index, 1);
|
||||
stopMeasure();
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Application initialization
|
||||
//------------------------------------------------------------------------------
|
||||
async function start() {
|
||||
const templates = await owl.utils.loadTemplates("templates.xml");
|
||||
const env = {
|
||||
qweb: new owl.QWeb(templates)
|
||||
};
|
||||
const app = new App(env);
|
||||
app.mount(document.body);
|
||||
}
|
||||
|
||||
start();
|
||||
@@ -1,32 +0,0 @@
|
||||
<templates>
|
||||
<div t-name="App" class="main">
|
||||
<div class="left-thing">
|
||||
<div>Number of msg: <t t-esc="state.messages.length"/></div>
|
||||
<button t-on-click="addMessages(100)">Add 100 messages</button>
|
||||
<button t-on-click="addMessages(1000)">Add 1000 messages</button>
|
||||
<button t-on-click="addMessages(10000)">Add 10000 messages</button>
|
||||
<button t-on-click="addMessages(50000)">Add 50000 messages</button>
|
||||
<button t-on-click="updateSomeMessages">Update every 10th message</button>
|
||||
<button t-on-click="clear">Clear</button>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content">
|
||||
<t t-foreach="state.messages" t-as="message">
|
||||
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="Message" class="message">
|
||||
<span class="author"><t t-esc="props.author"/></span>
|
||||
<span class="msg"><t t-esc="props.msg"/></span>
|
||||
<button class="remove" t-on-click="removeMessage">Remove</button>
|
||||
<t t-widget="Counter"/>
|
||||
</div>
|
||||
|
||||
<div t-name="Counter">
|
||||
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -1,221 +0,0 @@
|
||||
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Counter extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
counter: 0
|
||||
};
|
||||
this.increment = this.increment.bind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
return React.createElement(
|
||||
"div",
|
||||
null,
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: this.increment
|
||||
},
|
||||
"Value: ",
|
||||
this.state.counter
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
increment() {
|
||||
this.setState({
|
||||
counter: this.state.counter + 1
|
||||
});
|
||||
}
|
||||
}
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Message extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.removeMessage = this.removeMessage.bind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
return React.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "message"
|
||||
},
|
||||
React.createElement(
|
||||
"span",
|
||||
{
|
||||
className: "author"
|
||||
},
|
||||
this.props.message.author
|
||||
),
|
||||
React.createElement(
|
||||
"span",
|
||||
{
|
||||
className: "msg"
|
||||
},
|
||||
this.props.message.msg
|
||||
),
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
className: "remove",
|
||||
onClick: this.removeMessage
|
||||
},
|
||||
"Remove"
|
||||
),
|
||||
React.createElement(Counter, null)
|
||||
);
|
||||
}
|
||||
|
||||
removeMessage() {
|
||||
this.props.removeCB(this.props.message.id);
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Main extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
messages: []
|
||||
};
|
||||
this.removeMessage = this.removeMessage.bind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
const msgList = this.state.messages.map(m =>
|
||||
React.createElement(Message, {
|
||||
key: m.id,
|
||||
message: m,
|
||||
removeCB: this.removeMessage
|
||||
})
|
||||
);
|
||||
return React.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "main"
|
||||
},
|
||||
React.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "left-thing"
|
||||
},
|
||||
React.createElement(
|
||||
"div",
|
||||
null,
|
||||
"Number of msg: ",
|
||||
this.state.messages.length
|
||||
),
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: _ => this.addMessages(100)
|
||||
},
|
||||
"Add 100 messages"
|
||||
),
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: _ => this.addMessages(1000)
|
||||
},
|
||||
"Add 1000 messages"
|
||||
),
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: _ => this.addMessages(10000)
|
||||
},
|
||||
"Add 10000 messages"
|
||||
),
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: _ => this.addMessages(50000)
|
||||
},
|
||||
"Add 50000 messages"
|
||||
),
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: _ => this.updateSomeMessages()
|
||||
},
|
||||
"Update every 10th messags"
|
||||
),
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: _ => this.clear()
|
||||
},
|
||||
"Clear"
|
||||
)
|
||||
),
|
||||
React.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "right-thing"
|
||||
},
|
||||
React.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "content"
|
||||
},
|
||||
msgList
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
addMessages(n) {
|
||||
startMeasure("add " + n);
|
||||
const newMessages = this.state.messages.concat(buildData(n));
|
||||
this.setState({
|
||||
messages: newMessages
|
||||
});
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
clear() {
|
||||
startMeasure("clear");
|
||||
this.setState({
|
||||
messages: []
|
||||
});
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
updateSomeMessages() {
|
||||
startMeasure("update every 10th");
|
||||
const messages = this.state.messages;
|
||||
for (let i = 0; i < messages.length; i += 10) {
|
||||
messages[i].author += "!!!";
|
||||
}
|
||||
this.forceUpdate();
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
removeMessage(id) {
|
||||
startMeasure("remove message");
|
||||
const index = this.state.messages.findIndex(m => m.id === id);
|
||||
const messages = this.state.messages.slice();
|
||||
messages.splice(index, 1);
|
||||
this.setState({
|
||||
messages
|
||||
});
|
||||
stopMeasure();
|
||||
}
|
||||
} //-----------------------------
|
||||
// INIT
|
||||
//-----------------------------
|
||||
|
||||
ReactDOM.render(
|
||||
React.createElement(Main, null),
|
||||
document.getElementById("main")
|
||||
);
|
||||
@@ -1,42 +0,0 @@
|
||||
.main {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 220px 1fr;
|
||||
}
|
||||
|
||||
.left-thing {
|
||||
background-color: gray;
|
||||
padding: 20px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.left-thing button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.right-thing {
|
||||
padding: 20px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Message widget */
|
||||
.message .author {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.message {
|
||||
width: 400px;
|
||||
background-color: lightblue;
|
||||
margin: 10px 5px;
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.remove {
|
||||
float: right;
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
Vue.component("likes-counter", {
|
||||
data: function() {
|
||||
return {
|
||||
counter: 0
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
increment() {
|
||||
this.counter++;
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<button @click="increment">Value: {{counter}}</button>
|
||||
</div>`
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
Vue.component("my-message", {
|
||||
props: ["msg"],
|
||||
methods: {
|
||||
removeMessage() {
|
||||
this.$emit("removeMessage", this.msg.id);
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="message">
|
||||
<span class="author">{{msg.author}}</span>
|
||||
<span class="msg">{{msg.msg}}</span>
|
||||
<button class="remove" @click="removeMessage">Remove</button>
|
||||
<likes-counter></likes-counter>
|
||||
</div>`
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
const App = {
|
||||
name: "App",
|
||||
data() {
|
||||
return { messages: [] };
|
||||
},
|
||||
methods: {
|
||||
addMessages(n) {
|
||||
startMeasure("add " + n);
|
||||
const newMessages = buildData(n);
|
||||
this.messages.push.apply(this.messages, newMessages);
|
||||
stopMeasure();
|
||||
},
|
||||
|
||||
clear() {
|
||||
startMeasure("clear");
|
||||
this.messages = [];
|
||||
stopMeasure();
|
||||
},
|
||||
|
||||
updateSomeMessages() {
|
||||
startMeasure("update every 10th");
|
||||
const messages = this.messages;
|
||||
for (let i = 0; i < this.messages.length; i += 10) {
|
||||
messages[i].author += "!!!";
|
||||
}
|
||||
stopMeasure();
|
||||
},
|
||||
|
||||
removeMessage(id) {
|
||||
startMeasure("remove message");
|
||||
const index = this.messages.findIndex(m => m.id === id);
|
||||
this.messages.splice(index, 1);
|
||||
stopMeasure();
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="main">
|
||||
<div class="left-thing">
|
||||
<div>Number of msg: {{messages.length}}</div>
|
||||
<button @click="addMessages(100)">Add 100 messages</button>
|
||||
<button @click="addMessages(1000)">Add 1000 messages</button>
|
||||
<button @click="addMessages(10000)">Add 10000 messages</button>
|
||||
<button @click="addMessages(50000)">Add 50000 messages</button>
|
||||
<button @click="updateSomeMessages">Update every 10th message</button>
|
||||
<button @click="clear">Clear</button>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content">
|
||||
<my-message v-for="msg in messages" v-bind:key="msg.id" v-bind:msg="msg" @removeMessage="removeMessage"></my-message>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Application initialization
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
new Vue({
|
||||
render: h => h(App)
|
||||
}).$mount(`#main`);
|
||||
@@ -1,29 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>OWL Extras</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🦉 OWL Extra Stuff 🦉</h1>
|
||||
|
||||
|
||||
<h2>Benchmarks</h2>
|
||||
<p>The goal is to compare some operations on large lists</p>
|
||||
|
||||
<ul>
|
||||
<li><a href="benchmarks/odoo-widgets-12.0">Odoo Widgets (12.0)</a></li>
|
||||
<li><a href="benchmarks/odoo-widgets-12.3">Odoo Widgets (12.3)</a></li>
|
||||
<li><a href="benchmarks/owl-0.7.0">OWL 0.7.0</a></li>
|
||||
<li><a href="benchmarks/owl-0.8.0">OWL 0.8.0</a></li>
|
||||
<li><a href="benchmarks/owl-0.9.0">OWL 0.9.0</a></li>
|
||||
<li><a href="benchmarks/owl-0.10.0">OWL 0.10.0</a></li>
|
||||
<li><a href="benchmarks/owl-master">OWL Master</a></li>
|
||||
<li><a href="benchmarks/vue">Vue</a></li>
|
||||
<li><a href="benchmarks/react">React</a></li>
|
||||
</ul>
|
||||
|
||||
<h2>Playground</h2>
|
||||
<a href="playground">Playground</a>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,21 +0,0 @@
|
||||
# 🦉 Extra Stuff 🦉
|
||||
|
||||
To help work/improve/learn with OWL, we have here:
|
||||
|
||||
- a benchmarks application
|
||||
- a playground application
|
||||
|
||||
Both of them can be accessed by using a static http server. A simple python
|
||||
server is available in `server.py`. There is also a npm script to start it:
|
||||
`npm run extras` (and its version with a watcher: `npm run extras:watch`).
|
||||
|
||||
## Benchmarks
|
||||
|
||||
The benchmarks application is a very small application, implemented in different
|
||||
frameworks, and in different versions of Owl. This is a simple internal tool,
|
||||
useful to compare various performance metrics on some tasks.
|
||||
|
||||
## Playground
|
||||
|
||||
The playground is an important application designed to help learning and
|
||||
experimenting with Owl. It is available [online](https://odoo.github.io/owl/playground/).
|
||||
@@ -1,38 +0,0 @@
|
||||
import sys
|
||||
import thread
|
||||
import webbrowser
|
||||
import time
|
||||
|
||||
import BaseHTTPServer
|
||||
import SimpleHTTPServer
|
||||
|
||||
HOST = '127.0.0.1'
|
||||
PORT = 8000
|
||||
URL = 'http://{0}:{1}/extras'.format(HOST, PORT)
|
||||
|
||||
|
||||
# We define our own handler here to remap owl.js GET requests to the Owl build
|
||||
# in dist/. This is useful for the benchmarks and playground applications.
|
||||
# With this, we can simply copy the playground folder as is in the gh-page when
|
||||
# we want to update the playground.
|
||||
class OWLHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
if self.path == '/extras/owl.js':
|
||||
self.path = '/dist/owl.js'
|
||||
return SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
|
||||
|
||||
|
||||
def start_server():
|
||||
httpd = BaseHTTPServer.HTTPServer((HOST, PORT), OWLHandler)
|
||||
httpd.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
thread.start_new_thread(start_server, ())
|
||||
webbrowser.open_new(URL)
|
||||
|
||||
while True:
|
||||
try:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(0)
|
||||
+9
-11
@@ -1,18 +1,19 @@
|
||||
{
|
||||
"name": "owl",
|
||||
"version": "0.11.0",
|
||||
"version": "0.14.0",
|
||||
"description": "Odoo Web Library (OWL)",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"build:js": "tsc --target esnext --module es6 --outDir dist/owl src/*",
|
||||
"build:js": "tsc --target esnext --module es6 --outDir dist/owl",
|
||||
"build:bundle": "rollup -c",
|
||||
"build": "npm run build:js && npm run build:bundle",
|
||||
"minify": "uglifyjs dist/owl.js -o dist/owl.min.js --compress --mangle",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"extras:serve": "python extras/server.py",
|
||||
"extras": "npm run build && npm run extras:serve",
|
||||
"extras:watch": "npm-run-all --parallel extras:serve \"build:* -- --watch\""
|
||||
"tools:serve": "python3 tools/server.py || python tools/server.py",
|
||||
"tools": "npm run build && npm run tools:serve",
|
||||
"pretools:watch": "npm run build",
|
||||
"tools:watch": "npm-run-all --parallel tools:serve \"build:* -- --watch\""
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -34,16 +35,13 @@
|
||||
"rollup": "^1.6.0",
|
||||
"rollup-plugin-typescript2": "^0.20.1",
|
||||
"sass": "^1.16.1",
|
||||
"snabbdom-to-html": "^5.1.1",
|
||||
"source-map-support": "^0.5.10",
|
||||
"ts-jest": "^23.10.5",
|
||||
"typescript": "^3.2.2",
|
||||
"uglify-es": "^3.3.9"
|
||||
},
|
||||
"dependencies": {
|
||||
"jest-environment-jsdom": "^24.7.1",
|
||||
"snabbdom": "^0.7.3"
|
||||
"uglify-es": "^3.3.9",
|
||||
"jest-environment-jsdom": "^24.7.1"
|
||||
},
|
||||
"dependencies": {},
|
||||
"jest": {
|
||||
"roots": [
|
||||
"<rootDir>/src",
|
||||
|
||||
+1
-1
@@ -9,6 +9,6 @@ export default {
|
||||
format: "iife",
|
||||
name: "owl",
|
||||
extend: true,
|
||||
outro: `exports._version = '${version}';\nexports._date = '${new Date().toISOString()}';\nexports._hash = '${git.short()}';\nexports._url = 'https://github.com/odoo/owl';`
|
||||
outro: `exports.__info__.version = '${version}';\nexports.__info__.date = '${new Date().toISOString()}';\nexports.__info__.hash = '${git.short()}';\nexports.__info__.url = 'https://github.com/odoo/owl';`
|
||||
}
|
||||
};
|
||||
|
||||
+279
-105
@@ -1,4 +1,3 @@
|
||||
import { EventBus } from "./event_bus";
|
||||
import { Observer } from "./observer";
|
||||
import { QWeb, CompiledTemplate } from "./qweb_core";
|
||||
import { h, patch, VNode } from "./vdom";
|
||||
@@ -18,10 +17,24 @@ import { h, patch, VNode } from "./vdom";
|
||||
// Types/helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* An Env (environment) is an object that will be (mostly) shared between all
|
||||
* components of an Owl application. It is the location which should contain
|
||||
* the qweb instance necessary to render all components.
|
||||
*
|
||||
* Note that it is totally fine to extend the environment with application
|
||||
* specific keys/objects/whatever. For example, a key `isMobile` (to declare
|
||||
* if we are in "mobile" mode), or a shared bus could be useful.
|
||||
*/
|
||||
export interface Env {
|
||||
qweb: QWeb;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is mostly an internal detail of implementation. The Meta interface is
|
||||
* useful to typecheck and describe the internal keys used by Owl to manage the
|
||||
* component tree.
|
||||
*/
|
||||
export interface Meta<T extends Env, Props> {
|
||||
readonly id: number;
|
||||
vnode: VNode | null;
|
||||
@@ -53,14 +66,14 @@ const TEMPLATE_MAP: { [key: number]: { [name: string]: string } } = {};
|
||||
//------------------------------------------------------------------------------
|
||||
let nextId = 1;
|
||||
|
||||
export class Component<
|
||||
T extends Env,
|
||||
Props extends {},
|
||||
State extends {}
|
||||
> extends EventBus {
|
||||
export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
readonly __owl__: Meta<Env, Props>;
|
||||
template?: string;
|
||||
|
||||
/**
|
||||
* The `el` is the root element of the widget. Note that it could be null:
|
||||
* this is the case if the widget is not mounted yet, or is destroyed.
|
||||
*/
|
||||
get el(): HTMLElement | null {
|
||||
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
|
||||
}
|
||||
@@ -68,6 +81,11 @@ export class Component<
|
||||
env: T;
|
||||
state?: State;
|
||||
props: Props;
|
||||
|
||||
// type of props is not easily representable in typescript...
|
||||
static props?: any;
|
||||
static defaultProps?: any;
|
||||
|
||||
refs: {
|
||||
[key: string]: Component<T, any, any> | HTMLElement | undefined;
|
||||
} = {};
|
||||
@@ -96,8 +114,13 @@ export class Component<
|
||||
* the t-widget directive in a template)
|
||||
*/
|
||||
constructor(parent: Component<T, any, any> | T, props?: Props) {
|
||||
super();
|
||||
|
||||
const defaultProps = (<any>this.constructor).defaultProps;
|
||||
if (defaultProps) {
|
||||
props = this._applyDefaultProps(props, defaultProps);
|
||||
}
|
||||
if (QWeb.dev) {
|
||||
this._validateProps(props || {});
|
||||
}
|
||||
// is this a good idea?
|
||||
// Pro: if props is empty, we can create easily a widget
|
||||
// Con: this is not really safe
|
||||
@@ -208,10 +231,16 @@ export class Component<
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Mount the component to a target element.
|
||||
*
|
||||
* This should only be done if the component was created manually. Components
|
||||
* created declaratively in templates are managed by the Owl system.
|
||||
*/
|
||||
async mount(target: HTMLElement): Promise<void> {
|
||||
const vnode = await this._prepare();
|
||||
if (this.__owl__.isDestroyed) {
|
||||
// widget was destroyed before we get here...
|
||||
// component was destroyed before we get here...
|
||||
return;
|
||||
}
|
||||
this._patch(vnode);
|
||||
@@ -222,33 +251,6 @@ export class Component<
|
||||
}
|
||||
}
|
||||
|
||||
_callMounted() {
|
||||
const children = this.__owl__.children;
|
||||
for (let id in children) {
|
||||
const comp = children[id];
|
||||
if (!comp.__owl__.isMounted && this.el!.contains(comp.el)) {
|
||||
comp._callMounted();
|
||||
}
|
||||
}
|
||||
this.__owl__.isMounted = true;
|
||||
for (let key in this.__owl__.mountedHandlers) {
|
||||
this.__owl__.mountedHandlers[key]();
|
||||
}
|
||||
this.mounted();
|
||||
}
|
||||
|
||||
_callWillUnmount() {
|
||||
this.willUnmount();
|
||||
this.__owl__.isMounted = false;
|
||||
const children = this.__owl__.children;
|
||||
for (let id in children) {
|
||||
const comp = children[id];
|
||||
if (comp.__owl__.isMounted) {
|
||||
comp._callWillUnmount();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unmount() {
|
||||
if (this.__owl__.isMounted) {
|
||||
this._callWillUnmount();
|
||||
@@ -257,7 +259,8 @@ export class Component<
|
||||
}
|
||||
|
||||
async render(force: boolean = false, patchQueue?: any[]): Promise<void> {
|
||||
if (!this.__owl__.isMounted) {
|
||||
const __owl__ = this.__owl__;
|
||||
if (!__owl__.isMounted) {
|
||||
return;
|
||||
}
|
||||
const shouldPatch: boolean = !patchQueue;
|
||||
@@ -265,62 +268,54 @@ export class Component<
|
||||
patchQueue = [];
|
||||
}
|
||||
const renderVDom = this._render(force, patchQueue);
|
||||
const renderId = this.__owl__.renderId;
|
||||
const renderId = __owl__.renderId;
|
||||
await renderVDom;
|
||||
|
||||
if (
|
||||
shouldPatch &&
|
||||
this.__owl__.isMounted &&
|
||||
renderId === this.__owl__.renderId
|
||||
) {
|
||||
if (shouldPatch && __owl__.isMounted && renderId === __owl__.renderId) {
|
||||
// we only update the vnode and the actual DOM if no other rendering
|
||||
// occurred between now and when the render method was initially called.
|
||||
for (let i = 0; i < patchQueue!.length; i++) {
|
||||
const patchLen = patchQueue!.length;
|
||||
for (let i = 0; i < patchLen; i++) {
|
||||
const patch = patchQueue![i];
|
||||
patch.push(patch[0].willPatch());
|
||||
}
|
||||
for (let i = 0; i < patchQueue!.length; i++) {
|
||||
for (let i = 0; i < patchLen; i++) {
|
||||
const patch = patchQueue![i];
|
||||
patch[0]._patch(patch[1]);
|
||||
}
|
||||
|
||||
for (let i = patchQueue!.length - 1; i >= 0; i--) {
|
||||
for (let i = patchLen - 1; i >= 0; i--) {
|
||||
const patch = patchQueue![i];
|
||||
patch[0].patched(patch[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the component. This operation is quite complex:
|
||||
* - it recursively destroy all children
|
||||
* - call the willUnmount hooks if necessary
|
||||
* - remove the dom node from the dom
|
||||
*
|
||||
* This should only be called manually if you created the widget. Most widgets
|
||||
* will be automatically destroyed.
|
||||
*/
|
||||
destroy() {
|
||||
if (!this.__owl__.isDestroyed) {
|
||||
const __owl__ = this.__owl__;
|
||||
if (!__owl__.isDestroyed) {
|
||||
const el = this.el;
|
||||
this._destroy(this.__owl__.parent);
|
||||
this._destroy(__owl__.parent);
|
||||
if (el) {
|
||||
el.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_destroy(parent) {
|
||||
const isMounted = this.__owl__.isMounted;
|
||||
if (isMounted) {
|
||||
this.willUnmount();
|
||||
this.__owl__.isMounted = false;
|
||||
}
|
||||
const children = Object.values(this.__owl__.children);
|
||||
for (let child of children) {
|
||||
child._destroy(this);
|
||||
}
|
||||
if (parent) {
|
||||
let id = this.__owl__.id;
|
||||
delete parent.__owl__.children[id];
|
||||
this.__owl__.parent = null;
|
||||
}
|
||||
this.clear();
|
||||
this.__owl__.isDestroyed = true;
|
||||
delete this.__owl__.vnode;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by the component system whenever its props are
|
||||
* updated. If it returns true, then the component will be rendered.
|
||||
* Otherwise, it will skip the rendering (also, its props will not be updated)
|
||||
*/
|
||||
shouldUpdate(nextProps: Props): boolean {
|
||||
return true;
|
||||
}
|
||||
@@ -335,23 +330,87 @@ export class Component<
|
||||
* mode or not.
|
||||
*/
|
||||
async updateEnv(nextEnv: Partial<T>): Promise<void> {
|
||||
if (this.__owl__.parent && this.__owl__.parent.env === this.env) {
|
||||
const __owl__ = this.__owl__;
|
||||
if (__owl__.parent && __owl__.parent.env === this.env) {
|
||||
this.env = Object.create(this.env);
|
||||
}
|
||||
Object.assign(this.env, nextEnv);
|
||||
if (this.__owl__.isMounted) {
|
||||
if (__owl__.isMounted) {
|
||||
await this.render(true);
|
||||
}
|
||||
}
|
||||
|
||||
set(target: any, key: string | number, value: any) {
|
||||
this.__owl__.observer!.set(target, key, value);
|
||||
/**
|
||||
* Emit a custom event of type 'eventType' with the given 'payload' on the
|
||||
* component's el, if it exists. However, note that the event will only bubble
|
||||
* up to the parent DOM nodes. Thus, it must be called between mounted() and
|
||||
* willUnmount().
|
||||
*/
|
||||
trigger(eventType: string, payload?: any) {
|
||||
if (this.el) {
|
||||
const ev = new CustomEvent(eventType, {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
detail: payload
|
||||
});
|
||||
this.el.dispatchEvent(ev);
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
_destroy(parent: Component<any, any, any> | null) {
|
||||
const __owl__ = this.__owl__;
|
||||
const isMounted = __owl__.isMounted;
|
||||
if (isMounted) {
|
||||
this.willUnmount();
|
||||
__owl__.isMounted = false;
|
||||
}
|
||||
const children = __owl__.children;
|
||||
for (let key in children) {
|
||||
children[key]._destroy(this);
|
||||
}
|
||||
if (parent) {
|
||||
let id = __owl__.id;
|
||||
delete parent.__owl__.children[id];
|
||||
__owl__.parent = null;
|
||||
}
|
||||
__owl__.isDestroyed = true;
|
||||
delete __owl__.vnode;
|
||||
}
|
||||
|
||||
_callMounted() {
|
||||
const __owl__ = this.__owl__;
|
||||
const children = __owl__.children;
|
||||
for (let id in children) {
|
||||
const comp = children[id];
|
||||
if (!comp.__owl__.isMounted && this.el!.contains(comp.el)) {
|
||||
comp._callMounted();
|
||||
}
|
||||
}
|
||||
__owl__.isMounted = true;
|
||||
const handlers = __owl__.mountedHandlers;
|
||||
for (let key in handlers) {
|
||||
handlers[key]();
|
||||
}
|
||||
this.mounted();
|
||||
}
|
||||
|
||||
_callWillUnmount() {
|
||||
this.willUnmount();
|
||||
const __owl__ = this.__owl__;
|
||||
__owl__.isMounted = false;
|
||||
const children = __owl__.children;
|
||||
for (let id in children) {
|
||||
const comp = children[id];
|
||||
if (comp.__owl__.isMounted) {
|
||||
comp._callWillUnmount();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _updateProps(
|
||||
nextProps: Props,
|
||||
forceUpdate: boolean = false,
|
||||
@@ -359,6 +418,13 @@ export class Component<
|
||||
): Promise<void> {
|
||||
const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
|
||||
if (shouldUpdate) {
|
||||
const defaultProps = (<any>this.constructor).defaultProps;
|
||||
if (defaultProps) {
|
||||
nextProps = this._applyDefaultProps(nextProps, defaultProps);
|
||||
}
|
||||
if (QWeb.dev) {
|
||||
this._validateProps(nextProps);
|
||||
}
|
||||
await this.willUpdateProps(nextProps);
|
||||
this.props = nextProps;
|
||||
await this.render(forceUpdate, patchQueue);
|
||||
@@ -366,19 +432,22 @@ export class Component<
|
||||
}
|
||||
|
||||
_patch(vnode) {
|
||||
this.__owl__.renderPromise = null;
|
||||
const target = this.__owl__.vnode || document.createElement(vnode.sel!);
|
||||
this.__owl__.vnode = patch(target, vnode);
|
||||
const __owl__ = this.__owl__;
|
||||
__owl__.renderPromise = null;
|
||||
const target = __owl__.vnode || document.createElement(vnode.sel!);
|
||||
__owl__.vnode = patch(target, vnode);
|
||||
}
|
||||
_prepare(): Promise<VNode> {
|
||||
this.__owl__.renderProps = this.props;
|
||||
this.__owl__.renderPromise = this._prepareAndRender();
|
||||
return this.__owl__.renderPromise;
|
||||
const __owl__ = this.__owl__;
|
||||
__owl__.renderProps = this.props;
|
||||
__owl__.renderPromise = this._prepareAndRender();
|
||||
return __owl__.renderPromise;
|
||||
}
|
||||
|
||||
async _prepareAndRender(): Promise<VNode> {
|
||||
await this.willStart();
|
||||
if (this.__owl__.isDestroyed) {
|
||||
const __owl__ = this.__owl__;
|
||||
if (__owl__.isDestroyed) {
|
||||
return Promise.resolve(h("div"));
|
||||
}
|
||||
const qweb = this.env.qweb;
|
||||
@@ -409,33 +478,35 @@ export class Component<
|
||||
}
|
||||
}
|
||||
}
|
||||
this.__owl__.render = qweb.render.bind(qweb, this.template);
|
||||
__owl__.render = qweb.render.bind(qweb, this.template);
|
||||
this._observeState();
|
||||
return this._render();
|
||||
}
|
||||
|
||||
async _render(
|
||||
force: boolean = false,
|
||||
patchQueue: any[] = []
|
||||
): Promise<VNode> {
|
||||
this.__owl__.renderId++;
|
||||
const __owl__ = this.__owl__;
|
||||
__owl__.renderId++;
|
||||
const promises: Promise<void>[] = [];
|
||||
const patch: any[] = [this];
|
||||
if (this.__owl__.isMounted) {
|
||||
if (__owl__.isMounted) {
|
||||
patchQueue.push(patch);
|
||||
}
|
||||
if (this.__owl__.observer) {
|
||||
this.__owl__.observer.allowMutations = false;
|
||||
if (__owl__.observer) {
|
||||
__owl__.observer.allowMutations = false;
|
||||
}
|
||||
let vnode = this.__owl__.render!(this, {
|
||||
let vnode = __owl__.render!(this, {
|
||||
promises,
|
||||
handlers: this.__owl__.boundHandlers,
|
||||
mountedHandlers: this.__owl__.mountedHandlers,
|
||||
handlers: __owl__.boundHandlers,
|
||||
mountedHandlers: __owl__.mountedHandlers,
|
||||
forceUpdate: force,
|
||||
patchQueue
|
||||
});
|
||||
patch.push(vnode);
|
||||
if (this.__owl__.observer) {
|
||||
this.__owl__.observer.allowMutations = true;
|
||||
if (__owl__.observer) {
|
||||
__owl__.observer.allowMutations = true;
|
||||
}
|
||||
|
||||
// this part is critical for the patching process to be done correctly. The
|
||||
@@ -443,38 +514,141 @@ export class Component<
|
||||
// will update its own vnode representation without the knowledge of the
|
||||
// parent widget. With this, we make sure that the parent widget will be
|
||||
// able to patch itself properly after
|
||||
vnode.key = this.__owl__.id;
|
||||
this.__owl__.renderProps = this.props;
|
||||
this.__owl__.renderPromise = Promise.all(promises).then(() => vnode);
|
||||
return this.__owl__.renderPromise;
|
||||
vnode.key = __owl__.id;
|
||||
__owl__.renderProps = this.props;
|
||||
__owl__.renderPromise = Promise.all(promises).then(() => vnode);
|
||||
return __owl__.renderPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only called by qweb t-widget directive
|
||||
*/
|
||||
_mount(vnode: VNode, elm: HTMLElement): VNode {
|
||||
this.__owl__.vnode = patch(elm, vnode);
|
||||
if (
|
||||
this.__owl__.parent!.__owl__.isMounted &&
|
||||
!this.__owl__.isMounted
|
||||
) {
|
||||
const __owl__ = this.__owl__;
|
||||
__owl__.vnode = patch(elm, vnode);
|
||||
if (__owl__.parent!.__owl__.isMounted && !__owl__.isMounted) {
|
||||
this._callMounted();
|
||||
}
|
||||
return this.__owl__.vnode;
|
||||
return __owl__.vnode;
|
||||
}
|
||||
|
||||
__mount() {
|
||||
if (!this.__owl__.isMounted) {
|
||||
this.__owl__.isMounted = true;
|
||||
/**
|
||||
* Only called by qweb t-widget directive (when t-keepalive is set)
|
||||
*/
|
||||
_remount() {
|
||||
const __owl__ = this.__owl__;
|
||||
if (!__owl__.isMounted) {
|
||||
__owl__.isMounted = true;
|
||||
this.mounted();
|
||||
}
|
||||
}
|
||||
|
||||
_observeState() {
|
||||
if (this.state) {
|
||||
this.__owl__.observer = new Observer();
|
||||
this.__owl__.observer.observe(this.state);
|
||||
this.__owl__.observer.notifyCB = this.render.bind(this);
|
||||
const __owl__ = this.__owl__;
|
||||
__owl__.observer = new Observer();
|
||||
__owl__.observer.observe(this.state);
|
||||
__owl__.observer.notifyCB = this.render.bind(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply default props (only top level).
|
||||
*
|
||||
* Note that this method does not modify in place the props, it returns a new
|
||||
* prop object
|
||||
*/
|
||||
_applyDefaultProps(props: Object | undefined, defaultProps: Object): Props {
|
||||
props = props ? Object.create(props) : {};
|
||||
for (let propName in defaultProps) {
|
||||
if (props![propName] === undefined) {
|
||||
props![propName] = defaultProps[propName];
|
||||
}
|
||||
}
|
||||
return <Props>props;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the component props (or next props) against the (static) props
|
||||
* description. This is potentially an expensive operation: it may needs to
|
||||
* visit recursively the props and all the children to check if they are valid.
|
||||
* This is why it is only done in 'dev' mode.
|
||||
*/
|
||||
_validateProps(props: Object) {
|
||||
const propsDef = (<any>this.constructor).props;
|
||||
if (propsDef instanceof Array) {
|
||||
// list of strings (prop names)
|
||||
for (let i = 0, l = propsDef.length; i < l; i++) {
|
||||
if (!(propsDef[i] in props)) {
|
||||
throw new Error(
|
||||
`Missing props '${propsDef[i]}' (widget '${this.constructor.name}')`
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (propsDef) {
|
||||
// propsDef is an object now
|
||||
for (let propName in propsDef) {
|
||||
if (!(propName in props)) {
|
||||
if (propsDef[propName] && !propsDef[propName].optional) {
|
||||
throw new Error(
|
||||
`Missing props '${propName}' (widget '${this.constructor.name}')`
|
||||
);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let isValid = isValidProp(props[propName], propsDef[propName]);
|
||||
if (!isValid) {
|
||||
throw new Error(
|
||||
`Props '${propName}' of invalid type in widget '${
|
||||
this.constructor.name
|
||||
}'`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Prop validation helper
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Check if an invidual prop value matches its (static) prop definition
|
||||
*/
|
||||
function isValidProp(prop, propDef): boolean {
|
||||
if (typeof propDef === "function") {
|
||||
// Check if a value is constructed by some Constructor. Note that there is a
|
||||
// slight abuse of language: we want to consider primitive values as well.
|
||||
//
|
||||
// So, even though 1 is not an instance of Number, we want to consider that
|
||||
// it is valid.
|
||||
if (typeof prop === "object") {
|
||||
return prop instanceof propDef;
|
||||
}
|
||||
return typeof prop === propDef.name.toLowerCase();
|
||||
} else if (propDef instanceof Array) {
|
||||
// If this code is executed, this means that we want to check if a prop
|
||||
// matches at least one of its descriptor.
|
||||
let result = false;
|
||||
for (let i = 0, iLen = propDef.length; i < iLen; i++) {
|
||||
result = result || isValidProp(prop, propDef[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// propsDef is an object
|
||||
let result = isValidProp(prop, propDef.type);
|
||||
if (propDef.type === Array) {
|
||||
for (let i = 0, iLen = prop.length; i < iLen; i++) {
|
||||
result = result && isValidProp(prop[i], propDef.element);
|
||||
}
|
||||
}
|
||||
if (propDef.type === Object) {
|
||||
const shape = propDef.shape;
|
||||
for (let key in shape) {
|
||||
result = result && isValidProp(prop[key], shape[key]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
+2
-1
@@ -64,7 +64,8 @@ export class EventBus {
|
||||
*/
|
||||
trigger(eventType: string, ...args: any[]) {
|
||||
const subs = this.subscriptions[eventType] || [];
|
||||
for (let sub of subs) {
|
||||
for (let i = 0, iLen = subs.length; i < iLen; i++) {
|
||||
const sub = subs[i];
|
||||
sub.callback.call(sub.owner, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
+19
-1
@@ -12,10 +12,28 @@ export { Observer } from "./observer";
|
||||
// themselves in QWeb, otherwise these files will not even be loaded.
|
||||
import "./qweb_directives";
|
||||
import "./qweb_extensions";
|
||||
export { QWeb } from "./qweb_core";
|
||||
import { QWeb } from "./qweb_core";
|
||||
export { QWeb };
|
||||
|
||||
export { connect, Store } from "./store";
|
||||
import * as _utils from "./utils";
|
||||
|
||||
export const __info__ = {};
|
||||
|
||||
Object.defineProperty(__info__, "mode", {
|
||||
get() {
|
||||
return QWeb.dev ? "dev" : "prod";
|
||||
},
|
||||
set(mode: string) {
|
||||
QWeb.dev = mode === "dev";
|
||||
if (QWeb.dev) {
|
||||
const url = `https://github.com/odoo/owl/blob/master/doc/tooling.md#development-mode`;
|
||||
console.warn(
|
||||
`Owl is running in 'dev' mode. This is not suitable for production use. See ${url} for more information.`
|
||||
);
|
||||
} else {
|
||||
console.log(`Owl is now running in 'prod' mode.`)
|
||||
}
|
||||
}
|
||||
});
|
||||
export const utils = _utils;
|
||||
|
||||
+63
-17
@@ -30,10 +30,13 @@ const methodsToPatch = [
|
||||
"sort",
|
||||
"reverse"
|
||||
];
|
||||
const methodLen = methodsToPatch.length;
|
||||
|
||||
const ArrayProto = Array.prototype;
|
||||
const ModifiedArrayProto = Object.create(ArrayProto);
|
||||
|
||||
for (let method of methodsToPatch) {
|
||||
for (let i = 0; i < methodLen; i++) {
|
||||
const method = methodsToPatch[i];
|
||||
const initialMethod = ArrayProto[method];
|
||||
ModifiedArrayProto[method] = function(...args) {
|
||||
if (!this.__observer__.allowMutations) {
|
||||
@@ -57,8 +60,8 @@ for (let method of methodsToPatch) {
|
||||
break;
|
||||
}
|
||||
if (inserted) {
|
||||
for (let elem of inserted) {
|
||||
this.__observer__.observe(elem, this);
|
||||
for (let i = 0, iLen = inserted.length; i < iLen; i++) {
|
||||
this.__observer__.observe(inserted[i], this);
|
||||
}
|
||||
}
|
||||
return initialMethod.call(this, ...args);
|
||||
@@ -73,6 +76,24 @@ export class Observer {
|
||||
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;
|
||||
@@ -105,27 +126,48 @@ export class Observer {
|
||||
}
|
||||
|
||||
set(target: any, key: number | string, value: any) {
|
||||
this.rev++;
|
||||
this._addProp(target, key, value);
|
||||
target.__owl__.rev++;
|
||||
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) {
|
||||
const keys = Object.keys(obj);
|
||||
obj.__owl__ = { rev: this.rev, deepRev: this.rev, parent };
|
||||
obj.__owl__ = {
|
||||
rev: this.rev,
|
||||
deepRev: this.rev,
|
||||
parent,
|
||||
observer: this
|
||||
};
|
||||
Object.defineProperty(obj, "__owl__", { enumerable: false });
|
||||
for (let key of keys) {
|
||||
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 };
|
||||
(<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; i < arr.length; i++) {
|
||||
for (let i = 0, iLen = arr.length; i < iLen; i++) {
|
||||
this.observe(arr[i], arr);
|
||||
}
|
||||
}
|
||||
@@ -137,29 +179,33 @@ export class Observer {
|
||||
) {
|
||||
var self = this;
|
||||
Object.defineProperty(obj, key, {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get() {
|
||||
return value;
|
||||
},
|
||||
set(newVal) {
|
||||
if (newVal !== value) {
|
||||
self.rev++;
|
||||
if (!self.allowMutations) {
|
||||
throw new Error(
|
||||
`Observed state cannot be changed here! (key: "${key}", val: "${newVal}")`
|
||||
);
|
||||
}
|
||||
self._updateRevNumber(obj);
|
||||
value = newVal;
|
||||
self.observe(newVal, obj);
|
||||
obj.__owl__.rev!++;
|
||||
let parent = obj;
|
||||
do {
|
||||
parent.__owl__.deepRev++;
|
||||
} while ((parent = parent.__owl__.parent) && parent !== obj);
|
||||
self.notifyChange();
|
||||
}
|
||||
}
|
||||
});
|
||||
this.observe(value, obj);
|
||||
}
|
||||
_updateRevNumber(target: any) {
|
||||
this.rev++;
|
||||
target.__owl__.rev!++;
|
||||
let parent = target;
|
||||
do {
|
||||
parent.__owl__.deepRev++;
|
||||
} while ((parent = parent.__owl__.parent) && parent !== target);
|
||||
}
|
||||
}
|
||||
|
||||
+94
-94
@@ -1,4 +1,5 @@
|
||||
import { VNode, h } from "./vdom";
|
||||
import { QWebVar, compileExpr } from "./qweb_expressions";
|
||||
|
||||
/**
|
||||
* Owl QWeb Engine
|
||||
@@ -56,18 +57,6 @@ export interface Directive {
|
||||
//------------------------------------------------------------------------------
|
||||
// Const/global stuff/helpers
|
||||
//------------------------------------------------------------------------------
|
||||
const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date".split(
|
||||
","
|
||||
);
|
||||
|
||||
const WORD_REPLACEMENT = {
|
||||
and: "&&",
|
||||
or: "||",
|
||||
gt: ">",
|
||||
gte: ">=",
|
||||
lt: "<",
|
||||
lte: "<="
|
||||
};
|
||||
|
||||
const DISABLED_TAGS = [
|
||||
"input",
|
||||
@@ -96,7 +85,14 @@ const NODE_HOOKS_PARAMS = {
|
||||
remove: "(vn, rm)"
|
||||
};
|
||||
|
||||
export const UTILS = {
|
||||
interface Utils {
|
||||
h: typeof h;
|
||||
objectToAttrString(obj: Object): string;
|
||||
shallowEqual(p1: Object, p2: Object): boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const UTILS: Utils = {
|
||||
h: h,
|
||||
objectToAttrString(obj: Object): string {
|
||||
let classes: string[] = [];
|
||||
@@ -106,6 +102,14 @@ export const UTILS = {
|
||||
}
|
||||
}
|
||||
return classes.join(" ");
|
||||
},
|
||||
shallowEqual(p1, p2) {
|
||||
for (let k in p1) {
|
||||
if (p1[k] !== p2[k]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -126,12 +130,21 @@ let nextID = 1;
|
||||
export class QWeb {
|
||||
templates: { [name: string]: Template } = {};
|
||||
utils = UTILS;
|
||||
static widgets = Object.create(null);
|
||||
|
||||
// dev mode enables better error messages or more costly validations
|
||||
static dev: boolean = false;
|
||||
|
||||
// the id field is useful to be able to hash qweb instances. The current
|
||||
// use case is that component's templates are qweb dependant, and need to be
|
||||
// able to map a qweb instance to a template name.
|
||||
id = nextID++;
|
||||
|
||||
// slots contains sub templates defined with t-set inside t-widget nodes, and
|
||||
// are meant to be used by the t-slot directive.
|
||||
slots = {};
|
||||
nextSlotId = 1;
|
||||
|
||||
constructor(data?: string) {
|
||||
if (data) {
|
||||
this.addTemplates(data);
|
||||
@@ -148,6 +161,13 @@ export class QWeb {
|
||||
}
|
||||
}
|
||||
|
||||
static register(name: string, Component: any) {
|
||||
if (QWeb.widgets[name]) {
|
||||
throw new Error(`Component '${name}' has already been registered`);
|
||||
}
|
||||
QWeb.widgets[name] = Component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a template to the internal template map. Note that it is not
|
||||
* immediately compiled.
|
||||
@@ -260,6 +280,12 @@ export class QWeb {
|
||||
// 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) {
|
||||
throw new Error("A template should have one root node");
|
||||
@@ -273,11 +299,14 @@ export class QWeb {
|
||||
ctx.code.join("\n")
|
||||
) as CompiledTemplate;
|
||||
} catch (e) {
|
||||
const templateName = ctx.templateName.replace(/`/g, "'");
|
||||
console.groupCollapsed(`Invalid Code generated by ${templateName}`);
|
||||
console.warn(ctx.code.join("\n"));
|
||||
console.groupEnd();
|
||||
throw new Error(
|
||||
`Invalid generated code while compiling template '${ctx.templateName.replace(
|
||||
/`/g,
|
||||
"'"
|
||||
)}': ${e.message}`
|
||||
`Invalid generated code while compiling template '${templateName}': ${
|
||||
e.message
|
||||
}`
|
||||
);
|
||||
}
|
||||
if (isDebug) {
|
||||
@@ -305,13 +334,15 @@ export class QWeb {
|
||||
}
|
||||
if (ctx.parentNode) {
|
||||
ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`);
|
||||
} else if (ctx.parentTextNode) {
|
||||
ctx.addLine(`vn${ctx.parentTextNode}.text += \`${text}\`;`);
|
||||
} else {
|
||||
// this is an unusual situation: this text node is the result of the
|
||||
// template rendering.
|
||||
let nodeID = ctx.generateID();
|
||||
ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`);
|
||||
ctx.rootContext.rootNode = nodeID;
|
||||
ctx.rootContext.parentNode = nodeID;
|
||||
ctx.rootContext.parentTextNode = nodeID;
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -338,17 +369,20 @@ export class QWeb {
|
||||
}
|
||||
}
|
||||
|
||||
for (let directive of DIRECTIVES) {
|
||||
const DIR_N = DIRECTIVES.length;
|
||||
const ATTR_N = attributes.length;
|
||||
for (let i = 0; i < DIR_N; i++) {
|
||||
let directive = DIRECTIVES[i];
|
||||
let fullName;
|
||||
let value;
|
||||
for (let i = 0; i < attributes.length; i++) {
|
||||
const name = attributes[i].name;
|
||||
for (let j = 0; j < ATTR_N; j++) {
|
||||
const name = attributes[j].name;
|
||||
if (
|
||||
name === "t-" + directive.name ||
|
||||
name.startsWith("t-" + directive.name + "-")
|
||||
) {
|
||||
fullName = name;
|
||||
value = attributes[i].textContent;
|
||||
value = attributes[j].textContent;
|
||||
validDirectives.push({ directive, value, fullName });
|
||||
if (directive.name === "on") {
|
||||
withHandlers = true;
|
||||
@@ -457,9 +491,6 @@ export class QWeb {
|
||||
props.push(`${key}: _${val}`);
|
||||
}
|
||||
}
|
||||
function formatter(expr) {
|
||||
return "${" + ctx.formatExpression(expr) + "}";
|
||||
}
|
||||
|
||||
for (let i = 0; i < attributes.length; i++) {
|
||||
let name = attributes[i].name;
|
||||
@@ -483,7 +514,8 @@ export class QWeb {
|
||||
// dynamic attributes
|
||||
if (name.startsWith("t-att-")) {
|
||||
let attName = name.slice(6);
|
||||
let formattedValue = ctx.formatExpression(ctx.getValue(value!));
|
||||
const v = ctx.getValue(value);
|
||||
let formattedValue = v.id || ctx.formatExpression(v);
|
||||
if (
|
||||
formattedValue[0] === "{" &&
|
||||
formattedValue[formattedValue.length - 1] === "}"
|
||||
@@ -518,17 +550,13 @@ export class QWeb {
|
||||
// attribute contains 'non letters' => we want to quote it
|
||||
attName = '"' + attName + '"';
|
||||
}
|
||||
const formattedExpr = value!
|
||||
.replace(/\{\{.*?\}\}/g, s => formatter(s.slice(2, -2)))
|
||||
.replace(/\#\{.*?\}/g, s => formatter(s.slice(2, -1)));
|
||||
const formattedExpr = ctx.interpolate(value);
|
||||
const attID = ctx.generateID();
|
||||
let staticVal = (<Element>node).getAttribute(attName);
|
||||
if (staticVal) {
|
||||
ctx.addLine(
|
||||
`var _${attID} = '${staticVal} ' + \`${formattedExpr}\`;`
|
||||
);
|
||||
ctx.addLine(`var _${attID} = '${staticVal} ' + ${formattedExpr};`);
|
||||
} else {
|
||||
ctx.addLine(`var _${attID} = \`${formattedExpr}\`;`);
|
||||
ctx.addLine(`var _${attID} = ${formattedExpr};`);
|
||||
}
|
||||
attrs.push(`${attName}: _${attID}`);
|
||||
}
|
||||
@@ -592,18 +620,21 @@ export class QWeb {
|
||||
//------------------------------------------------------------------------------
|
||||
// Compilation Context
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export class Context {
|
||||
nextID: number = 1;
|
||||
code: string[] = [];
|
||||
variables: { [key: string]: any } = {};
|
||||
definedVariables: { [key: string]: 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;
|
||||
@@ -621,7 +652,7 @@ export class Context {
|
||||
}
|
||||
|
||||
withParent(node: number): Context {
|
||||
if (this === this.rootContext && this.parentNode) {
|
||||
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) {
|
||||
@@ -669,65 +700,34 @@ export class Context {
|
||||
return val in this.variables ? this.getValue(this.variables[val]) : val;
|
||||
}
|
||||
|
||||
formatExpression(e: string): string {
|
||||
e = e.trim();
|
||||
if (e[0] === "{" && e[e.length - 1] === "}") {
|
||||
const innerExpr = e
|
||||
.slice(1, -1)
|
||||
.split(",")
|
||||
.map(p => {
|
||||
let [key, val] = p.trim().split(":");
|
||||
if (key === "") {
|
||||
return "";
|
||||
}
|
||||
if (!val) {
|
||||
val = key;
|
||||
}
|
||||
return `${key}: ${this.formatExpression(val)}`;
|
||||
})
|
||||
.join(",");
|
||||
return "{" + innerExpr + "}";
|
||||
/**
|
||||
* 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))})`;
|
||||
}
|
||||
|
||||
// Thanks CHM for this code...
|
||||
const chars = e.split("");
|
||||
let instring = "";
|
||||
let invar = "";
|
||||
let invarPos = 0;
|
||||
let r = "";
|
||||
chars.push(" ");
|
||||
for (var i = 0, ilen = chars.length; i < ilen; i++) {
|
||||
var c = chars[i];
|
||||
if (instring.length) {
|
||||
if (c === instring && chars[i - 1] !== "\\") {
|
||||
instring = "";
|
||||
}
|
||||
} else if (c === '"' || c === "'") {
|
||||
instring = c;
|
||||
} else if (c.match(/[a-zA-Z_\$]/) && !invar.length) {
|
||||
invar = c;
|
||||
invarPos = i;
|
||||
continue;
|
||||
} else if (c.match(/\W/) && invar.length) {
|
||||
// TODO: Should check for possible spaces before dot
|
||||
if (chars[invarPos - 1] !== "." && RESERVED_WORDS.indexOf(invar) < 0) {
|
||||
if (!(invar in this.definedVariables)) {
|
||||
invar =
|
||||
WORD_REPLACEMENT[invar] ||
|
||||
(invar in this.variables &&
|
||||
this.formatExpression(this.variables[invar])) ||
|
||||
"context['" + invar + "']";
|
||||
}
|
||||
}
|
||||
r += invar;
|
||||
invar = "";
|
||||
} else if (invar.length) {
|
||||
invar += c;
|
||||
continue;
|
||||
}
|
||||
r += c;
|
||||
}
|
||||
const result = r.slice(0, -1);
|
||||
return result;
|
||||
let r = s.replace(
|
||||
/\{\{.*?\}\}/g,
|
||||
s => "${" + this.formatExpression(s.slice(2, -2)) + "}"
|
||||
);
|
||||
return "`" + r + "`";
|
||||
}
|
||||
}
|
||||
|
||||
+69
-63
@@ -1,4 +1,5 @@
|
||||
import { Context, QWeb, UTILS } from "./qweb_core";
|
||||
import { QWebExprVar } from "./qweb_expressions";
|
||||
|
||||
/**
|
||||
* Owl QWeb Directives
|
||||
@@ -14,7 +15,6 @@ import { Context, QWeb, UTILS } from "./qweb_core";
|
||||
* - t-log
|
||||
*/
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-esc and t-raw
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -30,42 +30,49 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Context) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
let exprID = value;
|
||||
if (!(value in ctx.definedVariables)) {
|
||||
exprID = `_${ctx.generateID()}`;
|
||||
ctx.addLine(`var ${exprID} = ${ctx.formatExpression(value)};`);
|
||||
}
|
||||
ctx.addIf(`${exprID} || ${exprID} === 0`);
|
||||
if (!ctx.parentNode) {
|
||||
throw new Error("Should not have a text node without a parent");
|
||||
}
|
||||
if (ctx.escaping) {
|
||||
ctx.addLine(`c${ctx.parentNode}.push({text: ${exprID}});`);
|
||||
} else {
|
||||
let fragID = ctx.generateID();
|
||||
ctx.addLine(`var frag${fragID} = this.utils.getFragment(${exprID})`);
|
||||
let tempNodeID = ctx.generateID();
|
||||
ctx.addLine(`var p${tempNodeID} = {hook: {`);
|
||||
ctx.addLine(
|
||||
` insert: n => n.elm.parentNode.replaceChild(frag${fragID}, n.elm),`
|
||||
);
|
||||
ctx.addLine(`}};`);
|
||||
ctx.addLine(`var vn${tempNodeID} = h('div', p${tempNodeID})`);
|
||||
ctx.addLine(`c${ctx.parentNode}.push(vn${tempNodeID});`);
|
||||
}
|
||||
if (node.childNodes.length) {
|
||||
ctx.addElse();
|
||||
qweb._compileChildren(node, ctx);
|
||||
}
|
||||
ctx.closeIf();
|
||||
return;
|
||||
}
|
||||
if (value instanceof NodeList) {
|
||||
for (let node of Array.from(value)) {
|
||||
if (value.xml instanceof NodeList) {
|
||||
for (let node of Array.from(value.xml)) {
|
||||
qweb._compileNode(<ChildNode>node, ctx);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let exprID: string;
|
||||
if (typeof value === "string") {
|
||||
exprID = `_${ctx.generateID()}`;
|
||||
ctx.addLine(`var ${exprID} = ${ctx.formatExpression(value)};`);
|
||||
} else {
|
||||
exprID = value.id;
|
||||
}
|
||||
ctx.addIf(`${exprID} || ${exprID} === 0`);
|
||||
if (ctx.escaping) {
|
||||
if (ctx.parentTextNode) {
|
||||
ctx.addLine(`vn${ctx.parentTextNode}.text += ${exprID};`);
|
||||
} else if (ctx.parentNode) {
|
||||
ctx.addLine(`c${ctx.parentNode}.push({text: ${exprID}});`);
|
||||
} else {
|
||||
let nodeID = ctx.generateID();
|
||||
ctx.rootContext.rootNode = nodeID;
|
||||
ctx.rootContext.parentTextNode = nodeID;
|
||||
ctx.addLine(`var vn${nodeID} = {text: ${exprID}};`);
|
||||
}
|
||||
} else {
|
||||
let fragID = ctx.generateID();
|
||||
ctx.addLine(`var frag${fragID} = this.utils.getFragment(${exprID})`);
|
||||
let tempNodeID = ctx.generateID();
|
||||
ctx.addLine(`var p${tempNodeID} = {hook: {`);
|
||||
ctx.addLine(
|
||||
` insert: n => n.elm.parentNode.replaceChild(frag${fragID}, n.elm),`
|
||||
);
|
||||
ctx.addLine(`}};`);
|
||||
ctx.addLine(`var vn${tempNodeID} = h('div', p${tempNodeID})`);
|
||||
ctx.addLine(`c${ctx.parentNode}.push(vn${tempNodeID});`);
|
||||
}
|
||||
if (node.childNodes.length) {
|
||||
ctx.addElse();
|
||||
qweb._compileChildren(node, ctx);
|
||||
}
|
||||
|
||||
ctx.closeIf();
|
||||
}
|
||||
|
||||
QWeb.addDirective({
|
||||
@@ -109,15 +116,21 @@ QWeb.addDirective({
|
||||
if (value) {
|
||||
const formattedValue = ctx.formatExpression(value);
|
||||
if (ctx.variables.hasOwnProperty(variable)) {
|
||||
ctx.addLine(`${ctx.variables[variable]} = ${formattedValue}`);
|
||||
ctx.addLine(
|
||||
`${(<QWebExprVar>ctx.variables[variable]).id} = ${formattedValue}`
|
||||
);
|
||||
} else {
|
||||
const varName = `_${ctx.generateID()}`;
|
||||
ctx.addLine(`var ${varName} = ${formattedValue};`);
|
||||
ctx.definedVariables[varName] = formattedValue;
|
||||
ctx.variables[variable] = varName;
|
||||
ctx.variables[variable] = {
|
||||
id: varName,
|
||||
expr: formattedValue
|
||||
};
|
||||
}
|
||||
} else {
|
||||
ctx.variables[variable] = node.childNodes;
|
||||
ctx.variables[variable] = {
|
||||
xml: node.childNodes
|
||||
};
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -189,30 +202,27 @@ QWeb.addDirective({
|
||||
tempCtx.nextID = ctx.rootContext.nextID;
|
||||
qweb._compileNode(nodeCopy, tempCtx);
|
||||
const vars = Object.assign({}, ctx.variables, tempCtx.variables);
|
||||
var definedVariables = Object.assign(
|
||||
{},
|
||||
ctx.definedVariables,
|
||||
tempCtx.definedVariables
|
||||
);
|
||||
ctx.rootContext.nextID = tempCtx.nextID;
|
||||
|
||||
// open new scope, if necessary
|
||||
const hasNewVariables = Object.keys(definedVariables).length > 0;
|
||||
const hasNewVariables = Object.keys(tempCtx.variables).length > 0;
|
||||
if (hasNewVariables) {
|
||||
ctx.addLine("{");
|
||||
ctx.indent();
|
||||
}
|
||||
|
||||
// add new variables, if any
|
||||
for (let key in definedVariables) {
|
||||
ctx.addLine(`let ${key} = ${definedVariables[key]}`);
|
||||
// add new variables, if any
|
||||
for (let key in tempCtx.variables) {
|
||||
const v = tempCtx.variables[key];
|
||||
if ((<QWebExprVar>v).expr) {
|
||||
ctx.addLine(`let ${(<QWebExprVar>v).id} = ${(<QWebExprVar>v).expr};`);
|
||||
}
|
||||
// todo: handle XML variables...
|
||||
}
|
||||
}
|
||||
|
||||
// compile sub template
|
||||
const subCtx = ctx
|
||||
.subContext("caller", nodeCopy)
|
||||
.subContext("variables", Object.create(vars))
|
||||
.subContext("definedVariables", Object.create(definedVariables));
|
||||
.subContext("variables", Object.create(vars));
|
||||
|
||||
qweb._compileNode(nodeTemplate.elem, subCtx);
|
||||
|
||||
@@ -243,22 +253,18 @@ QWeb.addDirective({
|
||||
ctx.addLine(
|
||||
`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`
|
||||
);
|
||||
ctx.addLine(
|
||||
`if (typeof _${arrayID} === 'number') { _${arrayID} = Array.from(Array(_${arrayID}).keys())}`
|
||||
);
|
||||
let keysID = ctx.generateID();
|
||||
ctx.addLine(
|
||||
`var _${keysID} = _${arrayID} instanceof Array ? _${arrayID} : Object.keys(_${arrayID});`
|
||||
);
|
||||
let valuesID = ctx.generateID();
|
||||
ctx.addLine(
|
||||
`var _${valuesID} = _${arrayID} instanceof Array ? _${arrayID} : Object.values(_${arrayID});`
|
||||
);
|
||||
ctx.addLine(`for (let i = 0; i < _${keysID}.length; i++) {`);
|
||||
ctx.addLine(`var _${keysID} = _${valuesID} = _${arrayID};`);
|
||||
ctx.addIf(`!(_${arrayID} instanceof Array)`);
|
||||
ctx.addLine(`_${keysID} = Object.keys(_${arrayID});`);
|
||||
ctx.addLine(`_${valuesID} = Object.values(_${arrayID});`);
|
||||
ctx.closeIf();
|
||||
ctx.addLine(`var _length${keysID} = _${keysID}.length;`);
|
||||
ctx.addLine(`for (let i = 0; i < _length${keysID}; i++) {`);
|
||||
ctx.indent();
|
||||
ctx.addLine(`context.${name}_first = i === 0;`);
|
||||
ctx.addLine(`context.${name}_last = i === _${keysID}.length - 1;`);
|
||||
ctx.addLine(`context.${name}_parity = i % 2 === 0 ? 'even' : 'odd';`);
|
||||
ctx.addLine(`context.${name}_last = i === _length${keysID} - 1;`);
|
||||
ctx.addLine(`context.${name}_index = i;`);
|
||||
ctx.addLine(`context.${name} = _${keysID}[i];`);
|
||||
ctx.addLine(`context.${name}_value = _${valuesID}[i];`);
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
/**
|
||||
* Owl QWeb Expression Parser
|
||||
*
|
||||
* Owl needs in various contexts to be able to understand the structure of a
|
||||
* string representing a javascript expression. The usual goal is to be able
|
||||
* to rewrite some variables. For example, if a template has
|
||||
*
|
||||
* ```xml
|
||||
* <t t-if="computeSomething({val: state.val})">...</t>
|
||||
* ```
|
||||
*
|
||||
* this needs to be translated in something like this:
|
||||
*
|
||||
* ```js
|
||||
* if (context["computeSomething"]({val: context["state"].val})) { ... }
|
||||
* ```
|
||||
*
|
||||
* This file contains the implementation of an extremely naive tokenizer/parser
|
||||
* and evaluator for javascript expressions. The supported grammar is basically
|
||||
* only expressive enough to understand the shape of objects, of arrays, and
|
||||
* various operators.
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Misc types, constants and helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date".split(
|
||||
","
|
||||
);
|
||||
|
||||
const WORD_REPLACEMENT = {
|
||||
and: "&&",
|
||||
or: "||",
|
||||
gt: ">",
|
||||
gte: ">=",
|
||||
lt: "<",
|
||||
lte: "<="
|
||||
};
|
||||
|
||||
export interface QWebExprVar {
|
||||
id: string;
|
||||
expr: string;
|
||||
}
|
||||
|
||||
export interface QWebXMLVar {
|
||||
xml: NodeList;
|
||||
}
|
||||
|
||||
export type QWebVar = QWebExprVar | QWebXMLVar;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Tokenizer
|
||||
//------------------------------------------------------------------------------
|
||||
type TKind =
|
||||
| "LEFT_BRACE"
|
||||
| "RIGHT_BRACE"
|
||||
| "LEFT_BRACKET"
|
||||
| "RIGHT_BRACKET"
|
||||
| "LEFT_PAREN"
|
||||
| "RIGHT_PAREN"
|
||||
| "COMMA"
|
||||
| "VALUE"
|
||||
| "SYMBOL"
|
||||
| "OPERATOR"
|
||||
| "COLON";
|
||||
|
||||
interface Token {
|
||||
type: TKind;
|
||||
value: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
const STATIC_TOKEN_MAP: { [key: string]: TKind } = {
|
||||
"{": "LEFT_BRACE",
|
||||
"}": "RIGHT_BRACE",
|
||||
"[": "LEFT_BRACKET",
|
||||
"]": "RIGHT_BRACKET",
|
||||
":": "COLON",
|
||||
",": "COMMA",
|
||||
"(": "LEFT_PAREN",
|
||||
")": "RIGHT_PAREN"
|
||||
};
|
||||
|
||||
const OPERATORS = ".,===,==,+,!,||,&&,>=,>,<=,<,?,-,*,/,%".split(',');
|
||||
|
||||
type Tokenizer = (expr: string) => Token | false;
|
||||
|
||||
let tokenizeString: Tokenizer = function(expr) {
|
||||
let s = expr[0];
|
||||
let start = s;
|
||||
if (s !== "'" && s !== '"') {
|
||||
return false;
|
||||
}
|
||||
let i = 1;
|
||||
let cur;
|
||||
while (expr[i] && expr[i] !== start) {
|
||||
cur = expr[i];
|
||||
s += cur;
|
||||
if (cur === "\\") {
|
||||
i++;
|
||||
cur = expr[i];
|
||||
if (!cur) {
|
||||
throw new Error("Invalid expression");
|
||||
}
|
||||
s += cur;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (expr[i] !== start) {
|
||||
throw new Error("Invalid expression");
|
||||
}
|
||||
s += start;
|
||||
return { type: "VALUE", value: s };
|
||||
};
|
||||
|
||||
let tokenizeNumber: Tokenizer = function(expr) {
|
||||
let s = expr[0];
|
||||
if (s && s.match(/[0-9]/)) {
|
||||
let i = 1;
|
||||
while (expr[i] && expr[i].match(/[0-9]|\./)) {
|
||||
s += expr[i];
|
||||
i++;
|
||||
}
|
||||
return { type: "VALUE", value: s };
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let tokenizeSymbol: Tokenizer = function(expr) {
|
||||
let s = expr[0];
|
||||
if (s && s.match(/[a-zA-Z_\$]/)) {
|
||||
let i = 1;
|
||||
while (expr[i] && expr[i].match(/\w/)) {
|
||||
s += expr[i];
|
||||
i++;
|
||||
}
|
||||
if (s in WORD_REPLACEMENT) {
|
||||
return { type: "OPERATOR", value: WORD_REPLACEMENT[s], size: s.length };
|
||||
}
|
||||
return { type: "SYMBOL", value: s };
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const tokenizeStatic: Tokenizer = function(expr) {
|
||||
const char = expr[0];
|
||||
if (char && char in STATIC_TOKEN_MAP) {
|
||||
return { type: STATIC_TOKEN_MAP[char], value: char };
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const tokenizeOperator: Tokenizer = function(expr) {
|
||||
for (let op of OPERATORS) {
|
||||
if (expr.startsWith(op)) {
|
||||
return { type: "OPERATOR", value: op };
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const TOKENIZERS = [
|
||||
tokenizeString,
|
||||
tokenizeNumber,
|
||||
tokenizeSymbol,
|
||||
tokenizeStatic,
|
||||
tokenizeOperator
|
||||
];
|
||||
|
||||
/**
|
||||
* Convert a javascript expression (as a string) into a list of tokens. For
|
||||
* example: `tokenize("1 + b")` will return:
|
||||
* ```js
|
||||
* [
|
||||
* {type: "VALUE", value: "1"},
|
||||
* {type: "OPERATOR", value: "+"},
|
||||
* {type: "SYMBOL", value: "b"}
|
||||
* ]
|
||||
* ```
|
||||
*/
|
||||
export function tokenize(expr: string): Token[] {
|
||||
const result: Token[] = [];
|
||||
let token: boolean | Token = true;
|
||||
|
||||
while (token) {
|
||||
expr = expr.trim();
|
||||
if (expr) {
|
||||
for (let tokenizer of TOKENIZERS) {
|
||||
token = tokenizer(expr);
|
||||
if (token) {
|
||||
result.push(token);
|
||||
expr = expr.slice(token.size || token.value.length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
token = false;
|
||||
}
|
||||
}
|
||||
if (expr.length) {
|
||||
throw new Error(`Tokenizer error: could not tokenize "${expr}"`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Expression "evaluator"
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* This is the main function exported by this file. This is the code that will
|
||||
* process an expression (given as a string) and returns another expression with
|
||||
* proper lookups in the context.
|
||||
*
|
||||
* Usually, this kind of code would be very simple to do if we had an AST (so,
|
||||
* if we had a javascript parser), since then, we would only need to find the
|
||||
* variables and replace them. However, a parser is more complicated, and there
|
||||
* are no standard builtin parser API.
|
||||
*
|
||||
* Since this method is applied to simple javasript expressions, and the work to
|
||||
* be done is actually quite simple, we actually can get away with not using a
|
||||
* parser, which helps with the code size.
|
||||
*
|
||||
* Here is the heuristic used by this method to determine if a token is a
|
||||
* variable:
|
||||
* - by default, all symbols are considered a variable
|
||||
* - unless the previous token is a dot (in that case, this is a property: `a.b`)
|
||||
* - or if the previous token is a left brace or a comma, and the next token is
|
||||
* a colon (in that case, this is an object key: `{a: b}`)
|
||||
*/
|
||||
export function compileExpr(
|
||||
expr: string,
|
||||
vars: { [key: string]: QWebVar }
|
||||
): string {
|
||||
const tokens = tokenize(expr);
|
||||
let result = "";
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
let token = tokens[i];
|
||||
if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) {
|
||||
// we need to find if it is a variable
|
||||
let isVar = true;
|
||||
let prevToken = tokens[i - 1];
|
||||
if (prevToken) {
|
||||
if (prevToken.type === "OPERATOR" && prevToken.value === ".") {
|
||||
isVar = false;
|
||||
} else if (
|
||||
prevToken.type === "LEFT_BRACE" ||
|
||||
prevToken.type === "COMMA"
|
||||
) {
|
||||
let nextToken = tokens[i + 1];
|
||||
if (nextToken && nextToken.type === "COLON") {
|
||||
isVar = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isVar) {
|
||||
if (token.value in vars && "id" in vars[token.value]) {
|
||||
token.value = (<QWebExprVar>vars[token.value]).id;
|
||||
} else {
|
||||
token.value = `context['${token.value}']`;
|
||||
}
|
||||
}
|
||||
}
|
||||
result += token.value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
+218
-76
@@ -1,4 +1,5 @@
|
||||
import { QWeb, UTILS } from "./qweb_core";
|
||||
import { VNode } from "./vdom";
|
||||
|
||||
/**
|
||||
* Owl QWeb Extensions
|
||||
@@ -9,40 +10,64 @@ import { QWeb, UTILS } from "./qweb_core";
|
||||
* - t-on
|
||||
* - t-ref
|
||||
* - t-transition
|
||||
* - t-widget/t-props/t-keepalive
|
||||
* - t-widget/t-keepalive
|
||||
* - t-mounted
|
||||
* - t-slot
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// 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 = fullName.slice(5);
|
||||
const [eventName, ...mods] = fullName.slice(5).split(".");
|
||||
if (!eventName) {
|
||||
throw new Error("Missing event name with t-on directive");
|
||||
}
|
||||
let extraArgs;
|
||||
let handler = value.replace(/\(.*\)/, function(args) {
|
||||
let handlerName = 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,
|
||||
"'"
|
||||
)}'\`)})()`;
|
||||
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}'] = (context['${handler}'] || ${error}).bind(owner, ${ctx.formatExpression(
|
||||
extraArgs
|
||||
)});`
|
||||
);
|
||||
ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
|
||||
} else {
|
||||
ctx.addLine(
|
||||
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || (context['${handler}'] || ${error}).bind(owner);`
|
||||
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`
|
||||
);
|
||||
ctx.addLine(
|
||||
`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`
|
||||
@@ -57,9 +82,9 @@ QWeb.addDirective({
|
||||
QWeb.addDirective({
|
||||
name: "ref",
|
||||
priority: 95,
|
||||
atNodeCreation({ ctx, nodeID, value, addNodeHook }) {
|
||||
atNodeCreation({ ctx, value, addNodeHook }) {
|
||||
const refKey = `ref${ctx.generateID()}`;
|
||||
ctx.addLine(`const ${refKey} = ${ctx.formatExpression(value)}`);
|
||||
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
|
||||
addNodeHook("create", `context.refs[${refKey}] = n.elm;`);
|
||||
}
|
||||
});
|
||||
@@ -67,16 +92,23 @@ QWeb.addDirective({
|
||||
//------------------------------------------------------------------------------
|
||||
// t-transition
|
||||
//------------------------------------------------------------------------------
|
||||
(<any>UTILS).nextFrame = function(cb: () => void) {
|
||||
UTILS.nextFrame = function(cb: () => void) {
|
||||
requestAnimationFrame(() => requestAnimationFrame(cb));
|
||||
};
|
||||
|
||||
(<any>UTILS).transitionCreate = function(elm: HTMLElement, name: string) {
|
||||
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");
|
||||
};
|
||||
|
||||
(<any>UTILS).transitionInsert = function(elm: HTMLElement, name: string) {
|
||||
const finalize = () => {
|
||||
elm.classList.remove(name + "-enter-active");
|
||||
elm.classList.remove(name + "-enter-to");
|
||||
@@ -88,16 +120,15 @@ QWeb.addDirective({
|
||||
});
|
||||
};
|
||||
|
||||
(<any>UTILS).transitionRemove = function(
|
||||
elm: HTMLElement,
|
||||
name: string,
|
||||
rm: () => void
|
||||
) {
|
||||
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 + "-enter-to");
|
||||
elm.classList.remove(name + "-leave-to");
|
||||
rm();
|
||||
};
|
||||
this.nextFrame(() => {
|
||||
@@ -146,12 +177,11 @@ function whenTransitionEnd(elm: HTMLElement, cb) {
|
||||
QWeb.addDirective({
|
||||
name: "transition",
|
||||
priority: 96,
|
||||
atNodeCreation({ ctx, value, addNodeHook }) {
|
||||
atNodeCreation({ value, addNodeHook }) {
|
||||
let name = value;
|
||||
const hooks = {
|
||||
create: `this.utils.transitionCreate(n.elm, '${name}');`,
|
||||
insert: `this.utils.transitionInsert(vn.elm, '${name}');`,
|
||||
remove: `this.utils.transitionRemove(vn.elm, '${name}', rm);`
|
||||
insert: `this.utils.transitionInsert(vn, '${name}');`,
|
||||
remove: `this.utils.transitionRemove(vn, '${name}', rm);`
|
||||
};
|
||||
for (let hookName in hooks) {
|
||||
addNodeHook(hookName, hooks[hookName]);
|
||||
@@ -163,6 +193,10 @@ QWeb.addDirective({
|
||||
// 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
|
||||
@@ -173,10 +207,17 @@ QWeb.addDirective({
|
||||
* explanation of the code generated by the t-widget directive for the following
|
||||
* situation:
|
||||
* ```xml
|
||||
* <t t-widget="child" t-key="'somestring'" t-props="{flag:state.flag}"/>
|
||||
* <t 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);
|
||||
@@ -191,7 +232,7 @@ QWeb.addDirective({
|
||||
* let _2_index = c1.length;
|
||||
* c1.push(null);
|
||||
*
|
||||
* // def3 is the deferred that will contain later either the new widget
|
||||
* // def3 is the promise that will contain later either the new widget
|
||||
* // creation, or the props update...
|
||||
* let def3;
|
||||
*
|
||||
@@ -210,24 +251,27 @@ QWeb.addDirective({
|
||||
* // computation, so it is certainly better to do it only once
|
||||
* let props4 = { flag: context["state"].flag };
|
||||
*
|
||||
* // If we have a widget, currently rendering, but not ready yet, and which was
|
||||
* // rendered with different props, we do not want to wait for it to be ready,
|
||||
* // then update it. We simply destroy it, and start anew.
|
||||
* if (
|
||||
* w4 &&
|
||||
* w4.__owl__.renderPromise &&
|
||||
* !w4.__owl__.isStarted &&
|
||||
* props4 !== w4.__owl__.renderProps
|
||||
* ) {
|
||||
* w4.destroy();
|
||||
* w4 = false;
|
||||
* // If 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["child"];
|
||||
* let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
|
||||
|
||||
* if (!W4) {
|
||||
* throw new Error("Cannot find the definition of widget 'child'");
|
||||
* }
|
||||
@@ -262,15 +306,21 @@ QWeb.addDirective({
|
||||
* // 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() {
|
||||
* // apparently, in some cases, it is necessary to call the destroy
|
||||
* // method here
|
||||
* w4.destroy();
|
||||
* // 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() {
|
||||
* // and here...
|
||||
* w4.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
|
||||
@@ -283,7 +333,9 @@ QWeb.addDirective({
|
||||
* } else {
|
||||
* // this is the 'update' path of the directive.
|
||||
* // the call to _updateProps is the actual widget update
|
||||
* def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
||||
* // 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
|
||||
@@ -310,19 +362,36 @@ QWeb.addDirective({
|
||||
name: "widget",
|
||||
extraNames: ["props", "keepalive"],
|
||||
priority: 100,
|
||||
atNodeEncounter({ ctx, value, node }): boolean {
|
||||
atNodeEncounter({ ctx, value, node, qweb }): boolean {
|
||||
ctx.addLine("//WIDGET");
|
||||
ctx.rootContext.shouldDefineOwner = true;
|
||||
let props = node.getAttribute("t-props");
|
||||
ctx.rootContext.shouldDefineQWeb = true;
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
let keepAlive = node.getAttribute("t-keepalive") ? true : false;
|
||||
|
||||
// t-on- events...
|
||||
const events: [string, string][] = [];
|
||||
// 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-")) {
|
||||
events.push([name.slice(5), attributes[i].textContent!]);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,9 +399,11 @@ QWeb.addDirective({
|
||||
if (key) {
|
||||
key = ctx.formatExpression(key);
|
||||
}
|
||||
if (props) {
|
||||
props = ctx.formatExpression(props);
|
||||
}
|
||||
|
||||
// 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();
|
||||
@@ -356,16 +427,26 @@ QWeb.addDirective({
|
||||
let refKey: string = "";
|
||||
if (ref) {
|
||||
refKey = `ref${ctx.generateID()}`;
|
||||
ctx.addLine(`const ${refKey} = ${ctx.formatExpression(ref)}`);
|
||||
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) {
|
||||
finalizeWidgetCode += `;delete context.refs[${refKey}]`;
|
||||
}();`;
|
||||
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");
|
||||
@@ -377,7 +458,7 @@ QWeb.addDirective({
|
||||
tattStyle = attVar;
|
||||
}
|
||||
let updateClassCode = "";
|
||||
if (classAttr || tattClass || styleAttr || tattStyle) {
|
||||
if (classAttr || tattClass || styleAttr || tattStyle || events.length) {
|
||||
let classCode = "";
|
||||
if (classAttr) {
|
||||
classCode =
|
||||
@@ -396,40 +477,83 @@ QWeb.addDirective({
|
||||
}`;
|
||||
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}}};`;
|
||||
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 props${widgetID} = ${props || "{}"};`);
|
||||
ctx.addLine(`let props${widgetID} = {${propStr}};`);
|
||||
ctx.addIf(
|
||||
`w${widgetID} && w${widgetID}.__owl__.renderPromise && !w${widgetID}.__owl__.vnode && props${widgetID} !== w${widgetID}.__owl__.renderProps`
|
||||
`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.addLine(`w${widgetID} = false;`);
|
||||
ctx.closeIf();
|
||||
ctx.closeIf();
|
||||
|
||||
ctx.addIf(`!w${widgetID}`);
|
||||
// new widget
|
||||
ctx.addLine(`let W${widgetID} = context.widgets['${value}'];`);
|
||||
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 "${value}"\`)}`
|
||||
`if (!W${widgetID}) {throw new Error('Cannot find the definition of widget "' + widgetKey${widgetID} + '"')}`
|
||||
);
|
||||
ctx.addLine(`w${widgetID} = new W${widgetID}(owner, props${widgetID});`);
|
||||
ctx.addLine(
|
||||
`context.__owl__.cmap[${templateID}] = w${widgetID}.__owl__.id;`
|
||||
);
|
||||
for (let [event, method] of events) {
|
||||
ctx.addLine(`w${widgetID}.on('${event}', owner, owner['${method}'])`);
|
||||
|
||||
// SLOTS
|
||||
const slotNodes = node.querySelectorAll("[t-set]");
|
||||
if (slotNodes.length) {
|
||||
const slotId = qweb.nextSlotId++;
|
||||
for (let i = 0, length = slotNodes.length; i < length; i++) {
|
||||
const slotNode = slotNodes[i];
|
||||
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);
|
||||
}
|
||||
ctx.addLine(`w${widgetID}.__owl__.slotId = ${slotId};`);
|
||||
}
|
||||
|
||||
ctx.addLine(`def${defID} = w${widgetID}._prepare();`);
|
||||
// hack: specify empty remove hook to prevent the node from being removed from the DOM
|
||||
// FIXME: click to re-add widget during remove transition -> leak
|
||||
ctx.addLine(
|
||||
`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn){let nvn=w${widgetID}._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}},remove(){${finalizeWidgetCode}},destroy(){${finalizeWidgetCode}}}});c${
|
||||
`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;});`
|
||||
);
|
||||
@@ -437,11 +561,11 @@ QWeb.addDirective({
|
||||
ctx.addElse();
|
||||
// need to update widget
|
||||
ctx.addLine(
|
||||
`def${defID} = w${widgetID}._updateProps(props${widgetID}, extra.forceUpdate, extra.patchQueue);`
|
||||
`def${defID} = def${defID} || w${widgetID}._updateProps(props${widgetID}, extra.forceUpdate, extra.patchQueue);`
|
||||
);
|
||||
let keepAliveCode = "";
|
||||
if (keepAlive) {
|
||||
keepAliveCode = `pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w${widgetID}.el,vn.elm);vn.elm=w${widgetID}.el;w${widgetID}.__mount();};`;
|
||||
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};${
|
||||
@@ -504,3 +628,21 @@ QWeb.addDirective({
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// 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;
|
||||
}
|
||||
});
|
||||
|
||||
+98
-96
@@ -21,7 +21,7 @@ import { Observer } from "./observer";
|
||||
// Store Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
type Mutation = ({ state, commit, set, getters }, payload: any) => void;
|
||||
type Mutation = ({ state, commit, getters }, payload: any) => void;
|
||||
type Action = ({ commit, state, dispatch, env, getters }, payload: any) => void;
|
||||
type Getter = ({ state, getters }, payload) => any;
|
||||
|
||||
@@ -46,7 +46,6 @@ export class Store extends EventBus {
|
||||
debug: boolean;
|
||||
env: any;
|
||||
observer: Observer;
|
||||
set: any;
|
||||
getters: { [name: string]: (payload?) => any };
|
||||
|
||||
constructor(config: StoreConfig, options: StoreOption = {}) {
|
||||
@@ -65,7 +64,6 @@ export class Store extends EventBus {
|
||||
if (this.debug) {
|
||||
this.history.push({ state: this.state });
|
||||
}
|
||||
this.set = this.observer.set.bind(this.observer);
|
||||
|
||||
for (let entry of Object.entries(config.getters || {})) {
|
||||
const name: string = entry[0];
|
||||
@@ -110,7 +108,6 @@ export class Store extends EventBus {
|
||||
{
|
||||
commit: this.commit.bind(this),
|
||||
state: this.state,
|
||||
set: this.set,
|
||||
getters: this.getters
|
||||
},
|
||||
payload
|
||||
@@ -153,11 +150,22 @@ 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(mapStateToProps, options: any = {}) {
|
||||
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;
|
||||
@@ -183,101 +191,95 @@ export function connect(mapStateToProps, options: any = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
return function<E extends EnvWithStore, P, S>(
|
||||
Comp: Constructor<Component<E, P, S>>
|
||||
) {
|
||||
const Result = class extends Comp {
|
||||
constructor(parent, props?: any) {
|
||||
const env = parent instanceof Component ? parent.env : parent;
|
||||
const ownProps = Object.assign({}, props || {});
|
||||
const storeProps = mapStateToProps(
|
||||
env.store.state,
|
||||
ownProps,
|
||||
env.store.getters
|
||||
);
|
||||
const mergedProps = Object.assign({}, props || {}, storeProps);
|
||||
super(parent, mergedProps);
|
||||
(<any>this.__owl__).ownProps = ownProps;
|
||||
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;
|
||||
(<any>this.__owl__).storeHash = hashFunction(
|
||||
{
|
||||
state: env.store.state,
|
||||
storeProps: storeProps,
|
||||
revNumber,
|
||||
deepRevNumber
|
||||
},
|
||||
{
|
||||
currentStoreProps: storeProps
|
||||
}
|
||||
);
|
||||
this._updateProps(ownProps, false);
|
||||
}
|
||||
/**
|
||||
* We do not use the mounted hook here for a subtle reason: we want the
|
||||
* updates to be called for the parents before the children. However,
|
||||
* if we use the mounted hook, this will be done in the reverse order.
|
||||
*/
|
||||
_callMounted() {
|
||||
this.env.store.on("update", this, this._checkUpdate);
|
||||
super._callMounted();
|
||||
}
|
||||
willUnmount() {
|
||||
this.env.store.off("update", this);
|
||||
super.willUnmount();
|
||||
}
|
||||
|
||||
_checkUpdate() {
|
||||
const ownProps = (<any>this.__owl__).ownProps;
|
||||
const storeProps = mapStateToProps(
|
||||
this.env.store.state,
|
||||
ownProps,
|
||||
this.env.store.getters
|
||||
);
|
||||
const options: any = {
|
||||
currentStoreProps: (<any>this.__owl__).currentStoreProps
|
||||
};
|
||||
const storeHash = hashFunction(
|
||||
{
|
||||
state: this.env.store.state,
|
||||
storeProps: storeProps,
|
||||
revNumber,
|
||||
deepRevNumber
|
||||
},
|
||||
options
|
||||
);
|
||||
let didChange = options.didChange;
|
||||
if (storeHash !== (<any>this.__owl__).storeHash) {
|
||||
didChange = true;
|
||||
(<any>this.__owl__).storeHash = storeHash;
|
||||
}
|
||||
if (didChange) {
|
||||
(<any>this.__owl__).currentStoreProps = storeProps;
|
||||
this._updateProps(ownProps, false);
|
||||
}
|
||||
}
|
||||
_updateProps(nextProps, forceUpdate, patchQueue?: any[]) {
|
||||
if ((<any>this.__owl__).ownProps !== nextProps) {
|
||||
(<any>this.__owl__).currentStoreProps = mapStateToProps(
|
||||
this.env.store.state,
|
||||
nextProps,
|
||||
this.env.store.getters
|
||||
);
|
||||
}
|
||||
(<any>this.__owl__).ownProps = nextProps;
|
||||
const mergedProps = Object.assign(
|
||||
{},
|
||||
}
|
||||
_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__).currentStoreProps
|
||||
(<any>this.__owl__).store.getters
|
||||
);
|
||||
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;
|
||||
(<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;
|
||||
}
|
||||
|
||||
+7
-5
@@ -11,11 +11,13 @@
|
||||
*/
|
||||
|
||||
export function whenReady(fn) {
|
||||
if (document.readyState === "complete") {
|
||||
fn();
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", fn, false);
|
||||
}
|
||||
return new Promise(function(resolve) {
|
||||
if (document.readyState !== "loading") {
|
||||
resolve();
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", resolve, false);
|
||||
}
|
||||
}).then(fn || function () {});
|
||||
}
|
||||
|
||||
const loadedScripts: { [key: string]: Promise<void> } = {};
|
||||
|
||||
+30
-25
@@ -15,12 +15,15 @@
|
||||
* - patch function (to apply a vnode to an actual DOM node)
|
||||
*/
|
||||
|
||||
|
||||
// because those in TypeScript are too restrictive: https://github.com/Microsoft/TSJS-lib-generator/pull/237
|
||||
declare global {
|
||||
interface Element {
|
||||
setAttribute(name: string, value: string | number | boolean): void;
|
||||
setAttributeNS(namespaceURI: string, qualifiedName: string, value: string | number | boolean): void;
|
||||
setAttributeNS(
|
||||
namespaceURI: string,
|
||||
qualifiedName: string,
|
||||
value: string | number | boolean
|
||||
): void;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,14 +59,7 @@ function vnode(
|
||||
elm: Element | Text | undefined
|
||||
): VNode {
|
||||
let key = data === undefined ? undefined : data.key;
|
||||
return {
|
||||
sel: sel,
|
||||
data: data,
|
||||
children: children,
|
||||
text: text,
|
||||
elm: elm,
|
||||
key: key
|
||||
};
|
||||
return {sel, data, children, text, elm, key};
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -162,6 +158,7 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
|
||||
function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
|
||||
let i: any,
|
||||
iLen: number,
|
||||
data = vnode.data;
|
||||
if (data !== undefined) {
|
||||
if (isDef((i = data.hook)) && isDef((i = i.init))) {
|
||||
@@ -193,9 +190,10 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
if (hash < dot) elm.setAttribute("id", sel.slice(hash + 1, dot));
|
||||
if (dotIdx > 0)
|
||||
elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " "));
|
||||
for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
|
||||
for (i = 0, iLen = cbs.create.length; i < iLen; ++i)
|
||||
cbs.create[i](emptyNode, vnode);
|
||||
if (array(children)) {
|
||||
for (i = 0; i < children.length; ++i) {
|
||||
for (i = 0, iLen = children.length; i < iLen; ++i) {
|
||||
const ch = children[i];
|
||||
if (ch != null) {
|
||||
api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue));
|
||||
@@ -233,13 +231,16 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
|
||||
function invokeDestroyHook(vnode: VNode) {
|
||||
let i: any,
|
||||
iLen: number,
|
||||
j: number,
|
||||
jLen: number,
|
||||
data = vnode.data;
|
||||
if (data !== undefined) {
|
||||
if (isDef((i = data.hook)) && isDef((i = i.destroy))) i(vnode);
|
||||
for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
|
||||
for (i = 0, iLen = cbs.destroy.length; i < iLen; ++i)
|
||||
cbs.destroy[i](vnode);
|
||||
if (vnode.children !== undefined) {
|
||||
for (j = 0; j < vnode.children.length; ++j) {
|
||||
for (j = 0, jLen = vnode.children.length; j < jLen; ++j) {
|
||||
i = vnode.children[j];
|
||||
if (i != null && typeof i !== "string") {
|
||||
invokeDestroyHook(i);
|
||||
@@ -257,6 +258,7 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
): void {
|
||||
for (; startIdx <= endIdx; ++startIdx) {
|
||||
let i: any,
|
||||
iLen: number,
|
||||
listeners: number,
|
||||
rm: () => void,
|
||||
ch = vnodes[startIdx];
|
||||
@@ -265,7 +267,8 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
invokeDestroyHook(ch);
|
||||
listeners = cbs.remove.length + 1;
|
||||
rm = createRmCb(ch.elm as Node, listeners);
|
||||
for (i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);
|
||||
for (i = 0, iLen = cbs.remove.length; i < iLen; ++i)
|
||||
cbs.remove[i](ch, rm);
|
||||
if (
|
||||
isDef((i = ch.data)) &&
|
||||
isDef((i = i.hook)) &&
|
||||
@@ -395,7 +398,7 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
vnode: VNode,
|
||||
insertedVnodeQueue: VNodeQueue
|
||||
) {
|
||||
let i: any, hook: any;
|
||||
let i: any, iLen: number, hook: any;
|
||||
if (
|
||||
isDef((i = vnode.data)) &&
|
||||
isDef((hook = i.hook)) &&
|
||||
@@ -408,7 +411,8 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
let ch = vnode.children;
|
||||
if (oldVnode === vnode) return;
|
||||
if (vnode.data !== undefined) {
|
||||
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
|
||||
for (i = 0, iLen = cbs.update.length; i < iLen; ++i)
|
||||
cbs.update[i](oldVnode, vnode);
|
||||
i = vnode.data.hook;
|
||||
if (isDef(i) && isDef((i = i.update))) i(oldVnode, vnode);
|
||||
}
|
||||
@@ -458,9 +462,9 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
}
|
||||
|
||||
return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
|
||||
let i: number, elm: Node, parent: Node;
|
||||
let i: number, iLen: number, elm: Node, parent: Node;
|
||||
const insertedVnodeQueue: VNodeQueue = [];
|
||||
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
|
||||
for (i = 0, iLen = cbs.pre.length; i < iLen; ++i) cbs.pre[i]();
|
||||
|
||||
if (!isVnode(oldVnode)) {
|
||||
oldVnode = emptyNodeAt(oldVnode);
|
||||
@@ -480,12 +484,12 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < insertedVnodeQueue.length; ++i) {
|
||||
for (i = 0, iLen = insertedVnodeQueue.length; i < iLen; ++i) {
|
||||
(((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)(
|
||||
insertedVnodeQueue[i]
|
||||
);
|
||||
}
|
||||
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
|
||||
for (i = 0, iLen = cbs.post.length; i < iLen; ++i) cbs.post[i]();
|
||||
return vnode;
|
||||
};
|
||||
}
|
||||
@@ -647,7 +651,7 @@ function addNS(
|
||||
): void {
|
||||
data.ns = "http://www.w3.org/2000/svg";
|
||||
if (sel !== "foreignObject" && children !== undefined) {
|
||||
for (let i = 0; i < children.length; ++i) {
|
||||
for (let i = 0, iLen = children.length; i < iLen; ++i) {
|
||||
let childData = children[i].data;
|
||||
if (childData !== undefined) {
|
||||
addNS(
|
||||
@@ -668,7 +672,8 @@ export function h(sel: any, b?: any, c?: any): VNode {
|
||||
var data: VNodeData = {},
|
||||
children: any,
|
||||
text: any,
|
||||
i: number;
|
||||
i: number,
|
||||
iLen: number;
|
||||
if (c !== undefined) {
|
||||
data = b;
|
||||
if (array(c)) {
|
||||
@@ -690,7 +695,7 @@ export function h(sel: any, b?: any, c?: any): VNode {
|
||||
}
|
||||
}
|
||||
if (children !== undefined) {
|
||||
for (i = 0; i < children.length; ++i) {
|
||||
for (i = 0, iLen = children.length; i < iLen; ++i) {
|
||||
if (primitive(children[i]))
|
||||
children[i] = vnode(
|
||||
undefined,
|
||||
@@ -788,7 +793,7 @@ function invokeHandler(handler: any, vnode?: VNode, event?: Event): void {
|
||||
}
|
||||
} else {
|
||||
// call multiple handlers
|
||||
for (var i = 0; i < handler.length; i++) {
|
||||
for (let i = 0, iLen = handler.length; i < iLen; i++) {
|
||||
invokeHandler(handler[i], vnode, event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`animations t-transition combined with t-widget 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let utils = this.utils;
|
||||
let QWeb = this.constructor;
|
||||
let owner = context;
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
//WIDGET
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let def3;
|
||||
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
|
||||
let props4 = {};
|
||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
||||
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
||||
def3 = w4.__owl__.renderPromise;
|
||||
} else {
|
||||
w4.destroy();
|
||||
w4 = false;
|
||||
}
|
||||
}
|
||||
if (!w4) {
|
||||
let widgetKey4 = \`Child\`;
|
||||
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
|
||||
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
|
||||
w4 = new W4(owner, props4);
|
||||
context.__owl__.cmap[4] = w4.__owl__.id;
|
||||
def3 = w4._prepare();
|
||||
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
|
||||
w4.destroy();
|
||||
};
|
||||
utils.transitionRemove(vn, 'chimay', finalize);}}});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[`animations t-transition combined with t-widget and t-if 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let utils = this.utils;
|
||||
let QWeb = this.constructor;
|
||||
let owner = context;
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
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) {
|
||||
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
||||
def3 = w4.__owl__.renderPromise;
|
||||
} else {
|
||||
w4.destroy();
|
||||
w4 = false;
|
||||
}
|
||||
}
|
||||
if (!w4) {
|
||||
let widgetKey4 = \`Child\`;
|
||||
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
|
||||
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
|
||||
w4 = new W4(owner, props4);
|
||||
context.__owl__.cmap[4] = w4.__owl__.id;
|
||||
def3 = w4._prepare();
|
||||
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
|
||||
w4.destroy();
|
||||
};
|
||||
utils.transitionRemove(vn, 'chimay', finalize);}}});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[`animations t-transition with no delay/duration 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('span', p1, c1);
|
||||
p1.hook = {
|
||||
insert: vn => {
|
||||
this.utils.transitionInsert(vn, 'jupiler');
|
||||
},
|
||||
remove: (vn, rm) => {
|
||||
this.utils.transitionRemove(vn, 'jupiler', rm);
|
||||
},
|
||||
};
|
||||
c1.push({text: \`blue\`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`animations t-transition, on a simple node (insert) 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('span', p1, c1);
|
||||
p1.hook = {
|
||||
insert: vn => {
|
||||
this.utils.transitionInsert(vn, 'chimay');
|
||||
},
|
||||
remove: (vn, rm) => {
|
||||
this.utils.transitionRemove(vn, 'chimay', rm);
|
||||
},
|
||||
};
|
||||
c1.push({text: \`blue\`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
@@ -3,6 +3,8 @@
|
||||
exports[`class and style attributes with t-widget dynamic t-att-style is properly added and updated on widget root el 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let utils = this.utils;
|
||||
let QWeb = this.constructor;
|
||||
let owner = context;
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
@@ -14,19 +16,24 @@ exports[`class and style attributes with t-widget dynamic t-att-style is properl
|
||||
const _5 = context['state'].style;
|
||||
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
|
||||
let props4 = {};
|
||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
|
||||
w4.destroy();
|
||||
w4 = false
|
||||
if (w4 && 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 W4 = context.widgets['child'];
|
||||
if (!W4) {throw new Error(\`Cannot find the definition of widget \\"child\\"\`)}
|
||||
let widgetKey4 = \`child\`;
|
||||
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
|
||||
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
|
||||
w4 = new W4(owner, props4);
|
||||
context.__owl__.cmap[4] = w4.__owl__.id;
|
||||
def3 = w4._prepare();
|
||||
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.style = _5}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn){let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove(){w4.destroy()},destroy(){w4.destroy()}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
||||
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.style = _5;}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
||||
} else {
|
||||
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
||||
def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
||||
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};w4.el.style=_5;let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
||||
}
|
||||
extra.promises.push(def3);
|
||||
@@ -37,6 +44,8 @@ exports[`class and style attributes with t-widget dynamic t-att-style is properl
|
||||
exports[`class and style attributes with t-widget t-att-class is properly added/removed on widget root el 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let utils = this.utils;
|
||||
let QWeb = this.constructor;
|
||||
let owner = context;
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
@@ -45,16 +54,21 @@ exports[`class and style attributes with t-widget t-att-class is properly added/
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let def3;
|
||||
const _5 = {a: context['state'].a,b: context['state'].b};
|
||||
const _5 = {a:context['state'].a,b:context['state'].b};
|
||||
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
|
||||
let props4 = {};
|
||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
|
||||
w4.destroy();
|
||||
w4 = false
|
||||
if (w4 && 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 W4 = context.widgets['child'];
|
||||
if (!W4) {throw new Error(\`Cannot find the definition of widget \\"child\\"\`)}
|
||||
let widgetKey4 = \`child\`;
|
||||
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
|
||||
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
|
||||
w4 = new W4(owner, props4);
|
||||
context.__owl__.cmap[4] = w4.__owl__.id;
|
||||
def3 = w4._prepare();
|
||||
@@ -62,9 +76,9 @@ exports[`class and style attributes with t-widget t-att-class is properly added/
|
||||
if (_5[k]) {
|
||||
vn.elm.classList.add(k);
|
||||
}
|
||||
}}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn){let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove(){w4.destroy()},destroy(){w4.destroy()}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
||||
}}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
||||
} else {
|
||||
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
||||
def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
||||
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let cl=w4.el.classList;for (let k in _5) {if (_5[k]) {cl.add(k)} else {cl.remove(k)}}let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
||||
}
|
||||
extra.promises.push(def3);
|
||||
@@ -75,6 +89,8 @@ exports[`class and style attributes with t-widget t-att-class is properly added/
|
||||
exports[`composition sub widgets with some state rendered in a loop 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let utils = this.utils;
|
||||
let QWeb = this.constructor;
|
||||
let owner = context;
|
||||
context = Object.create(context);
|
||||
var h = this.utils.h;
|
||||
@@ -82,13 +98,15 @@ exports[`composition sub widgets with some state rendered in a loop 1`] = `
|
||||
var vn1 = h('div', p1, c1);
|
||||
var _2 = context['state'].numbers;
|
||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||
if (typeof _2 === 'number') { _2 = Array.from(Array(_2).keys())}
|
||||
var _3 = _2 instanceof Array ? _2 : Object.keys(_2);
|
||||
var _4 = _2 instanceof Array ? _2 : Object.values(_2);
|
||||
for (let i = 0; i < _3.length; i++) {
|
||||
var _3 = _4 = _2;
|
||||
if (!(_2 instanceof Array)) {
|
||||
_3 = Object.keys(_2);
|
||||
_4 = Object.values(_2);
|
||||
}
|
||||
var _length3 = _3.length;
|
||||
for (let i = 0; i < _length3; i++) {
|
||||
context.number_first = i === 0;
|
||||
context.number_last = i === _3.length - 1;
|
||||
context.number_parity = i % 2 === 0 ? 'even' : 'odd';
|
||||
context.number_last = i === _length3 - 1;
|
||||
context.number_index = i;
|
||||
context.number = _3[i];
|
||||
context.number_value = _4[i];
|
||||
@@ -99,19 +117,24 @@ exports[`composition sub widgets with some state rendered in a loop 1`] = `
|
||||
let def6;
|
||||
let w7 = key8 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[key8]] : false;
|
||||
let props7 = {};
|
||||
if (w7 && w7.__owl__.renderPromise && !w7.__owl__.vnode && props7 !== w7.__owl__.renderProps) {
|
||||
w7.destroy();
|
||||
w7 = false
|
||||
if (w7 && 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 W7 = context.widgets['ChildWidget'];
|
||||
if (!W7) {throw new Error(\`Cannot find the definition of widget \\"ChildWidget\\"\`)}
|
||||
let widgetKey7 = \`ChildWidget\`;
|
||||
let W7 = context.widgets && context.widgets[widgetKey7] || QWeb.widgets[widgetKey7];
|
||||
if (!W7) {throw new Error('Cannot find the definition of widget \\"' + widgetKey7 + '\\"')}
|
||||
w7 = new W7(owner, props7);
|
||||
context.__owl__.cmap[key8] = w7.__owl__.id;
|
||||
def6 = w7._prepare();
|
||||
def6 = def6.then(vnode=>{let pvnode=h(vnode.sel, {key: key8, hook: {insert(vn){let nvn=w7._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove(){w7.destroy()},destroy(){w7.destroy()}}});c1[_5_index]=pvnode;w7.__owl__.pvnode = pvnode;});
|
||||
def6 = def6.then(vnode=>{let pvnode=h(vnode.sel, {key: key8, hook: {insert(vn) {let nvn=w7._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w7.destroy();}}});c1[_5_index]=pvnode;w7.__owl__.pvnode = pvnode;});
|
||||
} else {
|
||||
def6 = w7._updateProps(props7, extra.forceUpdate, extra.patchQueue);
|
||||
def6 = def6 || 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);
|
||||
@@ -120,43 +143,11 @@ exports[`composition sub widgets with some state rendered in a loop 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let owner = context;
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
//WIDGET
|
||||
let key5 = 'somestring';
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let def3;
|
||||
let w4 = key5 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[key5]] : false;
|
||||
let props4 = {flag: context['state'].flag};
|
||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
|
||||
w4.destroy();
|
||||
w4 = false
|
||||
}
|
||||
if (!w4) {
|
||||
let W4 = context.widgets['child'];
|
||||
if (!W4) {throw new Error(\`Cannot find the definition of widget \\"child\\"\`)}
|
||||
w4 = new W4(owner, props4);
|
||||
context.__owl__.cmap[key5] = w4.__owl__.id;
|
||||
def3 = w4._prepare();
|
||||
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: key5, hook: {insert(vn){let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove(){w4.destroy()},destroy(){w4.destroy()}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
||||
} else {
|
||||
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
||||
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
||||
}
|
||||
extra.promises.push(def3);
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`random stuff/miscellaneous t-props should not be undefined (snapshotting) 1`] = `
|
||||
exports[`composition t-widget with dynamic value 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let utils = this.utils;
|
||||
let QWeb = this.constructor;
|
||||
let owner = context;
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
@@ -167,22 +158,489 @@ exports[`random stuff/miscellaneous t-props should not be undefined (snapshottin
|
||||
let def3;
|
||||
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
|
||||
let props4 = {};
|
||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
|
||||
w4.destroy();
|
||||
w4 = false
|
||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
||||
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
||||
def3 = w4.__owl__.renderPromise;
|
||||
} else {
|
||||
w4.destroy();
|
||||
w4 = false;
|
||||
}
|
||||
}
|
||||
if (!w4) {
|
||||
let W4 = context.widgets['child'];
|
||||
if (!W4) {throw new Error(\`Cannot find the definition of widget \\"child\\"\`)}
|
||||
let widgetKey4 = (context['state'].widget);
|
||||
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
|
||||
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
|
||||
w4 = new W4(owner, props4);
|
||||
context.__owl__.cmap[4] = w4.__owl__.id;
|
||||
def3 = w4._prepare();
|
||||
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn){let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove(){w4.destroy()},destroy(){w4.destroy()}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
||||
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
||||
} else {
|
||||
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
||||
def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
||||
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
||||
}
|
||||
extra.promises.push(def3);
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`composition t-widget with dynamic value 2 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let utils = this.utils;
|
||||
let QWeb = this.constructor;
|
||||
let owner = context;
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
//WIDGET
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let def3;
|
||||
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
|
||||
let props4 = {};
|
||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
||||
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
||||
def3 = w4.__owl__.renderPromise;
|
||||
} else {
|
||||
w4.destroy();
|
||||
w4 = false;
|
||||
}
|
||||
}
|
||||
if (!w4) {
|
||||
let widgetKey4 = \`Widget\${context['state'].widget}\`;
|
||||
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
|
||||
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
|
||||
w4 = new W4(owner, props4);
|
||||
context.__owl__.cmap[4] = w4.__owl__.id;
|
||||
def3 = w4._prepare();
|
||||
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
||||
} else {
|
||||
def3 = 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[`other directives with t-widget t-on with handler bound to argument 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let utils = this.utils;
|
||||
let QWeb = this.constructor;
|
||||
let owner = context;
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
//WIDGET
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let def3;
|
||||
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
|
||||
let props4 = {};
|
||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
||||
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
||||
def3 = w4.__owl__.renderPromise;
|
||||
} else {
|
||||
w4.destroy();
|
||||
w4 = false;
|
||||
}
|
||||
}
|
||||
if (!w4) {
|
||||
let widgetKey4 = \`child\`;
|
||||
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
|
||||
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
|
||||
w4 = new W4(owner, props4);
|
||||
context.__owl__.cmap[4] = w4.__owl__.id;
|
||||
def3 = w4._prepare();
|
||||
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', owner['onEv'].bind(owner, 3));}};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[`other directives with t-widget t-on with handler bound to empty object (with non empty inner string) 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let utils = this.utils;
|
||||
let QWeb = this.constructor;
|
||||
let owner = context;
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
//WIDGET
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let def3;
|
||||
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
|
||||
let props4 = {};
|
||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
||||
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
||||
def3 = w4.__owl__.renderPromise;
|
||||
} else {
|
||||
w4.destroy();
|
||||
w4 = false;
|
||||
}
|
||||
}
|
||||
if (!w4) {
|
||||
let widgetKey4 = \`child\`;
|
||||
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
|
||||
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
|
||||
w4 = new W4(owner, props4);
|
||||
context.__owl__.cmap[4] = w4.__owl__.id;
|
||||
def3 = w4._prepare();
|
||||
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', owner['onEv'].bind(owner, {}));}};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[`other directives with t-widget t-on with handler bound to empty object 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let utils = this.utils;
|
||||
let QWeb = this.constructor;
|
||||
let owner = context;
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
//WIDGET
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let def3;
|
||||
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
|
||||
let props4 = {};
|
||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
||||
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
||||
def3 = w4.__owl__.renderPromise;
|
||||
} else {
|
||||
w4.destroy();
|
||||
w4 = false;
|
||||
}
|
||||
}
|
||||
if (!w4) {
|
||||
let widgetKey4 = \`child\`;
|
||||
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
|
||||
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
|
||||
w4 = new W4(owner, props4);
|
||||
context.__owl__.cmap[4] = w4.__owl__.id;
|
||||
def3 = w4._prepare();
|
||||
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', owner['onEv'].bind(owner, {}));}};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[`other directives with t-widget t-on with handler bound to object 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let utils = this.utils;
|
||||
let QWeb = this.constructor;
|
||||
let owner = context;
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
//WIDGET
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let def3;
|
||||
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
|
||||
let props4 = {};
|
||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
||||
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
||||
def3 = w4.__owl__.renderPromise;
|
||||
} else {
|
||||
w4.destroy();
|
||||
w4 = false;
|
||||
}
|
||||
}
|
||||
if (!w4) {
|
||||
let widgetKey4 = \`child\`;
|
||||
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
|
||||
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
|
||||
w4 = new W4(owner, props4);
|
||||
context.__owl__.cmap[4] = w4.__owl__.id;
|
||||
def3 = w4._prepare();
|
||||
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', owner['onEv'].bind(owner, {val:3}));}};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[`other directives with t-widget t-on with prevent and self modifiers (order matters) 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let utils = this.utils;
|
||||
let QWeb = this.constructor;
|
||||
let owner = context;
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
//WIDGET
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let def3;
|
||||
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
|
||||
let props4 = {};
|
||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
||||
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
||||
def3 = w4.__owl__.renderPromise;
|
||||
} else {
|
||||
w4.destroy();
|
||||
w4 = false;
|
||||
}
|
||||
}
|
||||
if (!w4) {
|
||||
let widgetKey4 = \`child\`;
|
||||
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
|
||||
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
|
||||
w4 = new W4(owner, props4);
|
||||
context.__owl__.cmap[4] = w4.__owl__.id;
|
||||
def3 = w4._prepare();
|
||||
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {e.preventDefault();if (e.target !== vn.elm) {return}owner['onEv'].call(owner, e);});}};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[`other directives with t-widget t-on with self and prevent modifiers (order matters) 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let utils = this.utils;
|
||||
let QWeb = this.constructor;
|
||||
let owner = context;
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
//WIDGET
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let def3;
|
||||
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
|
||||
let props4 = {};
|
||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
||||
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
||||
def3 = w4.__owl__.renderPromise;
|
||||
} else {
|
||||
w4.destroy();
|
||||
w4 = false;
|
||||
}
|
||||
}
|
||||
if (!w4) {
|
||||
let widgetKey4 = \`child\`;
|
||||
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
|
||||
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
|
||||
w4 = new W4(owner, props4);
|
||||
context.__owl__.cmap[4] = w4.__owl__.id;
|
||||
def3 = w4._prepare();
|
||||
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (e.target !== vn.elm) {return}e.preventDefault();owner['onEv'].call(owner, e);});}};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[`other directives with t-widget t-on with self modifier 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let utils = this.utils;
|
||||
let QWeb = this.constructor;
|
||||
let owner = context;
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
//WIDGET
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let def3;
|
||||
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
|
||||
let props4 = {};
|
||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
||||
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
||||
def3 = w4.__owl__.renderPromise;
|
||||
} else {
|
||||
w4.destroy();
|
||||
w4 = false;
|
||||
}
|
||||
}
|
||||
if (!w4) {
|
||||
let widgetKey4 = \`child\`;
|
||||
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
|
||||
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
|
||||
w4 = new W4(owner, props4);
|
||||
context.__owl__.cmap[4] = w4.__owl__.id;
|
||||
def3 = w4._prepare();
|
||||
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev-1', owner['onEv1'].bind(owner));vn.elm.addEventListener('ev-2', function (e) {if (e.target !== vn.elm) {return}owner['onEv2'].call(owner, e);});}};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[`other directives with t-widget t-on with stop and/or prevent modifiers 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let utils = this.utils;
|
||||
let QWeb = this.constructor;
|
||||
let owner = context;
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
//WIDGET
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let def3;
|
||||
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
|
||||
let props4 = {};
|
||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
||||
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
||||
def3 = w4.__owl__.renderPromise;
|
||||
} else {
|
||||
w4.destroy();
|
||||
w4 = false;
|
||||
}
|
||||
}
|
||||
if (!w4) {
|
||||
let widgetKey4 = \`child\`;
|
||||
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
|
||||
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
|
||||
w4 = new W4(owner, props4);
|
||||
context.__owl__.cmap[4] = w4.__owl__.id;
|
||||
def3 = w4._prepare();
|
||||
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev-1', function (e) {e.stopPropagation();owner['onEv1'].call(owner, e);});vn.elm.addEventListener('ev-2', function (e) {e.preventDefault();owner['onEv2'].call(owner, e);});vn.elm.addEventListener('ev-3', function (e) {e.stopPropagation();e.preventDefault();owner['onEv3'].call(owner, e);});}};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[`random stuff/miscellaneous snapshotting compiled code 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let utils = this.utils;
|
||||
let QWeb = this.constructor;
|
||||
let owner = context;
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
//WIDGET
|
||||
let key5 = 'somestring';
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let def3;
|
||||
let w4 = key5 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[key5]] : false;
|
||||
let props4 = {flag:context['state'].flag};
|
||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
||||
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
||||
def3 = w4.__owl__.renderPromise;
|
||||
} else {
|
||||
w4.destroy();
|
||||
w4 = false;
|
||||
}
|
||||
}
|
||||
if (!w4) {
|
||||
let widgetKey4 = \`child\`;
|
||||
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
|
||||
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
|
||||
w4 = new W4(owner, props4);
|
||||
context.__owl__.cmap[key5] = w4.__owl__.id;
|
||||
def3 = w4._prepare();
|
||||
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: key5, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
||||
} else {
|
||||
def3 = 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[`t-slot directive can define and call slots 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let utils = this.utils;
|
||||
let QWeb = this.constructor;
|
||||
let owner = context;
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
//WIDGET
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let def3;
|
||||
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
|
||||
let props4 = {};
|
||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
||||
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
||||
def3 = w4.__owl__.renderPromise;
|
||||
} else {
|
||||
w4.destroy();
|
||||
w4 = false;
|
||||
}
|
||||
}
|
||||
if (!w4) {
|
||||
let widgetKey4 = \`Dialog\`;
|
||||
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
|
||||
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
|
||||
w4 = new W4(owner, props4);
|
||||
context.__owl__.cmap[4] = w4.__owl__.id;
|
||||
w4.__owl__.slotId = 1;
|
||||
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[`t-slot directive can define and call slots 2`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
let c2 = [], p2 = {key:2};
|
||||
var vn2 = h('div', p2, c2);
|
||||
c1.push(vn2);
|
||||
const slot3 = this.slots[context.__owl__.slotId + '_' + 'header'];
|
||||
c2.push(slot3(context.__owl__.parent, extra));
|
||||
let c4 = [], p4 = {key:4};
|
||||
var vn4 = h('div', p4, c4);
|
||||
c1.push(vn4);
|
||||
const slot5 = this.slots[context.__owl__.slotId + '_' + 'footer'];
|
||||
c4.push(slot5(context.__owl__.parent, extra));
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
@@ -1,32 +1,16 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`animations t-transition, on a simple node 1`] = `
|
||||
exports[`attributes class and t-attf-class with ternary operation 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
let c2 = [], p2 = {key:2};
|
||||
var vn2 = h('span', p2, c2);
|
||||
c1.push(vn2);
|
||||
p2.hook = {
|
||||
create: (_, n) => {
|
||||
this.utils.transitionCreate(n.elm, 'chimay');
|
||||
},
|
||||
insert: vn => {
|
||||
this.utils.transitionInsert(vn.elm, 'chimay');
|
||||
},
|
||||
remove: (vn, rm) => {
|
||||
this.utils.transitionRemove(vn.elm, 'chimay', rm);
|
||||
},
|
||||
};
|
||||
c2.push({text: \`blue\`});
|
||||
return vn1;
|
||||
var _1 = 'hello ' + (context['value']?'world':'');
|
||||
let c2 = [], p2 = {key:2,attrs:{class: _1}};
|
||||
var vn2 = h('div', p2, c2);
|
||||
return vn2;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`animations t-transition, on a simple node 2`] = `"<span class=\\"chimay-enter chimay-enter-active\\">blue</span>"`;
|
||||
|
||||
exports[`attributes dynamic attribute falsy variable 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
@@ -86,7 +70,7 @@ exports[`attributes format expression 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
var h = this.utils.h;
|
||||
var _1 = \`\${context['value'] + 37}\`;
|
||||
var _1 = (context['value']+37);
|
||||
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
|
||||
var vn2 = h('div', p2, c2);
|
||||
return vn2;
|
||||
@@ -97,7 +81,7 @@ exports[`attributes format expression, other format 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
var h = this.utils.h;
|
||||
var _1 = \`\${context['value'] + 37}\`;
|
||||
var _1 = (context['value']+37);
|
||||
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
|
||||
var vn2 = h('div', p2, c2);
|
||||
return vn2;
|
||||
@@ -143,7 +127,7 @@ exports[`attributes from object variables set previously 1`] = `
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
var _2 = {a: 'b'};
|
||||
var _2 = {a:'b'};
|
||||
var _3 = _2.a;
|
||||
let c4 = [], p4 = {key:4,attrs:{class: _3}};
|
||||
var vn4 = h('span', p4, c4);
|
||||
@@ -239,7 +223,7 @@ exports[`attributes t-att-class with object 1`] = `
|
||||
) {
|
||||
var h = this.utils.h;
|
||||
var _1 = 'static';
|
||||
var _3 = this.utils.objectToAttrString({a: context['b'],c: context['d'],e: context['f']});
|
||||
var _3 = this.utils.objectToAttrString({a:context['b'],c:context['d'],e:context['f']});
|
||||
var _2 = 'static' + (_3 ? ' ' + _3 : '');
|
||||
let c4 = [], p4 = {key:4,attrs:{class: _2}};
|
||||
var vn4 = h('div', p4, c4);
|
||||
@@ -262,7 +246,7 @@ exports[`attributes tuple literal 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
var h = this.utils.h;
|
||||
var _1 = ['foo', 'bar'];
|
||||
var _1 = ['foo','bar'];
|
||||
let c2 = [], p2 = {key:2,attrs:{}};
|
||||
if (_1 instanceof Array) {
|
||||
p2.attrs[_1[0]] = _1[1];
|
||||
@@ -319,7 +303,7 @@ exports[`debugging t-log 1`] = `
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
var _2 = 42;
|
||||
console.log(_2 + 3)
|
||||
console.log(_2+3)
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
@@ -333,13 +317,15 @@ exports[`foreach does not pollute the rendering context 1`] = `
|
||||
var vn1 = h('div', p1, c1);
|
||||
var _2 = [1];
|
||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||
if (typeof _2 === 'number') { _2 = Array.from(Array(_2).keys())}
|
||||
var _3 = _2 instanceof Array ? _2 : Object.keys(_2);
|
||||
var _4 = _2 instanceof Array ? _2 : Object.values(_2);
|
||||
for (let i = 0; i < _3.length; i++) {
|
||||
var _3 = _4 = _2;
|
||||
if (!(_2 instanceof Array)) {
|
||||
_3 = Object.keys(_2);
|
||||
_4 = Object.values(_2);
|
||||
}
|
||||
var _length3 = _3.length;
|
||||
for (let i = 0; i < _length3; i++) {
|
||||
context.item_first = i === 0;
|
||||
context.item_last = i === _3.length - 1;
|
||||
context.item_parity = i % 2 === 0 ? 'even' : 'odd';
|
||||
context.item_last = i === _length3 - 1;
|
||||
context.item_index = i;
|
||||
context.item = _3[i];
|
||||
context.item_value = _4[i];
|
||||
@@ -359,15 +345,17 @@ exports[`foreach iterate on items (on a element node) 1`] = `
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
var _2 = [1, 2];
|
||||
var _2 = [1,2];
|
||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||
if (typeof _2 === 'number') { _2 = Array.from(Array(_2).keys())}
|
||||
var _3 = _2 instanceof Array ? _2 : Object.keys(_2);
|
||||
var _4 = _2 instanceof Array ? _2 : Object.values(_2);
|
||||
for (let i = 0; i < _3.length; i++) {
|
||||
var _3 = _4 = _2;
|
||||
if (!(_2 instanceof Array)) {
|
||||
_3 = Object.keys(_2);
|
||||
_4 = Object.values(_2);
|
||||
}
|
||||
var _length3 = _3.length;
|
||||
for (let i = 0; i < _length3; i++) {
|
||||
context.item_first = i === 0;
|
||||
context.item_last = i === _3.length - 1;
|
||||
context.item_parity = i % 2 === 0 ? 'even' : 'odd';
|
||||
context.item_last = i === _length3 - 1;
|
||||
context.item_index = i;
|
||||
context.item = _3[i];
|
||||
context.item_value = _4[i];
|
||||
@@ -390,15 +378,17 @@ exports[`foreach iterate on items 1`] = `
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
var _2 = [3, 2, 1];
|
||||
var _2 = [3,2,1];
|
||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||
if (typeof _2 === 'number') { _2 = Array.from(Array(_2).keys())}
|
||||
var _3 = _2 instanceof Array ? _2 : Object.keys(_2);
|
||||
var _4 = _2 instanceof Array ? _2 : Object.values(_2);
|
||||
for (let i = 0; i < _3.length; i++) {
|
||||
var _3 = _4 = _2;
|
||||
if (!(_2 instanceof Array)) {
|
||||
_3 = Object.keys(_2);
|
||||
_4 = Object.values(_2);
|
||||
}
|
||||
var _length3 = _3.length;
|
||||
for (let i = 0; i < _length3; i++) {
|
||||
context.item_first = i === 0;
|
||||
context.item_last = i === _3.length - 1;
|
||||
context.item_parity = i % 2 === 0 ? 'even' : 'odd';
|
||||
context.item_last = i === _length3 - 1;
|
||||
context.item_index = i;
|
||||
context.item = _3[i];
|
||||
context.item_value = _4[i];
|
||||
@@ -432,58 +422,15 @@ exports[`foreach iterate, dict param 1`] = `
|
||||
var vn1 = h('div', p1, c1);
|
||||
var _2 = context['value'];
|
||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||
if (typeof _2 === 'number') { _2 = Array.from(Array(_2).keys())}
|
||||
var _3 = _2 instanceof Array ? _2 : Object.keys(_2);
|
||||
var _4 = _2 instanceof Array ? _2 : Object.values(_2);
|
||||
for (let i = 0; i < _3.length; i++) {
|
||||
context.item_first = i === 0;
|
||||
context.item_last = i === _3.length - 1;
|
||||
context.item_parity = i % 2 === 0 ? 'even' : 'odd';
|
||||
context.item_index = i;
|
||||
context.item = _3[i];
|
||||
context.item_value = _4[i];
|
||||
c1.push({text: \` [\`});
|
||||
var _5 = context['item_index'];
|
||||
if (_5 || _5 === 0) {
|
||||
c1.push({text: _5});
|
||||
}
|
||||
c1.push({text: \`: \`});
|
||||
var _6 = context['item'];
|
||||
if (_6 || _6 === 0) {
|
||||
c1.push({text: _6});
|
||||
}
|
||||
c1.push({text: \` \`});
|
||||
var _7 = context['item_value'];
|
||||
if (_7 || _7 === 0) {
|
||||
c1.push({text: _7});
|
||||
}
|
||||
c1.push({text: \` - \`});
|
||||
var _8 = context['item_parity'];
|
||||
if (_8 || _8 === 0) {
|
||||
c1.push({text: _8});
|
||||
}
|
||||
c1.push({text: \`] \`});
|
||||
var _3 = _4 = _2;
|
||||
if (!(_2 instanceof Array)) {
|
||||
_3 = Object.keys(_2);
|
||||
_4 = Object.values(_2);
|
||||
}
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`foreach iterate, integer param 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
context = Object.create(context);
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
var _2 = 3;
|
||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||
if (typeof _2 === 'number') { _2 = Array.from(Array(_2).keys())}
|
||||
var _3 = _2 instanceof Array ? _2 : Object.keys(_2);
|
||||
var _4 = _2 instanceof Array ? _2 : Object.values(_2);
|
||||
for (let i = 0; i < _3.length; i++) {
|
||||
var _length3 = _3.length;
|
||||
for (let i = 0; i < _length3; i++) {
|
||||
context.item_first = i === 0;
|
||||
context.item_last = i === _3.length - 1;
|
||||
context.item_parity = i % 2 === 0 ? 'even' : 'odd';
|
||||
context.item_last = i === _length3 - 1;
|
||||
context.item_index = i;
|
||||
context.item = _3[i];
|
||||
context.item_value = _4[i];
|
||||
@@ -515,15 +462,17 @@ exports[`foreach iterate, position 1`] = `
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
var _2 = 5;
|
||||
var _2 = Array(5);
|
||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||
if (typeof _2 === 'number') { _2 = Array.from(Array(_2).keys())}
|
||||
var _3 = _2 instanceof Array ? _2 : Object.keys(_2);
|
||||
var _4 = _2 instanceof Array ? _2 : Object.values(_2);
|
||||
for (let i = 0; i < _3.length; i++) {
|
||||
var _3 = _4 = _2;
|
||||
if (!(_2 instanceof Array)) {
|
||||
_3 = Object.keys(_2);
|
||||
_4 = Object.values(_2);
|
||||
}
|
||||
var _length3 = _3.length;
|
||||
for (let i = 0; i < _length3; i++) {
|
||||
context.elem_first = i === 0;
|
||||
context.elem_last = i === _3.length - 1;
|
||||
context.elem_parity = i % 2 === 0 ? 'even' : 'odd';
|
||||
context.elem_last = i === _length3 - 1;
|
||||
context.elem_index = i;
|
||||
context.elem = _3[i];
|
||||
context.elem_value = _4[i];
|
||||
@@ -535,7 +484,7 @@ exports[`foreach iterate, position 1`] = `
|
||||
c1.push({text: \` last\`});
|
||||
}
|
||||
c1.push({text: \` (\`});
|
||||
var _5 = context['elem_parity'];
|
||||
var _5 = context['elem_index'];
|
||||
if (_5 || _5 === 0) {
|
||||
c1.push({text: _5});
|
||||
}
|
||||
@@ -552,15 +501,17 @@ exports[`foreach warn if no key in some case 1`] = `
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
var _2 = [1, 2];
|
||||
var _2 = [1,2];
|
||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||
if (typeof _2 === 'number') { _2 = Array.from(Array(_2).keys())}
|
||||
var _3 = _2 instanceof Array ? _2 : Object.keys(_2);
|
||||
var _4 = _2 instanceof Array ? _2 : Object.values(_2);
|
||||
for (let i = 0; i < _3.length; i++) {
|
||||
var _3 = _4 = _2;
|
||||
if (!(_2 instanceof Array)) {
|
||||
_3 = Object.keys(_2);
|
||||
_4 = Object.values(_2);
|
||||
}
|
||||
var _length3 = _3.length;
|
||||
for (let i = 0; i < _length3; i++) {
|
||||
context.item_first = i === 0;
|
||||
context.item_last = i === _3.length - 1;
|
||||
context.item_parity = i % 2 === 0 ? 'even' : 'odd';
|
||||
context.item_last = i === _length3 - 1;
|
||||
context.item_index = i;
|
||||
context.item = _3[i];
|
||||
context.item_value = _4[i];
|
||||
@@ -614,13 +565,15 @@ exports[`misc global 1`] = `
|
||||
var vn1 = h('div', p1, c1);
|
||||
var _2 = [4,5,6];
|
||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||
if (typeof _2 === 'number') { _2 = Array.from(Array(_2).keys())}
|
||||
var _3 = _2 instanceof Array ? _2 : Object.keys(_2);
|
||||
var _4 = _2 instanceof Array ? _2 : Object.values(_2);
|
||||
for (let i = 0; i < _3.length; i++) {
|
||||
var _3 = _4 = _2;
|
||||
if (!(_2 instanceof Array)) {
|
||||
_3 = Object.keys(_2);
|
||||
_4 = Object.values(_2);
|
||||
}
|
||||
var _length3 = _3.length;
|
||||
for (let i = 0; i < _length3; i++) {
|
||||
context.value_first = i === 0;
|
||||
context.value_last = i === _3.length - 1;
|
||||
context.value_parity = i % 2 === 0 ? 'even' : 'odd';
|
||||
context.value_last = i === _length3 - 1;
|
||||
context.value_index = i;
|
||||
context.value = _3[i];
|
||||
context.value_value = _4[i];
|
||||
@@ -632,13 +585,13 @@ exports[`misc global 1`] = `
|
||||
c5.push({text: _6});
|
||||
}
|
||||
{
|
||||
let _11 = 'bbb'
|
||||
let _11 = 'bbb';
|
||||
var _13 = 'agüero';
|
||||
let c14 = [], p14 = {key:14,attrs:{\\"falló\\": _13},on:{}};
|
||||
var vn14 = h('Año', p14, c14);
|
||||
c1.push(vn14);
|
||||
{
|
||||
let _15 = 'aaa'
|
||||
let _15 = 'aaa';
|
||||
let c16 = [], p16 = {key:16,on:{}};
|
||||
var vn16 = h('span', p16, c16);
|
||||
c14.push(vn16);
|
||||
@@ -658,16 +611,13 @@ exports[`misc global 1`] = `
|
||||
c17.push({text: \`foo default\`});
|
||||
}
|
||||
var _19 = 'bbb';
|
||||
{
|
||||
let _19 = 'bbb'
|
||||
let c20 = [], p20 = {key:20,on:{}};
|
||||
var vn20 = h('span', p20, c20);
|
||||
c14.push(vn20);
|
||||
if (_19 || _19 === 0) {
|
||||
c20.push({text: _19});
|
||||
} else {
|
||||
c20.push({text: \`foo default\`});
|
||||
}
|
||||
let c20 = [], p20 = {key:20,on:{}};
|
||||
var vn20 = h('span', p20, c20);
|
||||
c14.push(vn20);
|
||||
if (_19 || _19 === 0) {
|
||||
c20.push({text: _19});
|
||||
} else {
|
||||
c20.push({text: \`foo default\`});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -776,6 +726,18 @@ exports[`static templates empty div 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`static templates simple dynamic value 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
var h = this.utils.h;
|
||||
var _1 = context['text'];
|
||||
if (_1 || _1 === 0) {
|
||||
var vn2 = {text: _1};
|
||||
}
|
||||
return vn2;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`static templates simple string 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
@@ -785,6 +747,19 @@ exports[`static templates simple string 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`static templates simple string, with some dynamic value 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
var h = this.utils.h;
|
||||
var vn1 = {text: \`hello \`};
|
||||
var _2 = context['text'];
|
||||
if (_2 || _2 === 0) {
|
||||
vn1.text += _2;
|
||||
}
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-call (template calling basic caller 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
@@ -803,11 +778,8 @@ exports[`t-call (template calling inherit context 1`] = `
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
var _2 = 1;
|
||||
{
|
||||
let _2 = 1
|
||||
if (_2 || _2 === 0) {
|
||||
c1.push({text: _2});
|
||||
}
|
||||
if (_2 || _2 === 0) {
|
||||
c1.push({text: _2});
|
||||
}
|
||||
return vn1;
|
||||
}"
|
||||
@@ -820,7 +792,7 @@ exports[`t-call (template calling scoped parameters 1`] = `
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
{
|
||||
let _2 = 42
|
||||
let _2 = 42;
|
||||
c1.push({text: \`ok\`});
|
||||
}
|
||||
var _3 = context['foo'];
|
||||
@@ -847,7 +819,7 @@ exports[`t-call (template calling with unused setbody 1`] = `
|
||||
) {
|
||||
var h = this.utils.h;
|
||||
{
|
||||
let _1 = 3
|
||||
let _1 = 3;
|
||||
let c2 = [], p2 = {key:2};
|
||||
var vn2 = h('div', p2, c2);
|
||||
c2.push({text: \`ok\`});
|
||||
@@ -874,7 +846,7 @@ exports[`t-call (template calling with used set body 1`] = `
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('span', p1, c1);
|
||||
{
|
||||
let _2 = 'ok'
|
||||
let _2 = 'ok';
|
||||
if (_2 || _2 === 0) {
|
||||
c1.push({text: _2});
|
||||
}
|
||||
@@ -963,13 +935,13 @@ exports[`t-if boolean value condition elif 1`] = `
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
if (context['color'] == 'black') {
|
||||
if (context['color']=='black') {
|
||||
c1.push({text: \`black pearl\`});
|
||||
}
|
||||
else if (context['color'] == 'yellow') {
|
||||
else if (context['color']=='yellow') {
|
||||
c1.push({text: \`yellow submarine\`});
|
||||
}
|
||||
else if (context['color'] == 'red') {
|
||||
else if (context['color']=='red') {
|
||||
c1.push({text: \`red is dead\`});
|
||||
}
|
||||
else {
|
||||
@@ -1072,28 +1044,28 @@ exports[`t-if can use some boolean operators in expressions 1`] = `
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
if (context['cond1'] && context['cond2']) {
|
||||
if (context['cond1']&&context['cond2']) {
|
||||
c1.push({text: \`and\`});
|
||||
}
|
||||
if (context['cond1'] && context['cond3']) {
|
||||
if (context['cond1']&&context['cond3']) {
|
||||
c1.push({text: \`nope\`});
|
||||
}
|
||||
if (context['cond1'] || context['cond3']) {
|
||||
if (context['cond1']||context['cond3']) {
|
||||
c1.push({text: \`or\`});
|
||||
}
|
||||
if (context['cond3'] || context['cond4']) {
|
||||
if (context['cond3']||context['cond4']) {
|
||||
c1.push({text: \`nope\`});
|
||||
}
|
||||
if (context['m'] > 3) {
|
||||
if (context['m']>3) {
|
||||
c1.push({text: \`mgt\`});
|
||||
}
|
||||
if (context['n'] > 3) {
|
||||
if (context['n']>3) {
|
||||
c1.push({text: \`ngt\`});
|
||||
}
|
||||
if (context['m'] < 3) {
|
||||
if (context['m']<3) {
|
||||
c1.push({text: \`mlt\`});
|
||||
}
|
||||
if (context['n'] < 3) {
|
||||
if (context['n']<3) {
|
||||
c1.push({text: \`nlt\`});
|
||||
}
|
||||
return vn1;
|
||||
@@ -1123,13 +1095,15 @@ exports[`t-key t-key directive in a list 1`] = `
|
||||
var vn1 = h('ul', p1, c1);
|
||||
var _2 = context['beers'];
|
||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||
if (typeof _2 === 'number') { _2 = Array.from(Array(_2).keys())}
|
||||
var _3 = _2 instanceof Array ? _2 : Object.keys(_2);
|
||||
var _4 = _2 instanceof Array ? _2 : Object.values(_2);
|
||||
for (let i = 0; i < _3.length; i++) {
|
||||
var _3 = _4 = _2;
|
||||
if (!(_2 instanceof Array)) {
|
||||
_3 = Object.keys(_2);
|
||||
_4 = Object.values(_2);
|
||||
}
|
||||
var _length3 = _3.length;
|
||||
for (let i = 0; i < _length3; i++) {
|
||||
context.beer_first = i === 0;
|
||||
context.beer_last = i === _3.length - 1;
|
||||
context.beer_parity = i % 2 === 0 ? 'even' : 'odd';
|
||||
context.beer_last = i === _length3 - 1;
|
||||
context.beer_index = i;
|
||||
context.beer = _3[i];
|
||||
context.beer_value = _4[i];
|
||||
@@ -1154,7 +1128,10 @@ exports[`t-on can bind event handler 1`] = `
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1,on:{}};
|
||||
var vn1 = h('button', p1, c1);
|
||||
extra.handlers['click' + 1] = extra.handlers['click' + 1] || (context['add'] || (function () {throw new Error('Missing handler \\\\'' + 'add' + \`\\\\' when evaluating template 'test'\`)})()).bind(owner);
|
||||
if (!context['add']) {
|
||||
throw new Error('Missing handler \\\\'' + 'add' + \`\\\\' when evaluating template 'test'\`)
|
||||
}
|
||||
extra.handlers['click' + 1] = extra.handlers['click' + 1] || context['add'].bind(owner);
|
||||
p1.on['click'] = extra.handlers['click' + 1];
|
||||
c1.push({text: \`Click\`});
|
||||
return vn1;
|
||||
@@ -1168,20 +1145,26 @@ exports[`t-on can bind handlers with arguments 1`] = `
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1,on:{}};
|
||||
var vn1 = h('button', p1, c1);
|
||||
p1.on['click'] = (context['add'] || (function () {throw new Error('Missing handler \\\\'' + 'add' + \`\\\\' when evaluating template 'test'\`)})()).bind(owner, 5);
|
||||
if (!context['add']) {
|
||||
throw new Error('Missing handler \\\\'' + 'add' + \`\\\\' when evaluating template 'test'\`)
|
||||
}
|
||||
p1.on['click'] = context['add'].bind(owner, 5);
|
||||
c1.push({text: \`Click\`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on can bind handlers with empty object (with non empty inner string 1`] = `
|
||||
exports[`t-on can bind handlers with empty object (with non empty inner string) 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let owner = context;
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1,on:{}};
|
||||
var vn1 = h('button', p1, c1);
|
||||
p1.on['click'] = (context['doSomething'] || (function () {throw new Error('Missing handler \\\\'' + 'doSomething' + \`\\\\' when evaluating template 'test'\`)})()).bind(owner, {});
|
||||
if (!context['doSomething']) {
|
||||
throw new Error('Missing handler \\\\'' + 'doSomething' + \`\\\\' when evaluating template 'test'\`)
|
||||
}
|
||||
p1.on['click'] = context['doSomething'].bind(owner, {});
|
||||
c1.push({text: \`Click\`});
|
||||
return vn1;
|
||||
}"
|
||||
@@ -1194,7 +1177,10 @@ exports[`t-on can bind handlers with empty object 1`] = `
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1,on:{}};
|
||||
var vn1 = h('button', p1, c1);
|
||||
p1.on['click'] = (context['doSomething'] || (function () {throw new Error('Missing handler \\\\'' + 'doSomething' + \`\\\\' when evaluating template 'test'\`)})()).bind(owner, {});
|
||||
if (!context['doSomething']) {
|
||||
throw new Error('Missing handler \\\\'' + 'doSomething' + \`\\\\' when evaluating template 'test'\`)
|
||||
}
|
||||
p1.on['click'] = context['doSomething'].bind(owner, {});
|
||||
c1.push({text: \`Click\`});
|
||||
return vn1;
|
||||
}"
|
||||
@@ -1210,13 +1196,15 @@ exports[`t-on can bind handlers with loop variable as argument 1`] = `
|
||||
var vn1 = h('ul', p1, c1);
|
||||
var _2 = ['someval'];
|
||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||
if (typeof _2 === 'number') { _2 = Array.from(Array(_2).keys())}
|
||||
var _3 = _2 instanceof Array ? _2 : Object.keys(_2);
|
||||
var _4 = _2 instanceof Array ? _2 : Object.values(_2);
|
||||
for (let i = 0; i < _3.length; i++) {
|
||||
var _3 = _4 = _2;
|
||||
if (!(_2 instanceof Array)) {
|
||||
_3 = Object.keys(_2);
|
||||
_4 = Object.values(_2);
|
||||
}
|
||||
var _length3 = _3.length;
|
||||
for (let i = 0; i < _length3; i++) {
|
||||
context.action_first = i === 0;
|
||||
context.action_last = i === _3.length - 1;
|
||||
context.action_parity = i % 2 === 0 ? 'even' : 'odd';
|
||||
context.action_last = i === _length3 - 1;
|
||||
context.action_index = i;
|
||||
context.action = _3[i];
|
||||
context.action_value = _4[i];
|
||||
@@ -1226,7 +1214,10 @@ exports[`t-on can bind handlers with loop variable as argument 1`] = `
|
||||
let c6 = [], p6 = {key:6,on:{}};
|
||||
var vn6 = h('a', p6, c6);
|
||||
c5.push(vn6);
|
||||
p6.on['click'] = (context['activate'] || (function () {throw new Error('Missing handler \\\\'' + 'activate' + \`\\\\' when evaluating template 'test'\`)})()).bind(owner, context['action']);
|
||||
if (!context['activate']) {
|
||||
throw new Error('Missing handler \\\\'' + 'activate' + \`\\\\' when evaluating template 'test'\`)
|
||||
}
|
||||
p6.on['click'] = context['activate'].bind(owner, context['action']);
|
||||
c6.push({text: \`link\`});
|
||||
}
|
||||
return vn1;
|
||||
@@ -1240,7 +1231,10 @@ exports[`t-on can bind handlers with object arguments 1`] = `
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1,on:{}};
|
||||
var vn1 = h('button', p1, c1);
|
||||
p1.on['click'] = (context['add'] || (function () {throw new Error('Missing handler \\\\'' + 'add' + \`\\\\' when evaluating template 'test'\`)})()).bind(owner, {val: 5});
|
||||
if (!context['add']) {
|
||||
throw new Error('Missing handler \\\\'' + 'add' + \`\\\\' when evaluating template 'test'\`)
|
||||
}
|
||||
p1.on['click'] = context['add'].bind(owner, {val:5});
|
||||
c1.push({text: \`Click\`});
|
||||
return vn1;
|
||||
}"
|
||||
@@ -1253,9 +1247,15 @@ exports[`t-on can bind two event handlers 1`] = `
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1,on:{}};
|
||||
var vn1 = h('button', p1, c1);
|
||||
extra.handlers['click' + 1] = extra.handlers['click' + 1] || (context['handleClick'] || (function () {throw new Error('Missing handler \\\\'' + 'handleClick' + \`\\\\' when evaluating template 'test'\`)})()).bind(owner);
|
||||
if (!context['handleClick']) {
|
||||
throw new Error('Missing handler \\\\'' + 'handleClick' + \`\\\\' when evaluating template 'test'\`)
|
||||
}
|
||||
extra.handlers['click' + 1] = extra.handlers['click' + 1] || context['handleClick'].bind(owner);
|
||||
p1.on['click'] = extra.handlers['click' + 1];
|
||||
extra.handlers['dblclick' + 1] = extra.handlers['dblclick' + 1] || (context['handleDblClick'] || (function () {throw new Error('Missing handler \\\\'' + 'handleDblClick' + \`\\\\' when evaluating template 'test'\`)})()).bind(owner);
|
||||
if (!context['handleDblClick']) {
|
||||
throw new Error('Missing handler \\\\'' + 'handleDblClick' + \`\\\\' when evaluating template 'test'\`)
|
||||
}
|
||||
extra.handlers['dblclick' + 1] = extra.handlers['dblclick' + 1] || context['handleDblClick'].bind(owner);
|
||||
p1.on['dblclick'] = extra.handlers['dblclick' + 1];
|
||||
c1.push({text: \`Click\`});
|
||||
return vn1;
|
||||
@@ -1269,13 +1269,135 @@ exports[`t-on handler is bound to proper owner 1`] = `
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1,on:{}};
|
||||
var vn1 = h('button', p1, c1);
|
||||
extra.handlers['click' + 1] = extra.handlers['click' + 1] || (context['add'] || (function () {throw new Error('Missing handler \\\\'' + 'add' + \`\\\\' when evaluating template 'test'\`)})()).bind(owner);
|
||||
if (!context['add']) {
|
||||
throw new Error('Missing handler \\\\'' + 'add' + \`\\\\' when evaluating template 'test'\`)
|
||||
}
|
||||
extra.handlers['click' + 1] = extra.handlers['click' + 1] || context['add'].bind(owner);
|
||||
p1.on['click'] = extra.handlers['click' + 1];
|
||||
c1.push({text: \`Click\`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on t-on with prevent and self modifiers (order matters) 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let owner = context;
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
let c2 = [], p2 = {key:2,on:{}};
|
||||
var vn2 = h('button', p2, c2);
|
||||
c1.push(vn2);
|
||||
if (!context['onClick']) {
|
||||
throw new Error('Missing handler \\\\'' + 'onClick' + \`\\\\' when evaluating template 'test'\`)
|
||||
}
|
||||
extra.handlers['click' + 2] = extra.handlers['click' + 2] || function (e) {e.preventDefault();if (e.target !== this.elm) {return}context['onClick'].call(owner, e);};
|
||||
p2.on['click'] = extra.handlers['click' + 2];
|
||||
let c3 = [], p3 = {key:3};
|
||||
var vn3 = h('span', p3, c3);
|
||||
c2.push(vn3);
|
||||
c3.push({text: \`Button\`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on t-on with prevent and/or stop modifiers 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let owner = context;
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
let c2 = [], p2 = {key:2,on:{}};
|
||||
var vn2 = h('button', p2, c2);
|
||||
c1.push(vn2);
|
||||
if (!context['onClickPrevented']) {
|
||||
throw new Error('Missing handler \\\\'' + 'onClickPrevented' + \`\\\\' when evaluating template 'test'\`)
|
||||
}
|
||||
extra.handlers['click' + 2] = extra.handlers['click' + 2] || function (e) {e.preventDefault();context['onClickPrevented'].call(owner, e);};
|
||||
p2.on['click'] = extra.handlers['click' + 2];
|
||||
c2.push({text: \`Button 1\`});
|
||||
let c3 = [], p3 = {key:3,on:{}};
|
||||
var vn3 = h('button', p3, c3);
|
||||
c1.push(vn3);
|
||||
if (!context['onClickStopped']) {
|
||||
throw new Error('Missing handler \\\\'' + 'onClickStopped' + \`\\\\' when evaluating template 'test'\`)
|
||||
}
|
||||
extra.handlers['click' + 3] = extra.handlers['click' + 3] || function (e) {e.stopPropagation();context['onClickStopped'].call(owner, e);};
|
||||
p3.on['click'] = extra.handlers['click' + 3];
|
||||
c3.push({text: \`Button 2\`});
|
||||
let c4 = [], p4 = {key:4,on:{}};
|
||||
var vn4 = h('button', p4, c4);
|
||||
c1.push(vn4);
|
||||
if (!context['onClickPreventedAndStopped']) {
|
||||
throw new Error('Missing handler \\\\'' + 'onClickPreventedAndStopped' + \`\\\\' when evaluating template 'test'\`)
|
||||
}
|
||||
extra.handlers['click' + 4] = extra.handlers['click' + 4] || function (e) {e.preventDefault();e.stopPropagation();context['onClickPreventedAndStopped'].call(owner, e);};
|
||||
p4.on['click'] = extra.handlers['click' + 4];
|
||||
c4.push({text: \`Button 3\`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on t-on with self and prevent modifiers (order matters) 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let owner = context;
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
let c2 = [], p2 = {key:2,on:{}};
|
||||
var vn2 = h('button', p2, c2);
|
||||
c1.push(vn2);
|
||||
if (!context['onClick']) {
|
||||
throw new Error('Missing handler \\\\'' + 'onClick' + \`\\\\' when evaluating template 'test'\`)
|
||||
}
|
||||
extra.handlers['click' + 2] = extra.handlers['click' + 2] || function (e) {if (e.target !== this.elm) {return}e.preventDefault();context['onClick'].call(owner, e);};
|
||||
p2.on['click'] = extra.handlers['click' + 2];
|
||||
let c3 = [], p3 = {key:3};
|
||||
var vn3 = h('span', p3, c3);
|
||||
c2.push(vn3);
|
||||
c3.push({text: \`Button\`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-on t-on with self modifier 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let owner = context;
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
let c2 = [], p2 = {key:2,on:{}};
|
||||
var vn2 = h('button', p2, c2);
|
||||
c1.push(vn2);
|
||||
if (!context['onClick']) {
|
||||
throw new Error('Missing handler \\\\'' + 'onClick' + \`\\\\' when evaluating template 'test'\`)
|
||||
}
|
||||
extra.handlers['click' + 2] = extra.handlers['click' + 2] || context['onClick'].bind(owner);
|
||||
p2.on['click'] = extra.handlers['click' + 2];
|
||||
let c3 = [], p3 = {key:3};
|
||||
var vn3 = h('span', p3, c3);
|
||||
c2.push(vn3);
|
||||
c3.push({text: \`Button\`});
|
||||
let c4 = [], p4 = {key:4,on:{}};
|
||||
var vn4 = h('button', p4, c4);
|
||||
c1.push(vn4);
|
||||
if (!context['onClickSelf']) {
|
||||
throw new Error('Missing handler \\\\'' + 'onClickSelf' + \`\\\\' when evaluating template 'test'\`)
|
||||
}
|
||||
extra.handlers['click' + 4] = extra.handlers['click' + 4] || function (e) {if (e.target !== this.elm) {return}context['onClickSelf'].call(owner, e);};
|
||||
p4.on['click'] = extra.handlers['click' + 4];
|
||||
let c5 = [], p5 = {key:5};
|
||||
var vn5 = h('span', p5, c5);
|
||||
c4.push(vn5);
|
||||
c5.push({text: \`Button\`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-raw literal 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
@@ -1365,7 +1487,7 @@ exports[`t-ref can get a dynamic ref on a node 1`] = `
|
||||
let c2 = [], p2 = {key:2};
|
||||
var vn2 = h('span', p2, c2);
|
||||
c1.push(vn2);
|
||||
const ref3 = 'myspan' + 3
|
||||
const ref3 = \`myspan\${context['id']}\`;
|
||||
p2.hook = {
|
||||
create: (_, n) => {
|
||||
context.refs[ref3] = n.elm;
|
||||
@@ -1384,7 +1506,7 @@ exports[`t-ref can get a ref on a node 1`] = `
|
||||
let c2 = [], p2 = {key:2};
|
||||
var vn2 = h('span', p2, c2);
|
||||
c1.push(vn2);
|
||||
const ref3 = 'myspan'
|
||||
const ref3 = \`myspan\`;
|
||||
p2.hook = {
|
||||
create: (_, n) => {
|
||||
context.refs[ref3] = n.elm;
|
||||
@@ -1403,20 +1525,22 @@ exports[`t-ref refs in a loop 1`] = `
|
||||
var vn1 = h('div', p1, c1);
|
||||
var _2 = context['items'];
|
||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||
if (typeof _2 === 'number') { _2 = Array.from(Array(_2).keys())}
|
||||
var _3 = _2 instanceof Array ? _2 : Object.keys(_2);
|
||||
var _4 = _2 instanceof Array ? _2 : Object.values(_2);
|
||||
for (let i = 0; i < _3.length; i++) {
|
||||
var _3 = _4 = _2;
|
||||
if (!(_2 instanceof Array)) {
|
||||
_3 = Object.keys(_2);
|
||||
_4 = Object.values(_2);
|
||||
}
|
||||
var _length3 = _3.length;
|
||||
for (let i = 0; i < _length3; i++) {
|
||||
context.item_first = i === 0;
|
||||
context.item_last = i === _3.length - 1;
|
||||
context.item_parity = i % 2 === 0 ? 'even' : 'odd';
|
||||
context.item_last = i === _length3 - 1;
|
||||
context.item_index = i;
|
||||
context.item = _3[i];
|
||||
context.item_value = _4[i];
|
||||
let c5 = [], p5 = {key:context['item']};
|
||||
var vn5 = h('div', p5, c5);
|
||||
c1.push(vn5);
|
||||
const ref6 = context['item']
|
||||
const ref6 = (context['item']);
|
||||
p5.hook = {
|
||||
create: (_, n) => {
|
||||
context.refs[ref6] = n.elm;
|
||||
@@ -1437,7 +1561,7 @@ exports[`t-set evaluate value expression 1`] = `
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
var _2 = 1 + 2;
|
||||
var _2 = 1+2;
|
||||
if (_2 || _2 === 0) {
|
||||
c1.push({text: _2});
|
||||
}
|
||||
@@ -1451,7 +1575,7 @@ exports[`t-set evaluate value expression, part 2 1`] = `
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
var _2 = context['somevariable'] + 2;
|
||||
var _2 = context['somevariable']+2;
|
||||
if (_2 || _2 === 0) {
|
||||
c1.push({text: _2});
|
||||
}
|
||||
@@ -1527,7 +1651,7 @@ exports[`t-set t-set and t-if 1`] = `
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
var _2 = context['value'];
|
||||
if (_2 === 'ok') {
|
||||
if (_2==='ok') {
|
||||
c1.push({text: \`grimbergen\`});
|
||||
}
|
||||
return vn1;
|
||||
@@ -1540,7 +1664,7 @@ exports[`t-set t-set evaluates an expression only once 1`] = `
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
var _2 = context['value'] + ' artois';
|
||||
var _2 = context['value']+' artois';
|
||||
if (_2 || _2 === 0) {
|
||||
c1.push({text: _2});
|
||||
}
|
||||
@@ -1561,13 +1685,15 @@ exports[`t-set t-set should reuse variable if possible 1`] = `
|
||||
var _2 = 1;
|
||||
var _3 = context['list'];
|
||||
if (!_3) { throw new Error('QWeb error: Invalid loop expression')}
|
||||
if (typeof _3 === 'number') { _3 = Array.from(Array(_3).keys())}
|
||||
var _4 = _3 instanceof Array ? _3 : Object.keys(_3);
|
||||
var _5 = _3 instanceof Array ? _3 : Object.values(_3);
|
||||
for (let i = 0; i < _4.length; i++) {
|
||||
var _4 = _5 = _3;
|
||||
if (!(_3 instanceof Array)) {
|
||||
_4 = Object.keys(_3);
|
||||
_5 = Object.values(_3);
|
||||
}
|
||||
var _length4 = _4.length;
|
||||
for (let i = 0; i < _length4; i++) {
|
||||
context.elem_first = i === 0;
|
||||
context.elem_last = i === _4.length - 1;
|
||||
context.elem_parity = i % 2 === 0 ? 'even' : 'odd';
|
||||
context.elem_last = i === _length4 - 1;
|
||||
context.elem_index = i;
|
||||
context.elem = _4[i];
|
||||
context.elem_value = _5[i];
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`connecting a component to store connecting a component to a local store 1`] = `"<div></div>"`;
|
||||
|
||||
exports[`connecting a component to store connecting a component to a local store 2`] = `"<div><span>hello</span></div>"`;
|
||||
|
||||
exports[`connecting a component to store connecting a component works 1`] = `"<div></div>"`;
|
||||
|
||||
exports[`connecting a component to store connecting a component works 2`] = `"<div><span>hello</span></div>"`;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
import { Component, Env } from "../src/component";
|
||||
import { QWeb } from "../src/qweb_core";
|
||||
import {
|
||||
makeDeferred,
|
||||
makeTestFixture,
|
||||
makeTestEnv,
|
||||
nextTick,
|
||||
patchNextFrame,
|
||||
renderToDOM,
|
||||
unpatchNextFrame
|
||||
} from "./helpers";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Setup and helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// We create before each test:
|
||||
// - fixture: a div, appended to the DOM, intended to be the target of dom
|
||||
// manipulations. Note that it is removed after each test.
|
||||
// - qweb: a new QWeb instance
|
||||
// - env: a WEnv, necessary to create new widgets
|
||||
// - cssEl: a stylesheet injected into the dom
|
||||
|
||||
let fixture: HTMLElement;
|
||||
let qweb: QWeb;
|
||||
let env: Env;
|
||||
let cssEl: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = makeTestFixture();
|
||||
env = makeTestEnv();
|
||||
qweb = new QWeb();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.remove();
|
||||
});
|
||||
|
||||
class Widget extends Component<any, any, any> {}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Tests
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
describe("animations", () => {
|
||||
beforeEach(() => {
|
||||
cssEl = document.createElement("style");
|
||||
let css = `
|
||||
.chimay-enter-active, .chimay-leave-active {
|
||||
transition-property: opacity;
|
||||
transition-duration: 0.1s;
|
||||
}
|
||||
.chimay-enter, .chimay-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
`;
|
||||
cssEl.textContent = css.trim();
|
||||
document.head.appendChild(cssEl);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.head.removeChild(cssEl);
|
||||
unpatchNextFrame();
|
||||
});
|
||||
|
||||
test("t-transition, on a simple node (insert)", async () => {
|
||||
expect.assertions(5);
|
||||
qweb.addTemplate("test", `<span t-transition="chimay">blue</span>`);
|
||||
|
||||
let def = makeDeferred();
|
||||
patchNextFrame(cb => {
|
||||
expect(node.className).toBe("chimay-enter chimay-enter-active");
|
||||
cb();
|
||||
expect(node.className).toBe("chimay-enter-active chimay-enter-to");
|
||||
def.resolve();
|
||||
});
|
||||
let node: HTMLElement = <HTMLElement>renderToDOM(qweb, "test");
|
||||
|
||||
expect(node.className).toBe("chimay-enter chimay-enter-active");
|
||||
await def; // wait for the mocked repaint to be done
|
||||
node.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||
expect(node.className).toBe("");
|
||||
});
|
||||
|
||||
test("t-transition with no delay/duration", async () => {
|
||||
expect.assertions(4);
|
||||
qweb.addTemplate("test", `<span t-transition="jupiler">blue</span>`);
|
||||
|
||||
let def = makeDeferred();
|
||||
patchNextFrame(cb => {
|
||||
expect(node.className).toBe("jupiler-enter jupiler-enter-active");
|
||||
cb();
|
||||
expect(node.className).toBe("");
|
||||
def.resolve();
|
||||
});
|
||||
let node: HTMLElement = <HTMLElement>renderToDOM(qweb, "test");
|
||||
expect(node.className).toBe("jupiler-enter jupiler-enter-active");
|
||||
await def;
|
||||
});
|
||||
|
||||
test("t-transition on a conditional node", async () => {
|
||||
expect.assertions(7);
|
||||
|
||||
env.qweb.addTemplate(
|
||||
"TestWidget",
|
||||
`<div><span t-if="!state.hide" t-transition="chimay">blue</span></div>`
|
||||
);
|
||||
class TestWidget extends Widget {
|
||||
state = { hide: false };
|
||||
}
|
||||
const widget = new TestWidget(env);
|
||||
|
||||
// insert widget into the DOM
|
||||
let def = makeDeferred();
|
||||
var spanNode;
|
||||
patchNextFrame(cb => {
|
||||
expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
|
||||
cb();
|
||||
expect(spanNode.className).toBe("chimay-enter-active chimay-enter-to");
|
||||
def.resolve();
|
||||
});
|
||||
await widget.mount(fixture);
|
||||
spanNode = widget.el!.children[0];
|
||||
expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
|
||||
await def; // wait for the mocked repaint to be done
|
||||
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||
expect(spanNode.className).toBe("");
|
||||
|
||||
// remove span from the DOM
|
||||
def = makeDeferred();
|
||||
widget.state.hide = true;
|
||||
patchNextFrame(cb => {
|
||||
expect(spanNode.className).toBe("chimay-leave chimay-leave-active");
|
||||
cb();
|
||||
expect(spanNode.className).toBe("chimay-leave-active chimay-leave-to");
|
||||
def.resolve();
|
||||
});
|
||||
await def; // wait for the mocked repaint to be done
|
||||
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||
expect(spanNode.className).toBe("");
|
||||
});
|
||||
|
||||
test("t-transition combined with t-ref", async () => {
|
||||
expect.assertions(5);
|
||||
|
||||
env.qweb.addTemplate(
|
||||
"TestWidget",
|
||||
`<div><span t-ref="span" t-transition="chimay">blue</span></div>`
|
||||
);
|
||||
class TestWidget extends Widget {
|
||||
state = { hide: false };
|
||||
}
|
||||
const widget = new TestWidget(env);
|
||||
|
||||
// insert widget into the DOM
|
||||
let def = makeDeferred();
|
||||
var spanNode;
|
||||
patchNextFrame(cb => {
|
||||
expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
|
||||
cb();
|
||||
expect(spanNode.className).toBe("chimay-enter-active chimay-enter-to");
|
||||
def.resolve();
|
||||
});
|
||||
await widget.mount(fixture);
|
||||
spanNode = widget.el!.children[0];
|
||||
expect(widget.refs.span).toBe(spanNode);
|
||||
expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
|
||||
await def; // wait for the mocked repaint to be done
|
||||
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||
expect(spanNode.className).toBe("");
|
||||
});
|
||||
|
||||
test("t-transition combined with t-widget", async () => {
|
||||
expect.assertions(5);
|
||||
|
||||
env.qweb.addTemplate(
|
||||
"Parent",
|
||||
`<div><t t-widget="Child" t-transition="chimay"/></div>`
|
||||
);
|
||||
env.qweb.addTemplate("Child", `<span>blue</span>`);
|
||||
class Parent extends Widget {
|
||||
widgets = { Child: Child };
|
||||
}
|
||||
class Child extends Widget {}
|
||||
const widget = new Parent(env);
|
||||
|
||||
let def = makeDeferred();
|
||||
var spanNode;
|
||||
patchNextFrame(cb => {
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="chimay-enter chimay-enter-active">blue</span></div>'
|
||||
);
|
||||
cb();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="chimay-enter-active chimay-enter-to">blue</span></div>'
|
||||
);
|
||||
def.resolve();
|
||||
});
|
||||
await widget.mount(fixture);
|
||||
spanNode = widget.el!.children[0];
|
||||
|
||||
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="chimay-enter chimay-enter-active">blue</span></div>'
|
||||
);
|
||||
|
||||
await def; // wait for the mocked repaint to be done
|
||||
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||
expect(fixture.innerHTML).toBe('<div><span class="">blue</span></div>');
|
||||
});
|
||||
|
||||
test("t-transition combined with t-widget and t-if", async () => {
|
||||
expect.assertions(8);
|
||||
|
||||
env.qweb.addTemplate(
|
||||
"Parent",
|
||||
`<div><t t-if="state.display" t-widget="Child" t-transition="chimay"/></div>`
|
||||
);
|
||||
env.qweb.addTemplate("Child", `<span>blue</span>`);
|
||||
class Parent extends Widget {
|
||||
widgets = { Child: Child };
|
||||
state = { display: true };
|
||||
}
|
||||
class Child extends Widget {}
|
||||
const widget = new Parent(env);
|
||||
|
||||
let def = makeDeferred();
|
||||
var spanNode;
|
||||
patchNextFrame(cb => {
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="chimay-enter chimay-enter-active">blue</span></div>'
|
||||
);
|
||||
cb();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="chimay-enter-active chimay-enter-to">blue</span></div>'
|
||||
);
|
||||
def.resolve();
|
||||
});
|
||||
await widget.mount(fixture);
|
||||
spanNode = widget.el!.children[0];
|
||||
|
||||
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="chimay-enter chimay-enter-active">blue</span></div>'
|
||||
);
|
||||
|
||||
await def; // wait for the mocked repaint to be done
|
||||
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||
expect(fixture.innerHTML).toBe('<div><span class="">blue</span></div>');
|
||||
|
||||
// remove span from the DOM
|
||||
def = makeDeferred();
|
||||
widget.state.display = false;
|
||||
patchNextFrame(cb => {
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="chimay-leave chimay-leave-active" data-owl-key="4">blue</span></div>'
|
||||
);
|
||||
cb();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="4">blue</span></div>'
|
||||
);
|
||||
def.resolve();
|
||||
});
|
||||
await def; // wait for the mocked repaint to be done
|
||||
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||
expect(fixture.innerHTML).toBe("<div></div>");
|
||||
});
|
||||
|
||||
test("t-transition combined with t-mounted", async () => {
|
||||
env.qweb.addTemplate(
|
||||
"TestWidget",
|
||||
`<div><span t-if="state.flag" t-mounted="f" t-transition="chimay">blue</span></div>`
|
||||
);
|
||||
class TestWidget extends Widget {
|
||||
state = { flag: false };
|
||||
f() {}
|
||||
}
|
||||
const widget = new TestWidget(env);
|
||||
widget.f = jest.fn();
|
||||
await widget.mount(fixture);
|
||||
|
||||
patchNextFrame(cb => cb());
|
||||
expect(widget.f).toHaveBeenCalledTimes(0);
|
||||
|
||||
widget.state.flag = true;
|
||||
await nextTick();
|
||||
expect(widget.f).toHaveBeenCalledTimes(1);
|
||||
unpatchNextFrame();
|
||||
});
|
||||
|
||||
test("t-transition, remove and re-add before transitionend", async () => {
|
||||
expect.assertions(11);
|
||||
|
||||
env.qweb.addTemplates(
|
||||
`<templates>
|
||||
<div t-name="Parent">
|
||||
<button t-on-click="toggle">Toggle</button>
|
||||
<span t-if="state.flag" t-transition="chimay">blue</span>
|
||||
</div>
|
||||
</templates>`
|
||||
);
|
||||
class Parent extends Widget {
|
||||
constructor(parent) {
|
||||
super(parent);
|
||||
this.state = { flag: false };
|
||||
}
|
||||
toggle() {
|
||||
this.state.flag = !this.state.flag;
|
||||
}
|
||||
}
|
||||
|
||||
const widget = new Parent(env);
|
||||
await widget.mount(fixture);
|
||||
let button = widget.el!.querySelector("button");
|
||||
|
||||
let def = makeDeferred();
|
||||
let phase = "enter";
|
||||
patchNextFrame(cb => {
|
||||
let spans = fixture.querySelectorAll("span");
|
||||
expect(spans.length).toBe(1);
|
||||
expect(spans[0].className).toBe(`chimay-${phase} chimay-${phase}-active`);
|
||||
cb();
|
||||
expect(spans[0].className).toBe(
|
||||
`chimay-${phase}-active chimay-${phase}-to`
|
||||
);
|
||||
def.resolve();
|
||||
});
|
||||
|
||||
// click display the span
|
||||
button!.click();
|
||||
await def; // wait for the mocked repaint to be done
|
||||
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><button>Toggle</button><span class="">blue</span></div>'
|
||||
);
|
||||
|
||||
// click to remove the span, and click again to re-add it before transitionend
|
||||
def = makeDeferred();
|
||||
phase = "leave";
|
||||
button!.click();
|
||||
|
||||
await def; // wait for the mocked repaint to be done
|
||||
def = makeDeferred();
|
||||
phase = "enter";
|
||||
button!.click();
|
||||
|
||||
await def; // wait for the mocked repaint to be done
|
||||
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><button>Toggle</button><span class="">blue</span></div>'
|
||||
);
|
||||
});
|
||||
|
||||
test("t-transition combined with t-widget, remove and re-add before transitionend", async () => {
|
||||
expect.assertions(11);
|
||||
|
||||
env.qweb.addTemplates(
|
||||
`<templates>
|
||||
<div t-name="Parent">
|
||||
<button t-on-click="toggle">Toggle</button>
|
||||
<t t-if="state.flag" t-widget="Child" t-transition="chimay"/>
|
||||
</div>
|
||||
<span t-name="Child">blue</span>
|
||||
</templates>`
|
||||
);
|
||||
class Child extends Widget {}
|
||||
class Parent extends Widget {
|
||||
widgets = { Child };
|
||||
constructor(parent) {
|
||||
super(parent);
|
||||
this.state = { flag: false };
|
||||
}
|
||||
toggle() {
|
||||
this.state.flag = !this.state.flag;
|
||||
}
|
||||
}
|
||||
|
||||
const widget = new Parent(env);
|
||||
await widget.mount(fixture);
|
||||
let button = widget.el!.querySelector("button");
|
||||
|
||||
let def = makeDeferred();
|
||||
let phase = "enter";
|
||||
patchNextFrame(cb => {
|
||||
let spans = fixture.querySelectorAll("span");
|
||||
expect(spans.length).toBe(1);
|
||||
expect(spans[0].className).toBe(`chimay-${phase} chimay-${phase}-active`);
|
||||
cb();
|
||||
expect(spans[0].className).toBe(
|
||||
`chimay-${phase}-active chimay-${phase}-to`
|
||||
);
|
||||
def.resolve();
|
||||
});
|
||||
|
||||
// click display the span
|
||||
button!.click();
|
||||
await def; // wait for the mocked repaint to be done
|
||||
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><button>Toggle</button><span class="">blue</span></div>'
|
||||
);
|
||||
|
||||
// click to remove the span, and click again to re-add it before transitionend
|
||||
def = makeDeferred();
|
||||
phase = "leave";
|
||||
button!.click();
|
||||
|
||||
await def; // wait for the mocked repaint to be done
|
||||
def = makeDeferred();
|
||||
phase = "enter";
|
||||
button!.click();
|
||||
|
||||
await def; // wait for the mocked repaint to be done
|
||||
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><button>Toggle</button><span class="">blue</span></div>'
|
||||
);
|
||||
});
|
||||
});
|
||||
+505
-119
@@ -1,4 +1,5 @@
|
||||
import { Component, Env } from "../src/component";
|
||||
import { QWeb } from "../src/qweb_core";
|
||||
import {
|
||||
makeDeferred,
|
||||
makeTestFixture,
|
||||
@@ -285,7 +286,7 @@ describe("lifecycle hooks", () => {
|
||||
|
||||
env.qweb.addTemplate(
|
||||
"ParentWidget",
|
||||
`<div>><t t-widget="child" t-props="{n:state.n}"/></div>`
|
||||
`<div><t t-widget="child" n="state.n"/></div>`
|
||||
);
|
||||
class ParentWidget extends Widget {
|
||||
widgets = { child: ChildWidget };
|
||||
@@ -299,7 +300,7 @@ describe("lifecycle hooks", () => {
|
||||
}
|
||||
env.qweb.addTemplate(
|
||||
"ChildWidget",
|
||||
`<div><t t-widget="childchild" t-props="{n:props.n}"/></div>`
|
||||
`<div><t t-widget="childchild" n="props.n"/></div>`
|
||||
);
|
||||
class ChildWidget extends Widget {
|
||||
widgets = { childchild: ChildChildWidget };
|
||||
@@ -452,7 +453,7 @@ describe("lifecycle hooks", () => {
|
||||
`
|
||||
<div>
|
||||
<div t-if="state.flag">
|
||||
<t t-widget="ChildWidget" t-props="{n: state.n}"/>
|
||||
<t t-widget="ChildWidget" n="state.n"/>
|
||||
</div>
|
||||
</div>`
|
||||
);
|
||||
@@ -533,7 +534,7 @@ describe("lifecycle hooks", () => {
|
||||
let def = makeDeferred();
|
||||
env.qweb.addTemplate(
|
||||
"Parent",
|
||||
'<span><t t-widget="Child" t-props="{n: state.n}"/></span>'
|
||||
'<span><t t-widget="Child" n="state.n"/></span>'
|
||||
);
|
||||
class Parent extends Widget {
|
||||
state = { n: 1 };
|
||||
@@ -585,7 +586,7 @@ describe("lifecycle hooks", () => {
|
||||
|
||||
env.qweb.addTemplate(
|
||||
"Parent",
|
||||
'<div><t t-widget="Child" t-props="{a:state.a}"/></div>'
|
||||
'<div><t t-widget="Child" a="state.a"/></div>'
|
||||
);
|
||||
class Parent extends Widget {
|
||||
state = { a: 1 };
|
||||
@@ -628,7 +629,7 @@ describe("lifecycle hooks", () => {
|
||||
let shouldUpdate = false;
|
||||
env.qweb.addTemplate(
|
||||
"Parent",
|
||||
`<div><t t-widget="Child" t-props="{val:state.val}"/></div>`
|
||||
`<div><t t-widget="Child" val="state.val"/></div>`
|
||||
);
|
||||
class Parent extends Widget {
|
||||
state = { val: 42 };
|
||||
@@ -695,7 +696,7 @@ describe("lifecycle hooks", () => {
|
||||
"ParentWidget",
|
||||
`
|
||||
<div>
|
||||
<t t-widget="child" t-props="{v: state.n}"/>
|
||||
<t t-widget="child" v="state.n"/>
|
||||
</div>`
|
||||
);
|
||||
class ParentWidget extends Widget {
|
||||
@@ -742,7 +743,7 @@ describe("lifecycle hooks", () => {
|
||||
"ParentWidget",
|
||||
`
|
||||
<div>
|
||||
<t t-if="state.flag" t-widget="child" t-props="{v: state.n}" t-keepalive="1"/>
|
||||
<t t-if="state.flag" t-widget="child" v="state.n" t-keepalive="1"/>
|
||||
</div>`
|
||||
);
|
||||
class ParentWidget extends Widget {
|
||||
@@ -849,6 +850,30 @@ describe("composition", () => {
|
||||
expect(children(widget)[0].__owl__.parent).toBe(widget);
|
||||
});
|
||||
|
||||
test("can use widgets from the global registry", async () => {
|
||||
QWeb.register("WidgetB", WidgetB);
|
||||
env.qweb.addTemplate("ParentWidget", `<div><t t-widget="WidgetB"/></div>`);
|
||||
class ParentWidget extends Widget {}
|
||||
const widget = new ParentWidget(env);
|
||||
await widget.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><div>world</div></div>");
|
||||
delete QWeb.widgets["WidgetB"];
|
||||
});
|
||||
|
||||
test("don't fallback to global registry if widget defined locally", async () => {
|
||||
QWeb.register("WidgetB", WidgetB); // should not use this widget
|
||||
env.qweb.addTemplate("ParentWidget", `<div><t t-widget="WidgetB"/></div>`);
|
||||
env.qweb.addTemplate("AnotherWidgetB", `<span>Belgium</span>`);
|
||||
class AnotherWidgetB extends Widget {}
|
||||
class ParentWidget extends Widget {
|
||||
widgets = { WidgetB: AnotherWidgetB };
|
||||
}
|
||||
const widget = new ParentWidget(env);
|
||||
await widget.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><span>Belgium</span></div>");
|
||||
delete QWeb.widgets["WidgetB"];
|
||||
});
|
||||
|
||||
test("throw a nice error if it cannot find widget", async () => {
|
||||
expect.assertions(1);
|
||||
env.qweb.addTemplate(
|
||||
@@ -871,7 +896,7 @@ describe("composition", () => {
|
||||
test("t-refs on widget are widgets", async () => {
|
||||
env.qweb.addTemplate(
|
||||
"WidgetC",
|
||||
`<div>Hello<t t-ref="'mywidgetb'" t-widget="b"/></div>`
|
||||
`<div>Hello<t t-ref="mywidgetb" t-widget="b"/></div>`
|
||||
);
|
||||
class WidgetC extends Widget {
|
||||
widgets = { b: WidgetB };
|
||||
@@ -887,7 +912,7 @@ describe("composition", () => {
|
||||
"ParentWidget",
|
||||
`
|
||||
<div>
|
||||
<t t-foreach="state.list" t-as="elem" t-ref="'child'" t-key="elem" t-widget="Widget"/>
|
||||
<t t-foreach="state.list" t-as="elem" t-ref="child" t-key="elem" t-widget="Widget"/>
|
||||
</div>`
|
||||
);
|
||||
class ParentWidget extends Widget {
|
||||
@@ -913,8 +938,8 @@ describe("composition", () => {
|
||||
"ParentWidget",
|
||||
`
|
||||
<div>
|
||||
<t t-if="state.child1" t-ref="'child1'" t-widget="Widget"/>
|
||||
<t t-if="state.child2" t-ref="'child2'" t-widget="Widget"/>
|
||||
<t t-if="state.child1" t-ref="child1" t-widget="Widget"/>
|
||||
<t t-if="state.child2" t-ref="child2" t-widget="Widget"/>
|
||||
</div>`
|
||||
);
|
||||
class ParentWidget extends Widget {
|
||||
@@ -979,7 +1004,7 @@ describe("composition", () => {
|
||||
"ParentWidget",
|
||||
`<div>
|
||||
<t t-foreach="state.items" t-as="item">
|
||||
<t t-widget="Child" t-ref="item" t-key="item"/>
|
||||
<t t-widget="Child" t-ref="{{item}}" t-key="item"/>
|
||||
</t>
|
||||
</div>`
|
||||
);
|
||||
@@ -1119,6 +1144,39 @@ describe("composition", () => {
|
||||
expect(input2.value).toBe("test");
|
||||
});
|
||||
|
||||
test("sub widget with t-ref and t-keepalive", async () => {
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="ParentWidget">
|
||||
<t t-if="state.ok"><t t-widget="ChildWidget" t-ref="child" t-keepalive="1"/></t>
|
||||
</div>
|
||||
<span t-name="ChildWidget">Hello</span>
|
||||
</templates>`);
|
||||
class ParentWidget extends Widget {
|
||||
state = { ok: true };
|
||||
widgets = { ChildWidget };
|
||||
}
|
||||
class ChildWidget extends Widget {}
|
||||
const widget = new ParentWidget(env);
|
||||
await widget.mount(fixture);
|
||||
let child = children(widget)[0];
|
||||
|
||||
expect(fixture.innerHTML).toBe("<div><span>Hello</span></div>");
|
||||
expect(widget.refs.child).toEqual(child);
|
||||
|
||||
widget.state.ok = false;
|
||||
await nextTick();
|
||||
|
||||
expect(fixture.innerHTML).toBe("<div></div>");
|
||||
expect(widget.refs.child).toEqual(child);
|
||||
|
||||
widget.state.ok = true;
|
||||
await nextTick();
|
||||
|
||||
expect(fixture.innerHTML).toBe("<div><span>Hello</span></div>");
|
||||
expect(widget.refs.child).toEqual(child);
|
||||
});
|
||||
|
||||
test("sub widgets rendered in a loop", async () => {
|
||||
env.qweb.addTemplate("ChildWidget", `<span><t t-esc="props.n"/></span>`);
|
||||
class ChildWidget extends Widget {}
|
||||
@@ -1128,7 +1186,7 @@ describe("composition", () => {
|
||||
`
|
||||
<div>
|
||||
<t t-foreach="state.numbers" t-as="number">
|
||||
<t t-widget="ChildWidget" t-key="number" t-props="{n: number}"/>
|
||||
<t t-widget="ChildWidget" t-key="number" n="number"/>
|
||||
</t>
|
||||
</div>`
|
||||
);
|
||||
@@ -1229,13 +1287,43 @@ describe("composition", () => {
|
||||
`)
|
||||
);
|
||||
});
|
||||
|
||||
test("t-widget with dynamic value", async () => {
|
||||
env.qweb.addTemplate(
|
||||
"ParentWidget",
|
||||
`<div><t t-widget="{{state.widget}}"/></div>`
|
||||
);
|
||||
class ParentWidget extends Widget {
|
||||
widgets = { WidgetB };
|
||||
state = { widget: "WidgetB" };
|
||||
}
|
||||
const widget = new ParentWidget(env);
|
||||
await widget.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><div>world</div></div>");
|
||||
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("t-widget with dynamic value 2", async () => {
|
||||
env.qweb.addTemplate(
|
||||
"ParentWidget",
|
||||
`<div><t t-widget="Widget{{state.widget}}"/></div>`
|
||||
);
|
||||
class ParentWidget extends Widget {
|
||||
widgets = { WidgetB };
|
||||
state = { widget: "B" };
|
||||
}
|
||||
const widget = new ParentWidget(env);
|
||||
await widget.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><div>world</div></div>");
|
||||
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("props evaluation (with t-props directive)", () => {
|
||||
describe("props evaluation ", () => {
|
||||
test("explicit object prop", async () => {
|
||||
env.qweb.addTemplate(
|
||||
"Parent",
|
||||
`<div><t t-widget="child" t-props="{value: state.val}"/></div>`
|
||||
`<div><t t-widget="child" value="state.val"/></div>`
|
||||
);
|
||||
class Parent extends Widget {
|
||||
widgets = { child: Child };
|
||||
@@ -1256,37 +1344,13 @@ describe("props evaluation (with t-props directive)", () => {
|
||||
expect(fixture.innerHTML).toBe("<div><span>42</span></div>");
|
||||
});
|
||||
|
||||
test("object prop value", async () => {
|
||||
env.qweb.addTemplate(
|
||||
"Parent",
|
||||
`<div><t t-widget="child" t-props="state"/></div>`
|
||||
);
|
||||
class Parent extends Widget {
|
||||
widgets = { child: Child };
|
||||
state = { val: 42 };
|
||||
}
|
||||
|
||||
env.qweb.addTemplate("Child", `<span><t t-esc="state.someval"/></span>`);
|
||||
class Child extends Widget {
|
||||
state: { someval: number };
|
||||
constructor(parent: Parent, props: { val: number }) {
|
||||
super(parent);
|
||||
this.state = { someval: props.val };
|
||||
}
|
||||
}
|
||||
|
||||
const widget = new Parent(env);
|
||||
await widget.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><span>42</span></div>");
|
||||
});
|
||||
|
||||
test("accept ES6-like syntax for props (with getters)", async () => {
|
||||
env.qweb.addTemplate("Child", `<span><t t-esc="props.greetings"/></span>`);
|
||||
class Child extends Widget {}
|
||||
|
||||
env.qweb.addTemplate(
|
||||
"Parent",
|
||||
`<div><t t-widget="child" t-props="{greetings}"/></div>`
|
||||
`<div><t t-widget="child" greetings="greetings"/></div>`
|
||||
);
|
||||
class Parent extends Widget {
|
||||
widgets = { child: Child };
|
||||
@@ -1299,13 +1363,13 @@ describe("props evaluation (with t-props directive)", () => {
|
||||
expect(fixture.innerHTML).toBe("<div><span>hello aaron</span></div>");
|
||||
});
|
||||
|
||||
test("t-set works with t-props", async () => {
|
||||
test("t-set works ", async () => {
|
||||
env.qweb.addTemplate(
|
||||
"Parent",
|
||||
`
|
||||
<div>
|
||||
<t t-set="val" t-value="42"/>
|
||||
<t t-widget="child" t-props="{val:val}"/>
|
||||
<t t-widget="child" val="val"/>
|
||||
</div>`
|
||||
);
|
||||
class Parent extends Widget {
|
||||
@@ -1419,28 +1483,257 @@ describe("class and style attributes with t-widget", () => {
|
||||
|
||||
describe("other directives with t-widget", () => {
|
||||
test("t-on works as expected", async () => {
|
||||
let n = 0;
|
||||
env.qweb.addTemplate(
|
||||
"ParentWidget",
|
||||
`<div><t t-widget="child" t-on-customevent="someMethod"/></div>`
|
||||
);
|
||||
expect.assertions(4);
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="ParentWidget"><t t-widget="child" t-on-custom-event="someMethod"/></div>
|
||||
</templates>
|
||||
`);
|
||||
class ParentWidget extends Widget {
|
||||
widgets = { child: Child };
|
||||
someMethod(arg) {
|
||||
expect(arg).toBe(43);
|
||||
n++;
|
||||
n = 0;
|
||||
someMethod(ev) {
|
||||
expect(ev.detail).toBe(43);
|
||||
this.n++;
|
||||
}
|
||||
}
|
||||
class Child extends Widget {}
|
||||
const widget = new ParentWidget(env);
|
||||
await widget.mount(fixture);
|
||||
let child = children(widget)[0];
|
||||
expect(n).toBe(0);
|
||||
child.trigger("customevent", 43);
|
||||
expect(n).toBe(1);
|
||||
expect(widget.n).toBe(0);
|
||||
child.trigger("custom-event", 43);
|
||||
expect(widget.n).toBe(1);
|
||||
child.destroy();
|
||||
child.trigger("customevent", 43);
|
||||
expect(n).toBe(1);
|
||||
child.trigger("custom-event", 43);
|
||||
expect(widget.n).toBe(1);
|
||||
});
|
||||
|
||||
test("t-on with handler bound to argument", async () => {
|
||||
expect.assertions(3);
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="ParentWidget"><t t-widget="child" t-on-ev="onEv(3)"/></div>
|
||||
</templates>
|
||||
`);
|
||||
class ParentWidget extends Widget {
|
||||
widgets = { child: Child };
|
||||
onEv(n, ev) {
|
||||
expect(n).toBe(3);
|
||||
expect(ev.detail).toBe(43);
|
||||
}
|
||||
}
|
||||
class Child extends Widget {}
|
||||
const widget = new ParentWidget(env);
|
||||
await widget.mount(fixture);
|
||||
let child = children(widget)[0];
|
||||
child.trigger("ev", 43);
|
||||
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("t-on with handler bound to object", async () => {
|
||||
expect.assertions(3);
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="ParentWidget"><t t-widget="child" t-on-ev="onEv({val: 3})"/></div>
|
||||
</templates>
|
||||
`);
|
||||
class ParentWidget extends Widget {
|
||||
widgets = { child: Child };
|
||||
onEv(o, ev) {
|
||||
expect(o).toEqual({ val: 3 });
|
||||
expect(ev.detail).toBe(43);
|
||||
}
|
||||
}
|
||||
class Child extends Widget {}
|
||||
const widget = new ParentWidget(env);
|
||||
await widget.mount(fixture);
|
||||
let child = children(widget)[0];
|
||||
child.trigger("ev", 43);
|
||||
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("t-on with handler bound to empty object", async () => {
|
||||
expect.assertions(3);
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="ParentWidget"><t t-widget="child" t-on-ev="onEv({})"/></div>
|
||||
</templates>
|
||||
`);
|
||||
class ParentWidget extends Widget {
|
||||
widgets = { child: Child };
|
||||
onEv(o, ev) {
|
||||
expect(o).toEqual({});
|
||||
expect(ev.detail).toBe(43);
|
||||
}
|
||||
}
|
||||
class Child extends Widget {}
|
||||
const widget = new ParentWidget(env);
|
||||
await widget.mount(fixture);
|
||||
let child = children(widget)[0];
|
||||
child.trigger("ev", 43);
|
||||
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("t-on with handler bound to empty object (with non empty inner string)", async () => {
|
||||
expect.assertions(3);
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="ParentWidget"><t t-widget="child" t-on-ev="onEv({ })"/></div>
|
||||
</templates>
|
||||
`);
|
||||
class ParentWidget extends Widget {
|
||||
widgets = { child: Child };
|
||||
onEv(o, ev) {
|
||||
expect(o).toEqual({});
|
||||
expect(ev.detail).toBe(43);
|
||||
}
|
||||
}
|
||||
class Child extends Widget {}
|
||||
const widget = new ParentWidget(env);
|
||||
await widget.mount(fixture);
|
||||
let child = children(widget)[0];
|
||||
child.trigger("ev", 43);
|
||||
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("t-on with stop and/or prevent modifiers", async () => {
|
||||
expect.assertions(7);
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="ParentWidget">
|
||||
<t t-widget="child"
|
||||
t-on-ev-1.stop="onEv1"
|
||||
t-on-ev-2.prevent="onEv2"
|
||||
t-on-ev-3.stop.prevent="onEv3"/>
|
||||
</div>
|
||||
</templates>
|
||||
`);
|
||||
class ParentWidget extends Widget {
|
||||
widgets = { child: Child };
|
||||
onEv1(ev) {
|
||||
expect(ev.defaultPrevented).toBe(false);
|
||||
expect(ev.cancelBubble).toBe(true);
|
||||
}
|
||||
onEv2(ev) {
|
||||
expect(ev.defaultPrevented).toBe(true);
|
||||
expect(ev.cancelBubble).toBe(false);
|
||||
}
|
||||
onEv3(ev) {
|
||||
expect(ev.defaultPrevented).toBe(true);
|
||||
expect(ev.cancelBubble).toBe(true);
|
||||
}
|
||||
}
|
||||
class Child extends Widget {}
|
||||
const widget = new ParentWidget(env);
|
||||
await widget.mount(fixture);
|
||||
|
||||
const child = children(widget)[0];
|
||||
child.trigger("ev-1");
|
||||
child.trigger("ev-2");
|
||||
child.trigger("ev-3");
|
||||
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("t-on with self modifier", async () => {
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="ParentWidget">
|
||||
<t t-widget="child" t-on-ev-1="onEv1" t-on-ev-2.self="onEv2"/>
|
||||
</div>
|
||||
<div t-name="Child"><t t-widget="child"/></div>
|
||||
</templates>
|
||||
`);
|
||||
const steps: string[] = [];
|
||||
class ParentWidget extends Widget {
|
||||
widgets = { child: Child };
|
||||
onEv1(ev) {
|
||||
steps.push("onEv1");
|
||||
}
|
||||
onEv2(ev) {
|
||||
steps.push("onEv2");
|
||||
}
|
||||
}
|
||||
class Child extends Widget {
|
||||
widgets = { child: GrandChild };
|
||||
}
|
||||
class GrandChild extends Widget {}
|
||||
const widget = new ParentWidget(env);
|
||||
await widget.mount(fixture);
|
||||
|
||||
const child = children(widget)[0];
|
||||
const grandChild = children(child)[0];
|
||||
child.trigger("ev-1");
|
||||
child.trigger("ev-2");
|
||||
expect(steps).toEqual(["onEv1", "onEv2"]);
|
||||
grandChild.trigger("ev-1");
|
||||
grandChild.trigger("ev-2");
|
||||
expect(steps).toEqual(["onEv1", "onEv2", "onEv1"]);
|
||||
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("t-on with self and prevent modifiers (order matters)", async () => {
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="ParentWidget">
|
||||
<t t-widget="child" t-on-ev.self.prevent="onEv"/>
|
||||
</div>
|
||||
<div t-name="Child"><t t-widget="child"/></div>
|
||||
</templates>
|
||||
`);
|
||||
const steps: boolean[] = [];
|
||||
class ParentWidget extends Widget {
|
||||
widgets = { child: Child };
|
||||
onEv() {}
|
||||
}
|
||||
class Child extends Widget {
|
||||
widgets = { child: GrandChild };
|
||||
}
|
||||
class GrandChild extends Widget {}
|
||||
const widget = new ParentWidget(env);
|
||||
await widget.mount(fixture);
|
||||
(<HTMLElement>fixture).addEventListener("ev", function(e) {
|
||||
steps.push(e.defaultPrevented);
|
||||
});
|
||||
|
||||
const child = children(widget)[0];
|
||||
const grandChild = children(child)[0];
|
||||
child.trigger("ev");
|
||||
grandChild.trigger("ev");
|
||||
expect(steps).toEqual([true, false]);
|
||||
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("t-on with prevent and self modifiers (order matters)", async () => {
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="ParentWidget">
|
||||
<t t-widget="child" t-on-ev.prevent.self="onEv"/>
|
||||
</div>
|
||||
<div t-name="Child"><t t-widget="child"/></div>
|
||||
</templates>
|
||||
`);
|
||||
const steps: boolean[] = [];
|
||||
class ParentWidget extends Widget {
|
||||
widgets = { child: Child };
|
||||
onEv() {}
|
||||
}
|
||||
class Child extends Widget {
|
||||
widgets = { child: GrandChild };
|
||||
}
|
||||
class GrandChild extends Widget {}
|
||||
const widget = new ParentWidget(env);
|
||||
await widget.mount(fixture);
|
||||
(<HTMLElement>fixture).addEventListener("ev", function(e) {
|
||||
steps.push(e.defaultPrevented);
|
||||
});
|
||||
|
||||
const child = children(widget)[0];
|
||||
const grandChild = children(child)[0];
|
||||
child.trigger("ev");
|
||||
grandChild.trigger("ev");
|
||||
expect(steps).toEqual([true, true]);
|
||||
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("t-if works with t-widget", async () => {
|
||||
@@ -1555,7 +1848,7 @@ describe("random stuff/miscellaneous", () => {
|
||||
// directive as a key
|
||||
env.qweb.addTemplate(
|
||||
"Test",
|
||||
`<div><t t-foreach="2">txt</t><t t-widget="widget"/></div>`
|
||||
`<div><t t-foreach="Array(2)">txt</t><t t-widget="widget"/></div>`
|
||||
);
|
||||
class Test extends Widget {
|
||||
widgets = { widget: Widget };
|
||||
@@ -1571,7 +1864,7 @@ describe("random stuff/miscellaneous", () => {
|
||||
// twice.
|
||||
env.qweb.addTemplate(
|
||||
"Parent",
|
||||
`<div><t t-widget="child" t-props="{flag:state.flag}"/></div>`
|
||||
`<div><t t-widget="child" flag="state.flag"/></div>`
|
||||
);
|
||||
class Parent extends Widget {
|
||||
widgets = { child: Child };
|
||||
@@ -1595,7 +1888,7 @@ describe("random stuff/miscellaneous", () => {
|
||||
test("snapshotting compiled code", async () => {
|
||||
env.qweb.addTemplate(
|
||||
"Parent",
|
||||
`<div><t t-widget="child" t-key="'somestring'" t-props="{flag:state.flag}"/></div>`
|
||||
`<div><t t-widget="child" t-key="'somestring'" flag="state.flag"/></div>`
|
||||
);
|
||||
class Parent extends Widget {
|
||||
widgets = { child: Child };
|
||||
@@ -1613,20 +1906,6 @@ describe("random stuff/miscellaneous", () => {
|
||||
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("t-props should not be undefined (snapshotting)", async () => {
|
||||
env.qweb.addTemplate("Parent", `<div><t t-widget="child"/></div>`);
|
||||
class Parent extends Widget {
|
||||
widgets = { child: Child };
|
||||
}
|
||||
|
||||
env.qweb.addTemplate("Child", `<span>abc</span>`);
|
||||
class Child extends Widget {}
|
||||
|
||||
const widget = new Parent(env);
|
||||
await widget.mount(fixture);
|
||||
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("component semantics", async () => {
|
||||
let steps: string[] = [];
|
||||
let c: C;
|
||||
@@ -1775,7 +2054,6 @@ describe("random stuff/miscellaneous", () => {
|
||||
"C:_patch",
|
||||
"E:willUnmount",
|
||||
"E:destroy",
|
||||
"E:destroy", // maybe should look into this
|
||||
"F:_patch(from _mount)",
|
||||
"F:mounted",
|
||||
"D:_patch",
|
||||
@@ -1809,7 +2087,7 @@ describe("async rendering", () => {
|
||||
let n = 0;
|
||||
env.qweb.addTemplate(
|
||||
"W",
|
||||
`<div><t t-if="state.val > 1"><t t-widget="Child" t-props="{val: state.val}"/></t></div>`
|
||||
`<div><t t-if="state.val > 1"><t t-widget="Child" val="state.val"/></t></div>`
|
||||
);
|
||||
class W extends Widget {
|
||||
widgets = { Child };
|
||||
@@ -1910,8 +2188,8 @@ describe("async rendering", () => {
|
||||
"Parent",
|
||||
`
|
||||
<div>
|
||||
<t t-widget="ChildA" t-props="{val:state.valA}"/>
|
||||
<t t-if="state.flagB"><t t-widget="ChildB" t-props="{val:state.valB}"/></t>
|
||||
<t t-widget="ChildA" val="state.valA"/>
|
||||
<t t-if="state.flagB"><t t-widget="ChildB" val="state.valB"/></t>
|
||||
</div>`
|
||||
);
|
||||
class Parent extends Widget {
|
||||
@@ -1961,7 +2239,7 @@ describe("async rendering", () => {
|
||||
<ul>
|
||||
<t t-foreach="items" t-as="item">
|
||||
<li t-key="'li_'+item">
|
||||
<t t-widget="Child" t-props="{ item }"/>
|
||||
<t t-widget="Child" item="item"/>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
@@ -2005,7 +2283,7 @@ describe("async rendering", () => {
|
||||
env.qweb.addTemplate(
|
||||
"Parent",
|
||||
`
|
||||
<div><t t-if="state.flag"><t t-widget="Child" t-props="{val: state.val}"/></t></div>`
|
||||
<div><t t-if="state.flag"><t t-widget="Child" val="state.val"/></t></div>`
|
||||
);
|
||||
class Parent extends Widget {
|
||||
widgets = { Child };
|
||||
@@ -2030,6 +2308,50 @@ describe("async rendering", () => {
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div></div>");
|
||||
});
|
||||
|
||||
test("reuse widget if possible, in some async situation", async () => {
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<span t-name="ChildA">a<t t-esc="props.val"/></span>
|
||||
<span t-name="ChildB">b<t t-esc="props.val"/></span>
|
||||
<span t-name="Parent">
|
||||
<t t-if="state.flag">
|
||||
<t t-widget="ChildA" val="state.valA"/>
|
||||
<t t-widget="ChildB" val="state.valB"/>
|
||||
</t>
|
||||
</span>
|
||||
</templates>
|
||||
`);
|
||||
|
||||
let destroyCount = 0;
|
||||
class ChildA extends Widget {
|
||||
destroy() {
|
||||
destroyCount++;
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
class ChildB extends Widget {
|
||||
willStart(): any {
|
||||
return new Promise(function() {});
|
||||
}
|
||||
}
|
||||
class Parent extends Widget {
|
||||
widgets = { ChildA, ChildB };
|
||||
state = { valA: 1, valB: 2, flag: false };
|
||||
}
|
||||
const parent = new Parent(env);
|
||||
await parent.mount(fixture);
|
||||
|
||||
expect(destroyCount).toBe(0);
|
||||
|
||||
parent.state.flag = true;
|
||||
await nextTick();
|
||||
expect(destroyCount).toBe(0);
|
||||
|
||||
parent.state.valB = 3;
|
||||
await nextTick();
|
||||
expect(destroyCount).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updating environment", () => {
|
||||
@@ -2155,7 +2477,7 @@ describe("widget and observable state", () => {
|
||||
expect.assertions(1);
|
||||
env.qweb.addTemplate(
|
||||
"Parent",
|
||||
`<div><t t-widget="Child" t-props="state.obj"/></div>`
|
||||
`<div><t t-widget="Child" obj="state.obj"/></div>`
|
||||
);
|
||||
class Parent extends Widget {
|
||||
state = { obj: { coffee: 1 } };
|
||||
@@ -2164,7 +2486,7 @@ describe("widget and observable state", () => {
|
||||
class Child extends Widget {
|
||||
constructor(parent, props) {
|
||||
super(parent, props);
|
||||
props.coffee = 2;
|
||||
props.obj.coffee = 2;
|
||||
}
|
||||
}
|
||||
const parent = new Parent(env);
|
||||
@@ -2176,24 +2498,6 @@ describe("widget and observable state", () => {
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("widget can add observed keys to its state", async () => {
|
||||
env.qweb.addTemplate(
|
||||
"TestWidget",
|
||||
`<div><t t-esc="state.a"/><t t-esc="state.b"/></div>`
|
||||
);
|
||||
class TestWidget extends Widget {
|
||||
state: any = { a: 1 };
|
||||
}
|
||||
const widget = new TestWidget(env);
|
||||
await widget.mount(fixture);
|
||||
|
||||
expect(fixture.innerHTML).toBe("<div>1</div>");
|
||||
|
||||
widget.set(widget.state, "b", 2);
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div>12</div>");
|
||||
});
|
||||
});
|
||||
|
||||
describe("t-mounted directive", () => {
|
||||
@@ -2253,7 +2557,7 @@ describe("t-mounted directive", () => {
|
||||
test("combined with a t-ref", async () => {
|
||||
env.qweb.addTemplate(
|
||||
"TestWidget",
|
||||
`<div><input t-ref="'input'" t-mounted="f"/></div>`
|
||||
`<div><input t-ref="input" t-mounted="f"/></div>`
|
||||
);
|
||||
class TestWidget extends Widget {
|
||||
f() {}
|
||||
@@ -2264,25 +2568,6 @@ describe("t-mounted directive", () => {
|
||||
expect(widget.refs.input).toBeDefined();
|
||||
expect(widget.f).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("combined with a t-transition", async () => {
|
||||
env.qweb.addTemplate(
|
||||
"TestWidget",
|
||||
`<div><span t-if="state.flag" t-mounted="f" t-transition="chimay">blue</span></div>`
|
||||
);
|
||||
class TestWidget extends Widget {
|
||||
state = { flag: false };
|
||||
f() {}
|
||||
}
|
||||
const widget = new TestWidget(env);
|
||||
widget.f = jest.fn();
|
||||
await widget.mount(fixture);
|
||||
expect(widget.f).toHaveBeenCalledTimes(0);
|
||||
|
||||
widget.state.flag = true;
|
||||
await nextTick();
|
||||
expect(widget.f).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("can deduce template from name", () => {
|
||||
@@ -2328,3 +2613,104 @@ describe("can deduce template from name", () => {
|
||||
expect(fixture.innerHTML).toBe("<span>Rochefort 10</span>");
|
||||
});
|
||||
});
|
||||
|
||||
describe("t-slot directive", () => {
|
||||
test("can define and call slots", async () => {
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="Parent">
|
||||
<t t-widget="Dialog">
|
||||
<t t-set="header"><span>header</span></t>
|
||||
<t t-set="footer"><span>footer</span></t>
|
||||
</t>
|
||||
</div>
|
||||
<div t-name="Dialog">
|
||||
<div><t t-slot="header"/></div>
|
||||
<div><t t-slot="footer"/></div>
|
||||
</div>
|
||||
</templates>
|
||||
`);
|
||||
class Dialog extends Widget {}
|
||||
class Parent extends Widget {
|
||||
widgets = { Dialog };
|
||||
}
|
||||
const parent = new Parent(env);
|
||||
await parent.mount(fixture);
|
||||
|
||||
expect(fixture.innerHTML).toBe(
|
||||
"<div><div><div><span>header</span></div><div><span>footer</span></div></div></div>"
|
||||
);
|
||||
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
|
||||
expect(env.qweb.templates.Dialog.fn.toString()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("slots are rendered with proper context", async () => {
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="Parent">
|
||||
<span class="counter"><t t-esc="state.val"/></span>
|
||||
<t t-widget="Dialog">
|
||||
<t t-set="footer"><button t-on-click="doSomething">do something</button></t>
|
||||
</t>
|
||||
</div>
|
||||
<span t-name="Dialog"><t t-slot="footer"/></span>
|
||||
</templates>
|
||||
`);
|
||||
class Dialog extends Widget {}
|
||||
class Parent extends Widget {
|
||||
widgets = { Dialog };
|
||||
state = { val: 0 };
|
||||
doSomething() {
|
||||
this.state.val++;
|
||||
}
|
||||
}
|
||||
const parent = new Parent(env);
|
||||
await parent.mount(fixture);
|
||||
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="counter">0</span><span><button>do something</button></span></div>'
|
||||
);
|
||||
|
||||
fixture.querySelector("button")!.click();
|
||||
await nextTick();
|
||||
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="counter">1</span><span><button>do something</button></span></div>'
|
||||
);
|
||||
});
|
||||
|
||||
test("refs are properly bound in slots", async () => {
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="Parent">
|
||||
<span class="counter"><t t-esc="state.val"/></span>
|
||||
<t t-widget="Dialog">
|
||||
<t t-set="footer"><button t-ref="myButton" t-on-click="doSomething">do something</button></t>
|
||||
</t>
|
||||
</div>
|
||||
<span t-name="Dialog"><t t-slot="footer"/></span>
|
||||
</templates>
|
||||
`);
|
||||
class Dialog extends Widget {}
|
||||
class Parent extends Widget {
|
||||
widgets = { Dialog };
|
||||
state = { val: 0 };
|
||||
doSomething() {
|
||||
this.state.val++;
|
||||
}
|
||||
}
|
||||
const parent = new Parent(env);
|
||||
await parent.mount(fixture);
|
||||
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="counter">0</span><span><button>do something</button></span></div>'
|
||||
);
|
||||
|
||||
(<any>parent.refs.myButton).click();
|
||||
await nextTick();
|
||||
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="counter">1</span><span><button>do something</button></span></div>'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Doc Link Checker
|
||||
*
|
||||
* We define here a test to make sure that there are no dead link in the Owl
|
||||
* documentation.
|
||||
*/
|
||||
import * as fs from "fs";
|
||||
|
||||
const LINK_REGEXP = /\[([^\[]+)\]\(([^\)]+)\)/g;
|
||||
const HEADING_REGEXP = /\n(#+\s*)(.*)/g;
|
||||
|
||||
// files to be checked
|
||||
function getFiles(): string[] {
|
||||
const DOCFILES = fs.readdirSync("doc").map(f => `doc/${f}`);
|
||||
const MAINREADME = "README.md";
|
||||
return DOCFILES.concat(MAINREADME);
|
||||
}
|
||||
|
||||
test("All markdown links work", () => {
|
||||
let linkNumber = 0;
|
||||
let invalidLinkNumber = 0;
|
||||
const files = getFiles();
|
||||
const data = readDocData(files);
|
||||
for (let file of data) {
|
||||
for (let link of file.links) {
|
||||
// DEBUG: uncomment next line
|
||||
// console.warn(`Checking "${link.name}" in "${file.name}"`);
|
||||
linkNumber++;
|
||||
if (!isLinkValid(link, file, data)) {
|
||||
console.warn(`Invalid Link: "${link.name}" in "${file.name}"`);
|
||||
invalidLinkNumber++;
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(invalidLinkNumber).toBe(0);
|
||||
expect(linkNumber).toBeGreaterThan(10);
|
||||
});
|
||||
|
||||
interface MarkDownLink {
|
||||
name: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
interface MarkDownSection {
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
interface FileData {
|
||||
name: string;
|
||||
links: MarkDownLink[];
|
||||
sections: MarkDownSection[];
|
||||
}
|
||||
|
||||
function isLinkValid(
|
||||
link: MarkDownLink,
|
||||
current: FileData,
|
||||
files: FileData[]
|
||||
): boolean {
|
||||
const parts = link.link.split("#");
|
||||
const currentParts = current.name.split("/");
|
||||
const path = currentParts.length > 1 ? currentParts[0] + '/' : "";
|
||||
const fullName = path + parts[0];
|
||||
if (parts.length === 1) {
|
||||
// no # in url
|
||||
if (parts[0].endsWith(".md")) {
|
||||
// it is a local md file
|
||||
if (!files.find(f => f.name === fullName)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const file =
|
||||
parts[0] === "" ? current : files.find(f => f.name === fullName);
|
||||
if (!file) {
|
||||
return false;
|
||||
}
|
||||
if (!file.sections.find(s => s.slug === parts[1])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// adapted from https://medium.com/@mhagemann/the-ultimate-way-to-slugify-a-url-string-in-javascript-b8e4a0d849e1
|
||||
function slugify(str) {
|
||||
const a = "àáäâãåăæçèéëêǵḧìíïîḿńǹñòóöôœøṕŕßśșțùúüûǘẃẍÿź·_,:;";
|
||||
const b = "aaaaaaaaceeeeghiiiimnnnooooooprssstuuuuuwxyz-----";
|
||||
const p = new RegExp(a.split("").join("|"), "g");
|
||||
return str
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.replace(/\//g, "") // remove /
|
||||
.replace(/\s+/g, "-") // Replace spaces with -
|
||||
.replace(p, c => b.charAt(a.indexOf(c))) // Replace special characters
|
||||
.replace(/&/g, "-and-") // Replace & with ‘and’
|
||||
.replace(/[^\w\-]+/g, "") // Remove all non-word characters
|
||||
.replace(/\-\-+/g, "-") // Replace multiple - with single -
|
||||
.replace(/^-+/, "") // Trim - from start of text
|
||||
.replace(/-+$/, ""); // Trim - from end of text
|
||||
}
|
||||
|
||||
function readDocData(files: string[]): FileData[] {
|
||||
const result: FileData[] = [];
|
||||
|
||||
for (let file of files) {
|
||||
const fileData: FileData = {
|
||||
name: file,
|
||||
links: [],
|
||||
sections: []
|
||||
};
|
||||
const content = fs.readFileSync(file, { encoding: "utf8" });
|
||||
let m;
|
||||
// get links info
|
||||
do {
|
||||
m = LINK_REGEXP.exec(content);
|
||||
if (m) {
|
||||
fileData.links.push({ name: m[0], link: m[2] });
|
||||
}
|
||||
} while (m);
|
||||
// get sections info
|
||||
do {
|
||||
m = HEADING_REGEXP.exec(content);
|
||||
if (m) {
|
||||
fileData.sections.push({ name: m[0], slug: slugify(m[2]) });
|
||||
}
|
||||
} while (m);
|
||||
|
||||
result.push(fileData);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
+50
-1
@@ -1,5 +1,6 @@
|
||||
import { Env } from "../src/component";
|
||||
import { QWeb } from "../src/qweb_core";
|
||||
import { EvalContext, QWeb, UTILS } from "../src/qweb_core";
|
||||
import { patch } from "../src/vdom";
|
||||
import "../src/qweb_directives";
|
||||
import "../src/qweb_extensions";
|
||||
|
||||
@@ -42,3 +43,51 @@ export function makeTestEnv(): Env {
|
||||
qweb: new QWeb()
|
||||
};
|
||||
}
|
||||
|
||||
export function trim(str: string): string {
|
||||
return str.replace(/\s/g, "");
|
||||
}
|
||||
|
||||
export function renderToDOM(
|
||||
qweb: QWeb,
|
||||
template: string,
|
||||
context: EvalContext = {},
|
||||
extra?: any
|
||||
): HTMLElement | Text {
|
||||
const vnode = qweb.render(template, context, extra);
|
||||
|
||||
// we snapshot here the compiled code. This is useful to prevent unwanted code
|
||||
// change.
|
||||
expect(qweb.templates[template].fn.toString()).toMatchSnapshot();
|
||||
|
||||
if (vnode.sel === undefined) {
|
||||
return document.createTextNode(vnode.text!);
|
||||
}
|
||||
const node = document.createElement(vnode.sel!);
|
||||
const result = patch(node, vnode);
|
||||
return result.elm as HTMLElement;
|
||||
}
|
||||
|
||||
export function renderToString(
|
||||
qweb: QWeb,
|
||||
t: string,
|
||||
context: EvalContext = {}
|
||||
): string {
|
||||
const node = renderToDOM(qweb, t, context);
|
||||
return node instanceof Text ? node.textContent! : node.outerHTML;
|
||||
}
|
||||
|
||||
// hereafter, we define two helpers to patch/unpatch the nextFrame utils. This
|
||||
// is useful for animations tests, as we hook before repaints to trigger
|
||||
// animations (thanks to requestAnimationFrame). Patching nextFrame allows to
|
||||
// simulate calls to this hook. One must not forget to unpatch afterwards.
|
||||
let nextFrame = UTILS.nextFrame;
|
||||
export function patchNextFrame(f: Function) {
|
||||
UTILS.nextFrame = (cb: () => void) => {
|
||||
setTimeout(() => f(cb));
|
||||
};
|
||||
}
|
||||
|
||||
export function unpatchNextFrame() {
|
||||
UTILS.nextFrame = nextFrame;
|
||||
}
|
||||
|
||||
@@ -134,6 +134,57 @@ describe("observer", () => {
|
||||
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: [] };
|
||||
@@ -230,6 +281,26 @@ describe("observer", () => {
|
||||
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 };
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
import { Component, Env } from "../src/component";
|
||||
import { makeTestFixture, makeTestEnv } from "./helpers";
|
||||
import { QWeb } from "../src";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Setup and helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
let fixture: HTMLElement;
|
||||
let env: Env;
|
||||
let dev: boolean = false;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = makeTestFixture();
|
||||
env = makeTestEnv();
|
||||
dev = QWeb.dev;
|
||||
QWeb.dev = true;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.remove();
|
||||
QWeb.dev = dev;
|
||||
});
|
||||
|
||||
class Widget extends Component<any, any, any> {}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Tests
|
||||
//------------------------------------------------------------------------------
|
||||
describe("props validation", () => {
|
||||
test("validation is only done in dev mode", async () => {
|
||||
class TestWidget extends Widget {
|
||||
static props = ["message"];
|
||||
}
|
||||
|
||||
QWeb.dev = true;
|
||||
expect(() => {
|
||||
new TestWidget(env);
|
||||
}).toThrow();
|
||||
|
||||
QWeb.dev = false;
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
test("props validation is also done on update props", async () => {
|
||||
expect.assertions(1);
|
||||
class TestWidget extends Widget {
|
||||
static props = ["message"];
|
||||
}
|
||||
const w = new TestWidget(env, { message: "bottle" });
|
||||
try {
|
||||
await w._updateProps({});
|
||||
} catch (e) {
|
||||
expect(e.message).toBe("Missing props 'message' (widget 'TestWidget')");
|
||||
}
|
||||
});
|
||||
|
||||
test("props: list of strings", async () => {
|
||||
class TestWidget extends Widget {
|
||||
static props = ["message"];
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env);
|
||||
}).toThrow("Missing props 'message' (widget 'TestWidget')");
|
||||
});
|
||||
|
||||
test("validate simple types", async () => {
|
||||
const Tests = [
|
||||
{ type: Number, ok: 1, ko: "1" },
|
||||
{ type: Boolean, ok: true, ko: "1" },
|
||||
{ type: String, ok: "1", ko: 1 },
|
||||
{ type: Object, ok: {}, ko: "1" },
|
||||
{ type: Date, ok: new Date(), ko: "1" },
|
||||
{ type: Function, ok: () => {}, ko: "1" }
|
||||
];
|
||||
|
||||
for (let test of Tests) {
|
||||
let TestWidget = class extends Widget {
|
||||
static props = { p: test.type };
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env);
|
||||
}).toThrow("Missing props 'p'");
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: test.ok });
|
||||
}).not.toThrow();
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: test.ko });
|
||||
}).toThrow("Props 'p' of invalid type in widget");
|
||||
}
|
||||
});
|
||||
|
||||
test("validate simple types, alternate form", async () => {
|
||||
const Tests = [
|
||||
{ type: Number, ok: 1, ko: "1" },
|
||||
{ type: Boolean, ok: true, ko: "1" },
|
||||
{ type: String, ok: "1", ko: 1 },
|
||||
{ type: Object, ok: {}, ko: "1" },
|
||||
{ type: Date, ok: new Date(), ko: "1" },
|
||||
{ type: Function, ok: () => {}, ko: "1" }
|
||||
];
|
||||
|
||||
for (let test of Tests) {
|
||||
let TestWidget = class extends Widget {
|
||||
static props = { p: { type: test.type } };
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env);
|
||||
}).toThrow("Missing props 'p'");
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: test.ok });
|
||||
}).not.toThrow();
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: test.ko });
|
||||
}).toThrow("Props 'p' of invalid type in widget");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
test("can validate a prop with multiple types", async () => {
|
||||
let TestWidget = class extends Widget {
|
||||
static props = { p: [String, Boolean] };
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: "string" });
|
||||
new TestWidget(env, { p: true });
|
||||
}).not.toThrow();
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: 1 });
|
||||
}).toThrow("Props 'p' of invalid type in widget");
|
||||
});
|
||||
|
||||
test("can validate an optional props", async () => {
|
||||
let TestWidget = class extends Widget {
|
||||
static props = { p: { type: String, optional: true } };
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: "hey" });
|
||||
new TestWidget(env, { });
|
||||
}).not.toThrow();
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: 1 });
|
||||
}).toThrow();
|
||||
|
||||
});
|
||||
|
||||
|
||||
test("can validate an array with given primitive type", async () => {
|
||||
let TestWidget = class extends Widget {
|
||||
static props = { p: { type: Array, element: String } };
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: [] });
|
||||
new TestWidget(env, { p: ["string"] });
|
||||
}).not.toThrow();
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: [1] });
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: ["string", 1] });
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
test("can validate an array with multiple sub element types", async () => {
|
||||
let TestWidget = class extends Widget {
|
||||
static props = { p: { type: Array, element: [String, Boolean] } };
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: [] });
|
||||
new TestWidget(env, { p: ["string"] });
|
||||
new TestWidget(env, { p: [false, true, "string"] });
|
||||
}).not.toThrow();
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: [true, 1] });
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
test("can validate an object with simple shape", async () => {
|
||||
let TestWidget = class extends Widget {
|
||||
static props = {
|
||||
p: { type: Object, shape: { id: Number, url: String } }
|
||||
};
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: { id: 1, url: "url" } });
|
||||
new TestWidget(env, { p: { id: 1, url: "url", extra: true } });
|
||||
}).not.toThrow();
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: { id: "1", url: "url" } });
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: { id: 1 } });
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
test("can validate recursively complicated prop def", async () => {
|
||||
let TestWidget = class extends Widget {
|
||||
static props = {
|
||||
p: {
|
||||
type: Object,
|
||||
shape: {
|
||||
id: Number,
|
||||
url: [Boolean, {type: Array, element: Number}],
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: { id: 1, url: true } });
|
||||
new TestWidget(env, { p: { id: 1, url: [12]} });
|
||||
}).not.toThrow();
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: { id: 1, url: [12, true]} });
|
||||
}).toThrow();
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("default props", () => {
|
||||
test("can set default values", async () => {
|
||||
class TestWidget extends Widget {
|
||||
static defaultProps = {p: 4}
|
||||
}
|
||||
|
||||
const w = new TestWidget(env, {});
|
||||
expect(w.props.p).toBe(4);
|
||||
});
|
||||
|
||||
test("default values are also set whenever component is updated", async () => {
|
||||
class TestWidget extends Widget {
|
||||
static defaultProps = {p: 4}
|
||||
}
|
||||
|
||||
const w = new TestWidget(env, {p: 1});
|
||||
await w._updateProps({});
|
||||
expect(w.props.p).toBe(4);
|
||||
});
|
||||
});
|
||||
+145
-83
@@ -1,6 +1,5 @@
|
||||
import { EvalContext, QWeb } from "../src/qweb_core";
|
||||
import { patch } from "../src/vdom";
|
||||
import { normalize } from "./helpers";
|
||||
import { QWeb } from "../src/qweb_core";
|
||||
import { normalize, renderToDOM, renderToString, trim } from "./helpers";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Setup and helpers
|
||||
@@ -15,39 +14,6 @@ beforeEach(() => {
|
||||
qweb = new QWeb();
|
||||
});
|
||||
|
||||
function trim(str: string): string {
|
||||
return str.replace(/\s/g, "");
|
||||
}
|
||||
|
||||
function renderToDOM(
|
||||
qweb: QWeb,
|
||||
template: string,
|
||||
context: EvalContext = {},
|
||||
extra?: any
|
||||
): HTMLElement | Text {
|
||||
const vnode = qweb.render(template, context, extra);
|
||||
|
||||
// we snapshot here the compiled code. This is useful to prevent unwanted code
|
||||
// change.
|
||||
expect(qweb.templates[template].fn.toString()).toMatchSnapshot();
|
||||
|
||||
if (vnode.sel === undefined) {
|
||||
return document.createTextNode(vnode.text!);
|
||||
}
|
||||
const node = document.createElement(vnode.sel!);
|
||||
const result = patch(node, vnode);
|
||||
return result.elm as HTMLElement;
|
||||
}
|
||||
|
||||
function renderToString(
|
||||
qweb: QWeb,
|
||||
t: string,
|
||||
context: EvalContext = {}
|
||||
): string {
|
||||
const node = renderToDOM(qweb, t, context);
|
||||
return node instanceof Text ? node.textContent! : node.outerHTML;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Tests
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -58,6 +24,16 @@ describe("static templates", () => {
|
||||
expect(renderToString(qweb, "test")).toBe("hello vdom");
|
||||
});
|
||||
|
||||
test("simple dynamic value", () => {
|
||||
qweb.addTemplate("test", "<t><t t-esc=\"text\"/></t>");
|
||||
expect(renderToString(qweb, "test", {text: "hello vdom"})).toBe("hello vdom");
|
||||
});
|
||||
|
||||
test("simple string, with some dynamic value", () => {
|
||||
qweb.addTemplate("test", "<t>hello <t t-esc=\"text\"/></t>");
|
||||
expect(renderToString(qweb, "test", {text: "vdom"})).toBe("hello vdom");
|
||||
});
|
||||
|
||||
test("empty div", () => {
|
||||
qweb.addTemplate("test", "<div></div>");
|
||||
expect(renderToString(qweb, "test")).toBe("<div></div>");
|
||||
@@ -118,16 +94,6 @@ describe("error handling", () => {
|
||||
).toThrow("Missing event name with t-on directive");
|
||||
});
|
||||
|
||||
test("error when compiled code is invalid", () => {
|
||||
qweb.addTemplate(
|
||||
"templatename",
|
||||
`<div t-att-hey="}/^function invalid{{>'"></div>`
|
||||
);
|
||||
expect(() => qweb.render("templatename")).toThrow(
|
||||
"Invalid generated code while compiling template 'templatename': Unexpected token }"
|
||||
);
|
||||
});
|
||||
|
||||
test("error when unknown directive", () => {
|
||||
qweb.addTemplate(
|
||||
"templatename",
|
||||
@@ -211,7 +177,7 @@ describe("t-set", () => {
|
||||
test("t-set and t-if", () => {
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div >
|
||||
`<div>
|
||||
<t t-set="v" t-value="value"/>
|
||||
<t t-if="v === 'ok'">grimbergen</t>
|
||||
</div>`
|
||||
@@ -512,7 +478,7 @@ describe("attributes", () => {
|
||||
});
|
||||
|
||||
test("format expression, other format", () => {
|
||||
qweb.addTemplate("test", `<div t-attf-foo="#{value + 37}"/>`);
|
||||
qweb.addTemplate("test", `<div t-attf-foo="{{value + 37}}"/>`);
|
||||
const result = renderToString(qweb, "test", { value: 5 });
|
||||
expect(result).toBe(`<div foo="42"></div>`);
|
||||
});
|
||||
@@ -556,6 +522,15 @@ describe("attributes", () => {
|
||||
expect(result).toBe(`<div class="hello world"></div>`);
|
||||
});
|
||||
|
||||
test("class and t-attf-class with ternary operation", () => {
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div class="hello" t-attf-class="{{value ? 'world' : ''}}"/>`
|
||||
);
|
||||
const result = renderToString(qweb, "test", { value: true });
|
||||
expect(result).toBe(`<div class="hello world"></div>`);
|
||||
});
|
||||
|
||||
test("t-att-class with object", () => {
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
@@ -678,25 +653,13 @@ describe("foreach", () => {
|
||||
"test",
|
||||
`
|
||||
<div>
|
||||
<t t-foreach="5" t-as="elem">
|
||||
-<t t-if="elem_first"> first</t><t t-if="elem_last"> last</t> (<t t-esc="elem_parity"/>)
|
||||
<t t-foreach="Array(5)" t-as="elem">
|
||||
-<t t-if="elem_first"> first</t><t t-if="elem_last"> last</t> (<t t-esc="elem_index"/>)
|
||||
</t>
|
||||
</div>`
|
||||
);
|
||||
const result = trim(renderToString(qweb, "test"));
|
||||
const expected = `<div>-first(even)-(odd)-(even)-(odd)-last(even)</div>`;
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
test("iterate, integer param", () => {
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div><t t-foreach="3" t-as="item">
|
||||
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/>]
|
||||
</t></div>`
|
||||
);
|
||||
const result = trim(renderToString(qweb, "test"));
|
||||
const expected = `<div>[0:00][1:11][2:22]</div>`;
|
||||
const expected = `<div>-first(0)-(1)-(2)-(3)-last(4)</div>`;
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
@@ -706,14 +669,14 @@ describe("foreach", () => {
|
||||
`
|
||||
<div>
|
||||
<t t-foreach="value" t-as="item">
|
||||
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/> - <t t-esc="item_parity"/>]
|
||||
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/>]
|
||||
</t>
|
||||
</div>`
|
||||
);
|
||||
const result = trim(
|
||||
renderToString(qweb, "test", { value: { a: 1, b: 2, c: 3 } })
|
||||
);
|
||||
const expected = `<div>[0:a1-even][1:b2-odd][2:c3-even]</div>`;
|
||||
const expected = `<div>[0:a1][1:b2][2:c3]</div>`;
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
@@ -917,7 +880,7 @@ describe("t-on", () => {
|
||||
(<HTMLElement>node).click();
|
||||
});
|
||||
|
||||
test("can bind handlers with empty object (with non empty inner string", () => {
|
||||
test("can bind handlers with empty object (with non empty inner string)", () => {
|
||||
expect.assertions(2);
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
@@ -969,20 +932,132 @@ describe("t-on", () => {
|
||||
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
|
||||
(<HTMLElement>node).click();
|
||||
});
|
||||
|
||||
test("t-on with prevent and/or stop modifiers", async () => {
|
||||
expect.assertions(7);
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div>
|
||||
<button t-on-click.prevent="onClickPrevented">Button 1</button>
|
||||
<button t-on-click.stop="onClickStopped">Button 2</button>
|
||||
<button t-on-click.prevent.stop="onClickPreventedAndStopped">Button 3</button>
|
||||
</div>`
|
||||
);
|
||||
let owner = {
|
||||
onClickPrevented(e) {
|
||||
expect(e.defaultPrevented).toBe(true);
|
||||
expect(e.cancelBubble).toBe(false);
|
||||
},
|
||||
onClickStopped(e) {
|
||||
expect(e.defaultPrevented).toBe(false);
|
||||
expect(e.cancelBubble).toBe(true);
|
||||
},
|
||||
onClickPreventedAndStopped(e) {
|
||||
expect(e.defaultPrevented).toBe(true);
|
||||
expect(e.cancelBubble).toBe(true);
|
||||
}
|
||||
};
|
||||
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
|
||||
|
||||
const buttons = (<HTMLElement>node).getElementsByTagName("button");
|
||||
buttons[0].click();
|
||||
buttons[1].click();
|
||||
buttons[2].click();
|
||||
});
|
||||
|
||||
test("t-on with self modifier", async () => {
|
||||
expect.assertions(2);
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div>
|
||||
<button t-on-click="onClick"><span>Button</span></button>
|
||||
<button t-on-click.self="onClickSelf"><span>Button</span></button>
|
||||
</div>`
|
||||
);
|
||||
let steps: string[] = [];
|
||||
let owner = {
|
||||
onClick(e) {
|
||||
steps.push("onClick");
|
||||
},
|
||||
onClickSelf(e) {
|
||||
steps.push("onClickSelf");
|
||||
}
|
||||
};
|
||||
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
|
||||
|
||||
const buttons = (<HTMLElement>node).getElementsByTagName("button");
|
||||
const spans = (<HTMLElement>node).getElementsByTagName("span");
|
||||
spans[0].click();
|
||||
spans[1].click();
|
||||
buttons[0].click();
|
||||
buttons[1].click();
|
||||
|
||||
expect(steps).toEqual(["onClick", "onClick", "onClickSelf"]);
|
||||
});
|
||||
|
||||
test("t-on with self and prevent modifiers (order matters)", async () => {
|
||||
expect.assertions(2);
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div>
|
||||
<button t-on-click.self.prevent="onClick"><span>Button</span></button>
|
||||
</div>`
|
||||
);
|
||||
let steps: boolean[] = [];
|
||||
let owner = {
|
||||
onClick() {}
|
||||
};
|
||||
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
|
||||
(<HTMLElement>node).addEventListener("click", function(e) {
|
||||
steps.push(e.defaultPrevented);
|
||||
});
|
||||
|
||||
const button = (<HTMLElement>node).getElementsByTagName("button")[0];
|
||||
const span = (<HTMLElement>node).getElementsByTagName("span")[0];
|
||||
span.click();
|
||||
button.click();
|
||||
|
||||
expect(steps).toEqual([false, true]);
|
||||
});
|
||||
|
||||
test("t-on with prevent and self modifiers (order matters)", async () => {
|
||||
expect.assertions(2);
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div>
|
||||
<button t-on-click.prevent.self="onClick"><span>Button</span></button>
|
||||
</div>`
|
||||
);
|
||||
let steps: boolean[] = [];
|
||||
let owner = {
|
||||
onClick() {}
|
||||
};
|
||||
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
|
||||
(<HTMLElement>node).addEventListener("click", function(e) {
|
||||
steps.push(e.defaultPrevented);
|
||||
});
|
||||
|
||||
const button = (<HTMLElement>node).getElementsByTagName("button")[0];
|
||||
const span = (<HTMLElement>node).getElementsByTagName("span")[0];
|
||||
span.click();
|
||||
button.click();
|
||||
|
||||
expect(steps).toEqual([true, true]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("t-ref", () => {
|
||||
test("can get a ref on a node", () => {
|
||||
qweb.addTemplate("test", `<div><span t-ref="'myspan'"/></div>`);
|
||||
qweb.addTemplate("test", `<div><span t-ref="myspan"/></div>`);
|
||||
let refs: any = {};
|
||||
renderToDOM(qweb, "test", { refs });
|
||||
expect(refs.myspan.tagName).toBe("SPAN");
|
||||
});
|
||||
|
||||
test("can get a dynamic ref on a node", () => {
|
||||
qweb.addTemplate("test", `<div><span t-ref="'myspan' + 3"/></div>`);
|
||||
qweb.addTemplate("test", `<div><span t-ref="myspan{{id}}"/></div>`);
|
||||
let refs: any = {};
|
||||
renderToDOM(qweb, "test", { refs });
|
||||
renderToDOM(qweb, "test", { refs, id: 3 });
|
||||
expect(refs.myspan3.tagName).toBe("SPAN");
|
||||
});
|
||||
|
||||
@@ -992,7 +1067,7 @@ describe("t-ref", () => {
|
||||
`
|
||||
<div>
|
||||
<t t-foreach="items" t-as="item">
|
||||
<div t-ref="item" t-key="item"><t t-esc="item"/></div>
|
||||
<div t-ref="{{item}}" t-key="item"><t t-esc="item"/></div>
|
||||
</t>
|
||||
</div>`
|
||||
);
|
||||
@@ -1167,16 +1242,3 @@ describe("debugging", () => {
|
||||
console.log = consoleLog;
|
||||
});
|
||||
});
|
||||
|
||||
describe("animations", () => {
|
||||
test("t-transition, on a simple node", async () => {
|
||||
// this test does not test much, because it is not easy to test timing
|
||||
// transitions... we should do a little more effort for these tests.
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div><span t-transition="chimay">blue</span></div>`
|
||||
);
|
||||
let dom: HTMLElement = <HTMLElement>renderToDOM(qweb, "test");
|
||||
expect(dom.innerHTML).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
import { compileExpr, tokenize } from "../src/qweb_expressions";
|
||||
|
||||
describe("tokenizer", () => {
|
||||
test("simple tokens", () => {
|
||||
expect(tokenize("1.3")).toEqual([
|
||||
{ type: "VALUE", value: "1.3" }
|
||||
]);
|
||||
|
||||
expect(tokenize("{}")).toEqual([
|
||||
{ type: "LEFT_BRACE", value: "{" },
|
||||
{ type: "RIGHT_BRACE", value: "}" }
|
||||
]);
|
||||
expect(tokenize("{ }}")).toEqual([
|
||||
{ type: "LEFT_BRACE", value: "{" },
|
||||
{ type: "RIGHT_BRACE", value: "}" },
|
||||
{ type: "RIGHT_BRACE", value: "}" }
|
||||
]);
|
||||
expect(tokenize("a")).toEqual([{ type: "SYMBOL", value: "a" }]);
|
||||
expect(tokenize("true")).toEqual([{ type: "SYMBOL", value: "true" }]);
|
||||
expect(tokenize("abcde")).toEqual([{ type: "SYMBOL", value: "abcde" }]);
|
||||
expect(tokenize("_ab2")).toEqual([{ type: "SYMBOL", value: "_ab2" }]);
|
||||
expect(tokenize("$ab2")).toEqual([{ type: "SYMBOL", value: "$ab2" }]);
|
||||
expect(tokenize("ABC")).toEqual([{ type: "SYMBOL", value: "ABC" }]);
|
||||
|
||||
expect(tokenize("{a: 2}")).toEqual([
|
||||
{ type: "LEFT_BRACE", value: "{" },
|
||||
{ type: "SYMBOL", value: "a" },
|
||||
{ type: "COLON", value: ":" },
|
||||
{ type: "VALUE", value: "2" },
|
||||
{ type: "RIGHT_BRACE", value: "}" }
|
||||
]);
|
||||
expect(tokenize("a,")).toEqual([
|
||||
{ type: "SYMBOL", value: "a" },
|
||||
{ type: "COMMA", value: "," }
|
||||
]);
|
||||
expect(tokenize("][")).toEqual([
|
||||
{ type: "RIGHT_BRACKET", value: "]" },
|
||||
{ type: "LEFT_BRACKET", value: "[" }
|
||||
]);
|
||||
});
|
||||
|
||||
test("various operators", () => {
|
||||
expect(tokenize(">= <= < >")).toEqual([
|
||||
{ type: "OPERATOR", value: ">=" },
|
||||
{ type: "OPERATOR", value: "<=" },
|
||||
{ type: "OPERATOR", value: "<" },
|
||||
{ type: "OPERATOR", value: ">" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("strings", () => {
|
||||
expect(() => tokenize("'")).toThrow("Invalid expression");
|
||||
expect(() => tokenize("'\\")).toThrow("Invalid expression");
|
||||
expect(() => tokenize("'\\'")).toThrow("Invalid expression");
|
||||
expect(tokenize("'hello ged'")).toEqual([
|
||||
{ type: "VALUE", value: "'hello ged'" }
|
||||
]);
|
||||
expect(tokenize("'hello \\'ged\\''")).toEqual([
|
||||
{ type: "VALUE", value: "'hello \\'ged\\''" }
|
||||
]);
|
||||
|
||||
expect(() => tokenize('"')).toThrow("Invalid expression");
|
||||
expect(() => tokenize('"\\"')).toThrow("Invalid expression");
|
||||
expect(tokenize('"hello ged"')).toEqual([
|
||||
{ type: "VALUE", value: '"hello ged"' }
|
||||
]);
|
||||
expect(tokenize('"hello ged"}')).toEqual([
|
||||
{ type: "VALUE", value: '"hello ged"' },
|
||||
{ type: "RIGHT_BRACE", value: "}" }
|
||||
]);
|
||||
expect(tokenize('"hello \\"ged\\""')).toEqual([
|
||||
{ type: "VALUE", value: '"hello \\"ged\\""' }
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("expression evaluation", () => {
|
||||
test("simple static values", () => {
|
||||
expect(compileExpr("1", {})).toBe("1");
|
||||
expect(compileExpr("1 ", {})).toBe("1");
|
||||
expect(compileExpr("'some string#/, {' ", {})).toBe("'some string#/, {'");
|
||||
expect(compileExpr("{ } ", {})).toBe("{}");
|
||||
expect(compileExpr("{a: 1} ", {})).toBe("{a:1}");
|
||||
expect(compileExpr("{a: 1, b: 2 } ", {})).toBe("{a:1,b:2}");
|
||||
expect(compileExpr("[] ", {})).toBe("[]");
|
||||
expect(compileExpr("[1] ", {})).toBe("[1]");
|
||||
expect(compileExpr("['1', '2'] ", {})).toBe("['1','2']");
|
||||
expect(compileExpr("['1', \"2\"] ", {})).toBe("['1',\"2\"]");
|
||||
});
|
||||
|
||||
test("various types of 'words'", () => {
|
||||
expect(compileExpr("true", {})).toBe("true");
|
||||
expect(compileExpr("false", {})).toBe("false");
|
||||
expect(compileExpr("debugger", {})).toBe("debugger");
|
||||
});
|
||||
|
||||
test("parenthesis", () => {
|
||||
expect(compileExpr("(1)", {})).toBe("(1)");
|
||||
expect(compileExpr("a*(1 +3)", {})).toBe("context['a']*(1+3)");
|
||||
});
|
||||
|
||||
test("objects and sub objects", () => {
|
||||
expect(compileExpr("{a:{b:1}} ", {})).toBe("{a:{b:1}}");
|
||||
});
|
||||
|
||||
test("replacing variables", () => {
|
||||
expect(compileExpr("a", {})).toBe("context['a']");
|
||||
expect(compileExpr("a", { a: { id: "_3", expr: "" } })).toBe("_3");
|
||||
});
|
||||
|
||||
test("arrays and objects", () => {
|
||||
expect(compileExpr("[{b:1}] ", {})).toBe("[{b:1}]");
|
||||
expect(compileExpr("{a: []} ", {})).toBe("{a:[]}");
|
||||
expect(compileExpr("[{b:1, c: [1, {d: {e: 3}} ]}] ", {})).toBe(
|
||||
"[{b:1,c:[1,{d:{e:3}}]}]"
|
||||
);
|
||||
});
|
||||
|
||||
test("dot operator", () => {
|
||||
expect(compileExpr("a.b", {})).toBe("context['a'].b");
|
||||
expect(compileExpr("a.b.c", {})).toBe("context['a'].b.c");
|
||||
});
|
||||
|
||||
test("various unary operators", () => {
|
||||
expect(compileExpr("!flag", {})).toBe("!context['flag']");
|
||||
expect(compileExpr("-3", {})).toBe("-3");
|
||||
expect(compileExpr("-a", {})).toBe("-context['a']");
|
||||
});
|
||||
|
||||
test("various binary operators", () => {
|
||||
expect(compileExpr("color == 'black'", {})).toBe(
|
||||
"context['color']=='black'"
|
||||
);
|
||||
expect(compileExpr("a || b", {})).toBe("context['a']||context['b']");
|
||||
expect(compileExpr("color === 'black'", {})).toBe(
|
||||
"context['color']==='black'"
|
||||
);
|
||||
expect(compileExpr("'li_'+item", {})).toBe("'li_'+context['item']");
|
||||
expect(compileExpr("state.val > 1", {})).toBe("context['state'].val>1");
|
||||
});
|
||||
|
||||
test("boolean operations", () => {
|
||||
expect(compileExpr("a && b", {})).toBe("context['a']&&context['b']");
|
||||
});
|
||||
|
||||
test("ternary operators", () => {
|
||||
expect(compileExpr("a ? b: '2'", {})).toBe("context['a']?context['b']:'2'");
|
||||
expect(compileExpr("a ? b: (c or '2') ", {})).toBe(
|
||||
"context['a']?context['b']:(context['c']||'2')"
|
||||
);
|
||||
expect(compileExpr("a ? {test:c}: [1,u]", {})).toBe(
|
||||
"context['a']?{test:context['c']}:[1,context['u']]"
|
||||
);
|
||||
});
|
||||
|
||||
test("word replacement", () => {
|
||||
expect(compileExpr("a or b", {})).toBe("context['a']||context['b']");
|
||||
expect(compileExpr("a and b", {})).toBe("context['a']&&context['b']");
|
||||
});
|
||||
|
||||
test("function calls", () => {
|
||||
expect(compileExpr("a()", {})).toBe("context['a']()");
|
||||
expect(compileExpr("a(1)", {})).toBe("context['a'](1)");
|
||||
expect(compileExpr("a(1,2)", {})).toBe("context['a'](1,2)");
|
||||
expect(compileExpr("a(1,2,{a:[a]})", {})).toBe(
|
||||
"context['a'](1,2,{a:[context['a']]})"
|
||||
);
|
||||
expect(compileExpr("'x'.toUpperCase()", {})).toBe("'x'.toUpperCase()");
|
||||
expect(compileExpr("'x'.toUpperCase({a: 3})", {})).toBe(
|
||||
"'x'.toUpperCase({a:3})"
|
||||
);
|
||||
expect(
|
||||
compileExpr("'x'.toUpperCase(a)", { a: { id: "_v5", expr: "" } })
|
||||
).toBe("'x'.toUpperCase(_v5)");
|
||||
expect(
|
||||
compileExpr("'x'.toUpperCase({b: a})", { a: { id: "_v5", expr: "" } })
|
||||
).toBe("'x'.toUpperCase({b:_v5})");
|
||||
});
|
||||
});
|
||||
+146
-78
@@ -152,24 +152,6 @@ describe("basic use", () => {
|
||||
store.dispatch("someaction");
|
||||
});
|
||||
|
||||
test("set function is given to mutations", async () => {
|
||||
let updateCounter = 0;
|
||||
const state = { bertinchamps: "brune" };
|
||||
const mutations = {
|
||||
addInfo({ state, set }) {
|
||||
set(state, "chouffe", "blonde");
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, mutations, actions: {} });
|
||||
store.on("update", null, () => updateCounter++);
|
||||
|
||||
expect(updateCounter).toBe(0);
|
||||
store.commit("addInfo");
|
||||
await nextMicroTick();
|
||||
expect(updateCounter).toBe(1);
|
||||
expect(store.state).toEqual({ bertinchamps: "brune", chouffe: "blonde" });
|
||||
});
|
||||
|
||||
test("can have getters from store", async () => {
|
||||
const state = {
|
||||
beers: {
|
||||
@@ -414,7 +396,7 @@ describe("connecting a component to store", () => {
|
||||
`
|
||||
<div>
|
||||
<t t-foreach="props.todos" t-as="todo" t-key="todo">
|
||||
<t t-widget="Todo" t-props="todo"/>
|
||||
<t t-widget="Todo" msg="todo.msg"/>
|
||||
</t>
|
||||
</div>`
|
||||
);
|
||||
@@ -429,10 +411,13 @@ describe("connecting a component to store", () => {
|
||||
state.todos.push({ msg });
|
||||
}
|
||||
};
|
||||
function mapStateToProps(s) {
|
||||
function mapStoreToProps(s) {
|
||||
return { todos: s.todos };
|
||||
}
|
||||
const TodoApp = connect(mapStateToProps)(App);
|
||||
const TodoApp = connect(
|
||||
App,
|
||||
mapStoreToProps
|
||||
);
|
||||
const store = new Store({ state, mutations });
|
||||
(<any>env).store = store;
|
||||
const app = new TodoApp(env);
|
||||
@@ -452,7 +437,7 @@ describe("connecting a component to store", () => {
|
||||
state.todos[0].title = title;
|
||||
}
|
||||
};
|
||||
function mapStateToProps(s) {
|
||||
function mapStoreToProps(s) {
|
||||
return { todos: s.todos };
|
||||
}
|
||||
const store = new Store({ state, mutations });
|
||||
@@ -469,13 +454,15 @@ describe("connecting a component to store", () => {
|
||||
class App extends Component<any, any, any> {}
|
||||
|
||||
const DeepTodoApp = connect(
|
||||
mapStateToProps,
|
||||
App,
|
||||
mapStoreToProps,
|
||||
{ deep: true }
|
||||
)(App);
|
||||
);
|
||||
const ShallowTodoApp = connect(
|
||||
mapStateToProps,
|
||||
App,
|
||||
mapStoreToProps,
|
||||
{ deep: false }
|
||||
)(App);
|
||||
);
|
||||
(<any>env).store = store;
|
||||
const deepTodoApp = new DeepTodoApp(env);
|
||||
const shallowTodoApp = new ShallowTodoApp(env);
|
||||
@@ -494,6 +481,51 @@ describe("connecting a component to store", () => {
|
||||
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" t-key="todo">
|
||||
<t t-widget="Todo" msg="todo.msg"/>
|
||||
</t>
|
||||
</div>
|
||||
<span t-name="Todo"><t t-esc="props.msg"/></span>
|
||||
</templates>
|
||||
`);
|
||||
class App extends Component<any, any, any> {
|
||||
widgets = { Todo };
|
||||
}
|
||||
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 });
|
||||
}
|
||||
}
|
||||
});
|
||||
function mapStoreToProps(s) {
|
||||
return { todos: s.todos };
|
||||
}
|
||||
const TodoApp = connect(
|
||||
App,
|
||||
mapStoreToProps,
|
||||
{
|
||||
getStore: () => store
|
||||
}
|
||||
);
|
||||
const app = new TodoApp(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.addTemplate("Child", `<div/>`);
|
||||
@@ -506,7 +538,10 @@ describe("connecting a component to store", () => {
|
||||
}
|
||||
}
|
||||
|
||||
const ConnectedChild = connect(s => s)(Child);
|
||||
const ConnectedChild = connect(
|
||||
Child,
|
||||
s => s
|
||||
);
|
||||
|
||||
env.qweb.addTemplate(
|
||||
"Parent",
|
||||
@@ -548,16 +583,19 @@ describe("connecting a component to store", () => {
|
||||
|
||||
env.qweb.addTemplate("TodoItem", `<span><t t-esc="props.text"/></span>`);
|
||||
class TodoItem extends Component<any, any, any> {}
|
||||
const ConnectedTodo = connect((state, props) => {
|
||||
const todo = state.todos.find(t => t.id === props.id);
|
||||
return todo;
|
||||
})(TodoItem);
|
||||
const ConnectedTodo = connect(
|
||||
TodoItem,
|
||||
(state, props) => {
|
||||
const todo = state.todos.find(t => t.id === props.id);
|
||||
return todo;
|
||||
}
|
||||
);
|
||||
|
||||
env.qweb.addTemplate(
|
||||
"TodoList",
|
||||
`<div>
|
||||
<t t-foreach="props.todos" t-as="todo">
|
||||
<t t-widget="ConnectedTodo" t-props="todo"/>
|
||||
<t t-widget="ConnectedTodo" id="todo.id"/>
|
||||
</t>
|
||||
</div>`
|
||||
);
|
||||
@@ -565,10 +603,13 @@ describe("connecting a component to store", () => {
|
||||
widgets = { ConnectedTodo };
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
function mapStoreToProps(state) {
|
||||
return { todos: state.todos };
|
||||
}
|
||||
const ConnectedTodoList = connect(mapStateToProps)(TodoList);
|
||||
const ConnectedTodoList = connect(
|
||||
TodoList,
|
||||
mapStoreToProps
|
||||
);
|
||||
|
||||
(<any>env).store = store;
|
||||
const app = new ConnectedTodoList(env);
|
||||
@@ -606,19 +647,22 @@ describe("connecting a component to store", () => {
|
||||
</div>`
|
||||
);
|
||||
class TodoItem extends Component<any, any, any> {}
|
||||
const ConnectedTodo = connect((state, props, getters) => {
|
||||
const todo = state.todos.find(t => t.id === props.id);
|
||||
return {
|
||||
activeTodoText: getters.text(todo.id),
|
||||
importantTodoText: getters.importantTodoText()
|
||||
};
|
||||
})(TodoItem);
|
||||
const ConnectedTodo = connect(
|
||||
TodoItem,
|
||||
(state, props, getters) => {
|
||||
const todo = state.todos.find(t => t.id === props.id);
|
||||
return {
|
||||
activeTodoText: getters.text(todo.id),
|
||||
importantTodoText: getters.importantTodoText()
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
env.qweb.addTemplate(
|
||||
"TodoList",
|
||||
`<div>
|
||||
<t t-foreach="props.todos" t-as="todo">
|
||||
<t t-widget="ConnectedTodo" t-props="todo"/>
|
||||
<t t-widget="ConnectedTodo" id="todo.id"/>
|
||||
</t>
|
||||
</div>`
|
||||
);
|
||||
@@ -626,10 +670,13 @@ describe("connecting a component to store", () => {
|
||||
widgets = { ConnectedTodo };
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
function mapStoreToProps(state) {
|
||||
return { todos: state.todos };
|
||||
}
|
||||
const ConnectedTodoList = connect(mapStateToProps)(TodoList);
|
||||
const ConnectedTodoList = connect(
|
||||
TodoList,
|
||||
mapStoreToProps
|
||||
);
|
||||
|
||||
(<any>env).store = store;
|
||||
const app = new ConnectedTodoList(env);
|
||||
@@ -643,14 +690,17 @@ describe("connecting a component to store", () => {
|
||||
test("connected component is updated when props are updated", async () => {
|
||||
env.qweb.addTemplate("Beer", `<span><t t-esc="props.name"/></span>`);
|
||||
class Beer extends Component<any, any, any> {}
|
||||
const ConnectedBeer = connect((state, props) => {
|
||||
return state.beers[props.id];
|
||||
})(Beer);
|
||||
const ConnectedBeer = connect(
|
||||
Beer,
|
||||
(state, props) => {
|
||||
return state.beers[props.id];
|
||||
}
|
||||
);
|
||||
|
||||
env.qweb.addTemplate(
|
||||
"App",
|
||||
`<div>
|
||||
<t t-widget="ConnectedBeer" t-props="{id: state.beerId}"/>
|
||||
<t t-widget="ConnectedBeer" id="state.beerId"/>
|
||||
</div>`
|
||||
);
|
||||
class App extends Component<any, any, any> {
|
||||
@@ -691,10 +741,13 @@ describe("connecting a component to store", () => {
|
||||
const store = new Store({ state, mutations });
|
||||
(<any>env).store = store;
|
||||
|
||||
function mapStateToProps(state) {
|
||||
function mapStoreToProps(state) {
|
||||
return { beers: state.beers, otherKey: 1 };
|
||||
}
|
||||
const ConnectedApp = connect(mapStateToProps)(App);
|
||||
const ConnectedApp = connect(
|
||||
App,
|
||||
mapStoreToProps
|
||||
);
|
||||
const app = new ConnectedApp(env);
|
||||
|
||||
await app.mount(fixture);
|
||||
@@ -717,18 +770,21 @@ describe("connecting a component to store", () => {
|
||||
</div>`
|
||||
);
|
||||
class Beer extends Component<any, any, any> {}
|
||||
const ConnectedBeer = connect((state, props) => {
|
||||
return {
|
||||
selected: state.beers[props.id],
|
||||
consumed: state.beers[state.consumedID] || null,
|
||||
taster: state.taster
|
||||
};
|
||||
})(Beer);
|
||||
const ConnectedBeer = connect(
|
||||
Beer,
|
||||
(state, props) => {
|
||||
return {
|
||||
selected: state.beers[props.id],
|
||||
consumed: state.beers[state.consumedID] || null,
|
||||
taster: state.taster
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
env.qweb.addTemplate(
|
||||
"App",
|
||||
`<div>
|
||||
<t t-widget="ConnectedBeer" t-props="{id: state.beerId}"/>
|
||||
<t t-widget="ConnectedBeer" id="state.beerId"/>
|
||||
</div>`
|
||||
);
|
||||
class App extends Component<any, any, any> {
|
||||
@@ -786,18 +842,21 @@ describe("connecting a component to store", () => {
|
||||
</div>`
|
||||
);
|
||||
class Beer extends Component<any, any, any> {}
|
||||
const ConnectedBeer = connect((state, props) => {
|
||||
return {
|
||||
selected: state.beers[props.id],
|
||||
consumed: state.beers[state.consumedID] || null,
|
||||
taster: state.taster
|
||||
};
|
||||
})(Beer);
|
||||
const ConnectedBeer = connect(
|
||||
Beer,
|
||||
(state, props) => {
|
||||
return {
|
||||
selected: state.beers[props.id],
|
||||
consumed: state.beers[state.consumedID] || null,
|
||||
taster: state.taster
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
env.qweb.addTemplate(
|
||||
"App",
|
||||
`<div>
|
||||
<t t-widget="ConnectedBeer" t-props="{id: state.beerId}"/>
|
||||
<t t-widget="ConnectedBeer" id="state.beerId"/>
|
||||
</div>`
|
||||
);
|
||||
class App extends Component<any, any, any> {
|
||||
@@ -876,25 +935,31 @@ describe("connecting a component to store", () => {
|
||||
"Parent",
|
||||
`
|
||||
<div>
|
||||
<t t-widget="Child" t-ref="'child'" t-props="{key: props.current}"/>
|
||||
<t t-widget="Child" key="props.current"/>
|
||||
</div>
|
||||
`
|
||||
);
|
||||
class Parent extends Component<any, any, any> {
|
||||
widgets = { Child: ConnectedChild };
|
||||
}
|
||||
const ConnectedParent = connect(function(s) {
|
||||
steps.push("parent");
|
||||
return { current: s.current, isvisible: s.isvisible };
|
||||
})(Parent);
|
||||
const ConnectedParent = connect(
|
||||
Parent,
|
||||
function(s) {
|
||||
steps.push("parent");
|
||||
return { current: s.current, isvisible: s.isvisible };
|
||||
}
|
||||
);
|
||||
|
||||
env.qweb.addTemplate("Child", `<span><t t-esc="props.msg"/></span>`);
|
||||
class Child extends Component<any, any, any> {}
|
||||
|
||||
const ConnectedChild = connect(function(s, props) {
|
||||
steps.push("child");
|
||||
return { msg: s.msg[props.key] };
|
||||
})(Child);
|
||||
const ConnectedChild = connect(
|
||||
Child,
|
||||
function(s, props) {
|
||||
steps.push("child");
|
||||
return { msg: s.msg[props.key] };
|
||||
}
|
||||
);
|
||||
|
||||
const state = { current: "a", msg: { a: "a", b: "b" } };
|
||||
const mutations = {
|
||||
@@ -927,9 +992,12 @@ describe("connecting a component to store", () => {
|
||||
steps.push("patched");
|
||||
}
|
||||
}
|
||||
const ConnectedApp = connect(function(s) {
|
||||
return { msg: s.msg };
|
||||
})(App);
|
||||
const ConnectedApp = connect(
|
||||
App,
|
||||
function(s) {
|
||||
return { msg: s.msg };
|
||||
}
|
||||
);
|
||||
|
||||
const state = { msg: "a" };
|
||||
const mutations = {
|
||||
|
||||
+118
-35
@@ -1,4 +1,9 @@
|
||||
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
|
||||
import {
|
||||
buildData,
|
||||
startMeasure,
|
||||
stopMeasure,
|
||||
formatNumber
|
||||
} from "../shared/utils.js";
|
||||
|
||||
odoo.define("app", function(require) {
|
||||
const Widget = require("web.Widget");
|
||||
@@ -97,14 +102,23 @@ odoo.define("app", function(require) {
|
||||
"click .o_btn_msg.10000": function() {
|
||||
this.addMessages(10000);
|
||||
},
|
||||
"click .o_btn_msg.50000": function() {
|
||||
this.addMessages(50000);
|
||||
"click .o_btn_msg.30000": function() {
|
||||
this.addMessages(30000);
|
||||
},
|
||||
"click .updateSomeMessages": function() {
|
||||
this.updateSomeMessages();
|
||||
},
|
||||
"click .clear": function() {
|
||||
this.clear();
|
||||
},
|
||||
"click .o_multiple": function() {
|
||||
this.multipleFlag = !this.multipleFlag;
|
||||
},
|
||||
"click .o_clear": function() {
|
||||
this.clearFlag = !this.clearFlag;
|
||||
},
|
||||
"click .clear-log": function() {
|
||||
this.$log[0].innerHTML = "";
|
||||
}
|
||||
},
|
||||
custom_events: {
|
||||
@@ -116,11 +130,15 @@ odoo.define("app", function(require) {
|
||||
this.widgets = {};
|
||||
this.isInDom = false;
|
||||
this.messageCount = 0;
|
||||
this.multipleFlag = false;
|
||||
this.clearFlag = false;
|
||||
},
|
||||
|
||||
start: function() {
|
||||
this.$content = this.$(".content");
|
||||
this.$msgCount = this.$(".message_count");
|
||||
this.$log = this.$(".log-content");
|
||||
this.log("Benchmarking odoo widgets, 12.0");
|
||||
},
|
||||
on_attach_callback: function() {
|
||||
this.isInDom = true;
|
||||
@@ -144,34 +162,41 @@ odoo.define("app", function(require) {
|
||||
},
|
||||
|
||||
addMessages: function(n) {
|
||||
var self = this;
|
||||
startMeasure("add " + n);
|
||||
const defs = [];
|
||||
const messages = buildData(n);
|
||||
for (let message of messages) {
|
||||
const widget = new Message(this, message);
|
||||
this.widgets[message.id] = widget;
|
||||
defs.push(widget.appendTo("<div>"));
|
||||
}
|
||||
$.when
|
||||
.apply($, defs)
|
||||
.then(function() {
|
||||
for (let message of messages) {
|
||||
let widget = self.widgets[message.id];
|
||||
dom.append(self.$content, widget.$el, {
|
||||
in_DOM: this.isInDom,
|
||||
callbacks: [{ widget: widget }]
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(function() {
|
||||
self.messageCount += n;
|
||||
self.updateMessageCount();
|
||||
stopMeasure();
|
||||
});
|
||||
const self = this;
|
||||
this.benchmark("add " + n, () => {
|
||||
const defs = [];
|
||||
const messages = buildData(n);
|
||||
for (let message of messages) {
|
||||
const widget = new Message(this, message);
|
||||
this.widgets[message.id] = widget;
|
||||
defs.push(widget.appendTo("<div>"));
|
||||
}
|
||||
return $.when
|
||||
.apply($, defs)
|
||||
.then(function() {
|
||||
for (let message of messages) {
|
||||
let widget = self.widgets[message.id];
|
||||
dom.append(self.$content, widget.$el, {
|
||||
in_DOM: this.isInDom,
|
||||
callbacks: [{ widget: widget }]
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(function() {
|
||||
self.messageCount += n;
|
||||
self.updateMessageCount();
|
||||
});
|
||||
});
|
||||
},
|
||||
clear: function() {
|
||||
startMeasure("clear");
|
||||
this._clear();
|
||||
stopMeasure(info => {
|
||||
this.log(info.msg);
|
||||
});
|
||||
},
|
||||
|
||||
_clear: function() {
|
||||
this.$content.empty();
|
||||
for (let key in this.widgets) {
|
||||
this.widgets[key].destroy();
|
||||
@@ -179,15 +204,14 @@ odoo.define("app", function(require) {
|
||||
}
|
||||
this.messageCount = 0;
|
||||
this.updateMessageCount();
|
||||
stopMeasure();
|
||||
},
|
||||
updateSomeMessages: function() {
|
||||
startMeasure("update every 10th");
|
||||
const widgets = Object.values(this.widgets);
|
||||
for (let i = 0; i < widgets.length; i += 10) {
|
||||
widgets[i].update();
|
||||
}
|
||||
stopMeasure();
|
||||
this.benchmark("update every 10th", () => {
|
||||
const widgets = Object.values(this.widgets);
|
||||
for (let i = 0; i < widgets.length; i += 10) {
|
||||
widgets[i].update();
|
||||
}
|
||||
});
|
||||
},
|
||||
_onRemoveMessage: function(ev) {
|
||||
startMeasure("remove message");
|
||||
@@ -196,6 +220,65 @@ odoo.define("app", function(require) {
|
||||
this.messageCount--;
|
||||
this.updateMessageCount();
|
||||
stopMeasure();
|
||||
},
|
||||
|
||||
benchmark: function(message, fn, callback) {
|
||||
if (this.multipleFlag) {
|
||||
const N = 20;
|
||||
let n = N;
|
||||
let total = 0;
|
||||
let cb = info => {
|
||||
let finalize = () => {
|
||||
n--;
|
||||
total += info.delta;
|
||||
if (n === 0) {
|
||||
const avg = total / N;
|
||||
this.log(`Average: ${formatNumber(avg)}ms`, true);
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
} else {
|
||||
this._benchmark(message, fn, cb);
|
||||
}
|
||||
};
|
||||
|
||||
if (this.clearFlag) {
|
||||
this._benchmark("clear", this._clear.bind(this), finalize, false);
|
||||
} else {
|
||||
finalize();
|
||||
}
|
||||
};
|
||||
this._benchmark(message, fn, cb);
|
||||
} else {
|
||||
this._benchmark(message, fn, callback);
|
||||
}
|
||||
},
|
||||
|
||||
_benchmark: function(message, fn, cb, log = true) {
|
||||
setTimeout(() => {
|
||||
startMeasure(message);
|
||||
const benchmark = fn();
|
||||
(benchmark && benchmark.then ? benchmark : $.when()).then(() => {
|
||||
stopMeasure(info => {
|
||||
if (log) {
|
||||
this.log(info.msg);
|
||||
}
|
||||
if (cb) {
|
||||
cb(info);
|
||||
}
|
||||
});
|
||||
}, 10);
|
||||
});
|
||||
},
|
||||
|
||||
log: function(str, isBold) {
|
||||
const div = document.createElement("div");
|
||||
if (isBold) {
|
||||
div.classList.add("bold");
|
||||
}
|
||||
div.textContent = `> ${str}`;
|
||||
this.$log[0].appendChild(div);
|
||||
this.$log[0].scrollTop = this.$log[0].scrollHeight;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<templates>
|
||||
<div class="main" t-name="root">
|
||||
<div class="left-thing">
|
||||
<div class="title">Actions</div>
|
||||
<div class="panel">
|
||||
<button class="o_btn_msg 100">Add 100 messages</button>
|
||||
<button class="o_btn_msg 1000">Add 1k messages</button>
|
||||
<button class="o_btn_msg 10000">Add 10k messages</button>
|
||||
<button class="o_btn_msg 30000">Add 30k messages</button>
|
||||
<button class="updateSomeMessages">Update every 10th message</button>
|
||||
<button class="clear">Clear</button>
|
||||
</div>
|
||||
<div class="flags">
|
||||
<div>
|
||||
<input type="checkbox" class="o_multiple" id="multipleflag" />
|
||||
<label for="multipleflag">Do it 20x</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" class="o_clear" id="clearFlag" />
|
||||
<label for="clearFlag">Clear after</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info message_count">
|
||||
Number of messages: 0
|
||||
</div>
|
||||
<div class="title">Log <span class="clear-log">(clear)</span></div>
|
||||
<div class="log">
|
||||
<div class="log-content"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="message" class="message">
|
||||
<span class="author"><t t-esc="widget.author"/></span>
|
||||
<span class="msg"><t t-esc="widget.msg"/></span>
|
||||
<button class="remove">Remove</button>
|
||||
</div>
|
||||
|
||||
<div t-name="counter">
|
||||
<button class="o_increment"></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
+118
-35
@@ -1,4 +1,9 @@
|
||||
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
|
||||
import {
|
||||
buildData,
|
||||
startMeasure,
|
||||
stopMeasure,
|
||||
formatNumber
|
||||
} from "../shared/utils.js";
|
||||
|
||||
odoo.define("app", function(require) {
|
||||
const Widget = require("web.Widget");
|
||||
@@ -97,14 +102,23 @@ odoo.define("app", function(require) {
|
||||
"click .o_btn_msg.10000": function() {
|
||||
this.addMessages(10000);
|
||||
},
|
||||
"click .o_btn_msg.50000": function() {
|
||||
this.addMessages(50000);
|
||||
"click .o_btn_msg.30000": function() {
|
||||
this.addMessages(30000);
|
||||
},
|
||||
"click .updateSomeMessages": function() {
|
||||
this.updateSomeMessages();
|
||||
},
|
||||
"click .clear": function() {
|
||||
this.clear();
|
||||
},
|
||||
"click .o_multiple": function() {
|
||||
this.multipleFlag = !this.multipleFlag;
|
||||
},
|
||||
"click .o_clear": function() {
|
||||
this.clearFlag = !this.clearFlag;
|
||||
},
|
||||
"click .clear-log": function() {
|
||||
this.$log[0].innerHTML = "";
|
||||
}
|
||||
},
|
||||
custom_events: {
|
||||
@@ -116,11 +130,15 @@ odoo.define("app", function(require) {
|
||||
this.widgets = {};
|
||||
this.isInDom = false;
|
||||
this.messageCount = 0;
|
||||
this.multipleFlag = false;
|
||||
this.clearFlag = false;
|
||||
},
|
||||
|
||||
start: function() {
|
||||
this.$content = this.$(".content");
|
||||
this.$msgCount = this.$(".message_count");
|
||||
this.$log = this.$(".log-content");
|
||||
this.log("Benchmarking odoo widgets, 12.3");
|
||||
},
|
||||
on_attach_callback: function() {
|
||||
this.isInDom = true;
|
||||
@@ -144,34 +162,41 @@ odoo.define("app", function(require) {
|
||||
},
|
||||
|
||||
addMessages: function(n) {
|
||||
var self = this;
|
||||
startMeasure("add " + n);
|
||||
const defs = [];
|
||||
const messages = buildData(n);
|
||||
for (let message of messages) {
|
||||
const widget = new Message(this, message);
|
||||
this.widgets[message.id] = widget;
|
||||
defs.push(widget.appendTo("<div>"));
|
||||
}
|
||||
$.when
|
||||
.apply($, defs)
|
||||
.then(function() {
|
||||
for (let message of messages) {
|
||||
let widget = self.widgets[message.id];
|
||||
dom.append(self.$content, widget.$el, {
|
||||
in_DOM: this.isInDom,
|
||||
callbacks: [{ widget: widget }]
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(function() {
|
||||
self.messageCount += n;
|
||||
self.updateMessageCount();
|
||||
stopMeasure();
|
||||
});
|
||||
const self = this;
|
||||
this.benchmark("add " + n, () => {
|
||||
const defs = [];
|
||||
const messages = buildData(n);
|
||||
for (let message of messages) {
|
||||
const widget = new Message(this, message);
|
||||
this.widgets[message.id] = widget;
|
||||
defs.push(widget.appendTo("<div>"));
|
||||
}
|
||||
return $.when
|
||||
.apply($, defs)
|
||||
.then(function() {
|
||||
for (let message of messages) {
|
||||
let widget = self.widgets[message.id];
|
||||
dom.append(self.$content, widget.$el, {
|
||||
in_DOM: this.isInDom,
|
||||
callbacks: [{ widget: widget }]
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(function() {
|
||||
self.messageCount += n;
|
||||
self.updateMessageCount();
|
||||
});
|
||||
});
|
||||
},
|
||||
clear: function() {
|
||||
startMeasure("clear");
|
||||
this._clear();
|
||||
stopMeasure(info => {
|
||||
this.log(info.msg);
|
||||
});
|
||||
},
|
||||
|
||||
_clear: function() {
|
||||
this.$content.empty();
|
||||
for (let key in this.widgets) {
|
||||
this.widgets[key].destroy();
|
||||
@@ -179,15 +204,14 @@ odoo.define("app", function(require) {
|
||||
}
|
||||
this.messageCount = 0;
|
||||
this.updateMessageCount();
|
||||
stopMeasure();
|
||||
},
|
||||
updateSomeMessages: function() {
|
||||
startMeasure("update every 10th");
|
||||
const widgets = Object.values(this.widgets);
|
||||
for (let i = 0; i < widgets.length; i += 10) {
|
||||
widgets[i].update();
|
||||
}
|
||||
stopMeasure();
|
||||
this.benchmark("update every 10th", () => {
|
||||
const widgets = Object.values(this.widgets);
|
||||
for (let i = 0; i < widgets.length; i += 10) {
|
||||
widgets[i].update();
|
||||
}
|
||||
});
|
||||
},
|
||||
_onRemoveMessage: function(ev) {
|
||||
startMeasure("remove message");
|
||||
@@ -196,6 +220,65 @@ odoo.define("app", function(require) {
|
||||
this.messageCount--;
|
||||
this.updateMessageCount();
|
||||
stopMeasure();
|
||||
},
|
||||
|
||||
benchmark: function(message, fn, callback) {
|
||||
if (this.multipleFlag) {
|
||||
const N = 20;
|
||||
let n = N;
|
||||
let total = 0;
|
||||
let cb = info => {
|
||||
let finalize = () => {
|
||||
n--;
|
||||
total += info.delta;
|
||||
if (n === 0) {
|
||||
const avg = total / N;
|
||||
this.log(`Average: ${formatNumber(avg)}ms`, true);
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
} else {
|
||||
this._benchmark(message, fn, cb);
|
||||
}
|
||||
};
|
||||
|
||||
if (this.clearFlag) {
|
||||
this._benchmark("clear", this._clear.bind(this), finalize, false);
|
||||
} else {
|
||||
finalize();
|
||||
}
|
||||
};
|
||||
this._benchmark(message, fn, cb);
|
||||
} else {
|
||||
this._benchmark(message, fn, callback);
|
||||
}
|
||||
},
|
||||
|
||||
_benchmark: function(message, fn, cb, log = true) {
|
||||
setTimeout(() => {
|
||||
startMeasure(message);
|
||||
const benchmark = fn();
|
||||
(benchmark && benchmark.then ? benchmark : $.when()).then(() => {
|
||||
stopMeasure(info => {
|
||||
if (log) {
|
||||
this.log(info.msg);
|
||||
}
|
||||
if (cb) {
|
||||
cb(info);
|
||||
}
|
||||
});
|
||||
}, 10);
|
||||
});
|
||||
},
|
||||
|
||||
log: function(str, isBold) {
|
||||
const div = document.createElement("div");
|
||||
if (isBold) {
|
||||
div.classList.add("bold");
|
||||
}
|
||||
div.textContent = `> ${str}`;
|
||||
this.$log[0].appendChild(div);
|
||||
this.$log[0].scrollTop = this.$log[0].scrollHeight;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<templates>
|
||||
<div class="main" t-name="root">
|
||||
<div class="left-thing">
|
||||
<div class="title">Actions</div>
|
||||
<div class="panel">
|
||||
<button class="o_btn_msg 100">Add 100 messages</button>
|
||||
<button class="o_btn_msg 1000">Add 1k messages</button>
|
||||
<button class="o_btn_msg 10000">Add 10k messages</button>
|
||||
<button class="o_btn_msg 30000">Add 30k messages</button>
|
||||
<button class="updateSomeMessages">Update every 10th message</button>
|
||||
<button class="clear">Clear</button>
|
||||
</div>
|
||||
<div class="flags">
|
||||
<div>
|
||||
<input type="checkbox" class="o_multiple" id="multipleflag" />
|
||||
<label for="multipleflag">Do it 20x</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" class="o_clear" id="clearFlag" />
|
||||
<label for="clearFlag">Clear after</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info message_count">
|
||||
Number of messages: 0
|
||||
</div>
|
||||
<div class="title">Log <span class="clear-log">(clear)</span></div>
|
||||
<div class="log">
|
||||
<div class="log-content"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="message" class="message">
|
||||
<span class="author"><t t-esc="widget.author"/></span>
|
||||
<span class="msg"><t t-esc="widget.msg"/></span>
|
||||
<button class="remove">Remove</button>
|
||||
</div>
|
||||
|
||||
<div t-name="counter">
|
||||
<button class="o_increment"></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,173 @@
|
||||
import {
|
||||
buildData,
|
||||
startMeasure,
|
||||
stopMeasure,
|
||||
formatNumber
|
||||
} from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Likes Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Counter extends owl.Component {
|
||||
state = { counter: 0 };
|
||||
template = "Counter";
|
||||
|
||||
increment() {
|
||||
this.state.counter++;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Message extends owl.Component {
|
||||
widgets = { Counter };
|
||||
template = "Message";
|
||||
|
||||
shouldUpdate(nextProps) {
|
||||
return nextProps !== this.props;
|
||||
}
|
||||
removeMessage() {
|
||||
this.trigger("remove_message", {
|
||||
id: this.props.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class App extends owl.Component {
|
||||
widgets = { Message };
|
||||
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
|
||||
template = "App";
|
||||
|
||||
mounted() {
|
||||
this.log(
|
||||
`Benchmarking Owl v${owl._version} (build date: ${
|
||||
owl._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(data) {
|
||||
this.benchmark("remove message", () => {
|
||||
const index = this.state.messages.findIndex(m => m.id === data.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,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" t-props="message" t-on-remove_message="removeMessage"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="Message" class="message">
|
||||
<span class="author"><t t-esc="props.author"/></span>
|
||||
<span class="msg"><t t-esc="props.msg"/></span>
|
||||
<button class="remove" t-on-click="removeMessage">Remove</button>
|
||||
<t t-widget="Counter"/>
|
||||
</div>
|
||||
|
||||
<div t-name="Counter">
|
||||
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,170 @@
|
||||
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 !== this.props;
|
||||
}
|
||||
removeMessage() {
|
||||
this.trigger("remove_message", {
|
||||
id: this.props.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class App extends owl.Component {
|
||||
widgets = { Message };
|
||||
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
|
||||
|
||||
mounted() {
|
||||
this.log(
|
||||
`Benchmarking Owl v${owl._version} (build date: ${
|
||||
owl._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(data) {
|
||||
this.benchmark("remove message", () => {
|
||||
const index = this.state.messages.findIndex(m => m.id === data.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.11.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" t-props="message" t-on-remove_message="removeMessage"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="Message" class="message">
|
||||
<span class="author"><t t-esc="props.author"/></span>
|
||||
<span class="msg"><t t-esc="props.msg"/></span>
|
||||
<button class="remove" t-on-click="removeMessage">Remove</button>
|
||||
<t t-widget="Counter"/>
|
||||
</div>
|
||||
|
||||
<div t-name="Counter">
|
||||
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,170 @@
|
||||
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 !== this.props;
|
||||
}
|
||||
removeMessage() {
|
||||
this.trigger("remove_message", {
|
||||
id: this.props.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(data) {
|
||||
this.benchmark("remove message", () => {
|
||||
const index = this.state.messages.findIndex(m => m.id === data.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.12.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" t-props="message" t-on-remove_message="removeMessage"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="Message" class="message">
|
||||
<span class="author"><t t-esc="props.author"/></span>
|
||||
<span class="msg"><t t-esc="props.msg"/></span>
|
||||
<button class="remove" t-on-click="removeMessage">Remove</button>
|
||||
<t t-widget="Counter"/>
|
||||
</div>
|
||||
|
||||
<div t-name="Counter">
|
||||
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,170 @@
|
||||
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.13.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,173 @@
|
||||
import {
|
||||
buildData,
|
||||
startMeasure,
|
||||
stopMeasure,
|
||||
formatNumber
|
||||
} from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Likes Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Counter extends owl.Component {
|
||||
state = { counter: 0 };
|
||||
template = "Counter";
|
||||
|
||||
increment() {
|
||||
this.state.counter++;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Message extends owl.Component {
|
||||
widgets = { Counter };
|
||||
template = "Message";
|
||||
|
||||
shouldUpdate(nextProps) {
|
||||
return nextProps !== this.props;
|
||||
}
|
||||
removeMessage() {
|
||||
this.trigger("remove_message", {
|
||||
id: this.props.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class App extends owl.Component {
|
||||
widgets = { Message };
|
||||
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
|
||||
template = "App";
|
||||
|
||||
mounted() {
|
||||
this.log(
|
||||
`Benchmarking Owl v${owl._version} (build date: ${
|
||||
owl._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(data) {
|
||||
this.benchmark("remove message", () => {
|
||||
const index = this.state.messages.findIndex(m => m.id === data.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,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" t-props="message" t-on-remove_message="removeMessage"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="Message" class="message">
|
||||
<span class="author"><t t-esc="props.author"/></span>
|
||||
<span class="msg"><t t-esc="props.msg"/></span>
|
||||
<button class="remove" t-on-click="removeMessage">Remove</button>
|
||||
<t t-widget="Counter"/>
|
||||
</div>
|
||||
|
||||
<div t-name="Counter">
|
||||
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,173 @@
|
||||
import {
|
||||
buildData,
|
||||
startMeasure,
|
||||
stopMeasure,
|
||||
formatNumber
|
||||
} from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Likes Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Counter extends owl.Component {
|
||||
state = { counter: 0 };
|
||||
template = "Counter";
|
||||
|
||||
increment() {
|
||||
this.state.counter++;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Message extends owl.Component {
|
||||
widgets = { Counter };
|
||||
template = "Message";
|
||||
|
||||
shouldUpdate(nextProps) {
|
||||
return nextProps !== this.props;
|
||||
}
|
||||
removeMessage() {
|
||||
this.trigger("remove_message", {
|
||||
id: this.props.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class App extends owl.Component {
|
||||
widgets = { Message };
|
||||
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
|
||||
template = "App";
|
||||
|
||||
mounted() {
|
||||
this.log(
|
||||
`Benchmarking Owl v${owl._version} (build date: ${
|
||||
owl._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(data) {
|
||||
this.benchmark("remove message", () => {
|
||||
const index = this.state.messages.findIndex(m => m.id === data.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,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" t-props="message" t-on-remove_message="removeMessage"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="Message" class="message">
|
||||
<span class="author"><t t-esc="props.author"/></span>
|
||||
<span class="msg"><t t-esc="props.msg"/></span>
|
||||
<button class="remove" t-on-click="removeMessage">Remove</button>
|
||||
<t t-widget="Counter"/>
|
||||
</div>
|
||||
|
||||
<div t-name="Counter">
|
||||
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,173 @@
|
||||
import {
|
||||
buildData,
|
||||
startMeasure,
|
||||
stopMeasure,
|
||||
formatNumber
|
||||
} from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Likes Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Counter extends owl.Component {
|
||||
state = { counter: 0 };
|
||||
template = "Counter";
|
||||
|
||||
increment() {
|
||||
this.state.counter++;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Message extends owl.Component {
|
||||
widgets = { Counter };
|
||||
template = "Message";
|
||||
|
||||
shouldUpdate(nextProps) {
|
||||
return nextProps !== this.props;
|
||||
}
|
||||
removeMessage() {
|
||||
this.trigger("remove_message", {
|
||||
id: this.props.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class App extends owl.Component {
|
||||
widgets = { Message };
|
||||
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
|
||||
template = "App";
|
||||
|
||||
mounted() {
|
||||
this.log(
|
||||
`Benchmarking Owl v${owl._version} (build date: ${
|
||||
owl._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(data) {
|
||||
this.benchmark("remove message", () => {
|
||||
const index = this.state.messages.findIndex(m => m.id === data.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,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" t-props="message" t-on-remove_message="removeMessage"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="Message" class="message">
|
||||
<span class="author"><t t-esc="props.author"/></span>
|
||||
<span class="msg"><t t-esc="props.msg"/></span>
|
||||
<button class="remove" t-on-click="removeMessage">Remove</button>
|
||||
<t t-widget="Counter"/>
|
||||
</div>
|
||||
|
||||
<div t-name="Counter">
|
||||
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,171 @@
|
||||
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 setState = owl.Observer.set;
|
||||
const messages = this.state.messages;
|
||||
for (let i = 0; i < this.state.messages.length; i += 10) {
|
||||
const msg = Object.assign({}, messages[i]);
|
||||
msg.author += "!!!";
|
||||
setState(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();
|
||||
@@ -7,7 +7,6 @@
|
||||
<script src='../../owl.js'></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id='main'></div>
|
||||
<script src='app.js' type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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,345 @@
|
||||
import {
|
||||
buildData,
|
||||
startMeasure,
|
||||
stopMeasure,
|
||||
formatNumber
|
||||
} from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Counter extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
counter: 0
|
||||
};
|
||||
this.increment = this.increment.bind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
return React.createElement(
|
||||
"div",
|
||||
null,
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: this.increment
|
||||
},
|
||||
"Value: ",
|
||||
this.state.counter
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
increment() {
|
||||
this.setState({
|
||||
counter: this.state.counter + 1
|
||||
});
|
||||
}
|
||||
}
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Message extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.removeMessage = this.removeMessage.bind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
return React.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "message"
|
||||
},
|
||||
React.createElement(
|
||||
"span",
|
||||
{
|
||||
className: "author"
|
||||
},
|
||||
this.props.message.author
|
||||
),
|
||||
React.createElement(
|
||||
"span",
|
||||
{
|
||||
className: "msg"
|
||||
},
|
||||
this.props.message.msg
|
||||
),
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
className: "remove",
|
||||
onClick: this.removeMessage
|
||||
},
|
||||
"Remove"
|
||||
),
|
||||
React.createElement(Counter, null)
|
||||
);
|
||||
}
|
||||
|
||||
removeMessage() {
|
||||
this.props.removeCB(this.props.message.id);
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Main extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
messages: [],
|
||||
multipleFlag: false,
|
||||
clearAfterFlag: false
|
||||
};
|
||||
this.removeMessage = this.removeMessage.bind(this);
|
||||
this.toggleMultipleFlag = this.toggleMultipleFlag.bind(this);
|
||||
this.toggleClearAfterFlag = this.toggleClearAfterFlag.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.log(`Benchmarking React v${React.version}`);
|
||||
}
|
||||
log(str, isBold) {
|
||||
const div = document.createElement("div");
|
||||
if (isBold) {
|
||||
div.classList.add("bold");
|
||||
}
|
||||
div.textContent = `> ${str}`;
|
||||
this.refs.logref.appendChild(div);
|
||||
this.refs.logref.scrollTop = this.refs.logref.scrollHeight;
|
||||
}
|
||||
|
||||
toggleMultipleFlag() {
|
||||
this.setState({ multipleFlag: !this.state.multipleFlag });
|
||||
}
|
||||
toggleClearAfterFlag() {
|
||||
this.setState({ clearAfterFlag: !this.state.clearAfterFlag });
|
||||
}
|
||||
|
||||
render() {
|
||||
const msgList = this.state.messages.map(m =>
|
||||
React.createElement(Message, {
|
||||
key: m.id,
|
||||
message: m,
|
||||
removeCB: this.removeMessage
|
||||
})
|
||||
);
|
||||
return React.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "main"
|
||||
},
|
||||
React.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "left-thing"
|
||||
},
|
||||
React.createElement("div", { className: "title" }, "Actions"),
|
||||
React.createElement(
|
||||
"div",
|
||||
{ className: "panel" },
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: _ => this.addMessages(100)
|
||||
},
|
||||
"Add 100 messages"
|
||||
),
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: _ => this.addMessages(1000)
|
||||
},
|
||||
"Add 1k messages"
|
||||
),
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: _ => this.addMessages(10000)
|
||||
},
|
||||
"Add 10k messages"
|
||||
),
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: _ => this.addMessages(30000)
|
||||
},
|
||||
"Add 30k messages"
|
||||
),
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: _ => this.updateSomeMessages()
|
||||
},
|
||||
"Update every 10th message"
|
||||
),
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: _ => this.clear()
|
||||
},
|
||||
"Clear"
|
||||
)
|
||||
),
|
||||
React.createElement(
|
||||
"div",
|
||||
{ className: "flags" },
|
||||
React.createElement(
|
||||
"div",
|
||||
null,
|
||||
React.createElement("input", {
|
||||
type: "checkbox",
|
||||
id: "multipleFlag",
|
||||
value: this.state.multipleFlag,
|
||||
onChange: this.toggleMultipleFlag
|
||||
}),
|
||||
React.createElement("label", { for: "multipleFlag" }, " Do it 20x")
|
||||
),
|
||||
React.createElement(
|
||||
"div",
|
||||
null,
|
||||
React.createElement("input", {
|
||||
type: "checkbox",
|
||||
id: "clearFlag",
|
||||
value: this.state.clearAfterFlag,
|
||||
onChange: this.toggleClearAfterFlag
|
||||
}),
|
||||
React.createElement("label", { for: "clearFlag" }, " Clear after")
|
||||
)
|
||||
),
|
||||
React.createElement(
|
||||
"div",
|
||||
{ className: "info" },
|
||||
"Number of messages: ",
|
||||
this.state.messages.length
|
||||
),
|
||||
React.createElement(
|
||||
"div",
|
||||
{ className: "title" },
|
||||
"Log ",
|
||||
React.createElement("span", { className: "clear-log" }, "(clear)")
|
||||
),
|
||||
React.createElement(
|
||||
"div",
|
||||
{ className: "log" },
|
||||
React.createElement("div", {
|
||||
className: "log-content",
|
||||
ref: "logref"
|
||||
})
|
||||
)
|
||||
),
|
||||
React.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "right-thing"
|
||||
},
|
||||
React.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "content"
|
||||
},
|
||||
msgList
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
addMessages(n) {
|
||||
this.benchmark("add " + n, () => {
|
||||
const newMessages = this.state.messages.concat(buildData(n));
|
||||
this.setState({
|
||||
messages: newMessages
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._benchmark("clear", () => {
|
||||
this.setState({
|
||||
messages: []
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateSomeMessages() {
|
||||
this.benchmark("update every 10th", () => {
|
||||
const messages = this.state.messages;
|
||||
for (let i = 0; i < messages.length; i += 10) {
|
||||
messages[i].author += "!!!";
|
||||
}
|
||||
this.forceUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
removeMessage(id) {
|
||||
startMeasure("remove message");
|
||||
const index = this.state.messages.findIndex(m => m.id === id);
|
||||
const messages = this.state.messages.slice();
|
||||
messages.splice(index, 1);
|
||||
this.setState({
|
||||
messages
|
||||
});
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
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.setState({ 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);
|
||||
}
|
||||
} //-----------------------------
|
||||
// INIT
|
||||
//-----------------------------
|
||||
|
||||
ReactDOM.render(React.createElement(Main, null), document.body);
|
||||
Vendored
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user