Compare commits

..

25 Commits

Author SHA1 Message Date
Géry Debongnie f60904b740 [REL] bump to v0.12.0 2019-05-29 11:26:21 +02:00
Aaron Bohy 376f801541 [IMP] qweb: t-widget: accept dyn values 2019-05-29 11:22:12 +02:00
Aaron Bohy 26f14180e2 [IMP] qweb: t-ref: dyn values with string interpolation
Closes #100
2019-05-29 11:22:12 +02:00
Géry Debongnie bced9f3d04 [IMP] *: use lower level loops in hot code
The goal is to gain a few % of perf improvement.

closes #125
2019-05-29 11:20:30 +02:00
Géry Debongnie 94208d9cbb [IMP] extras: add some css 2019-05-29 10:50:29 +02:00
Géry Debongnie 681983142d [IMP] component: add props validation and default props system
closes #116
2019-05-28 11:56:22 +02:00
Géry Debongnie a525b42537 [IMP] playground: improve code samples
closes #126
2019-05-27 13:33:26 +02:00
Géry Debongnie a4cc085b6d [DOC] qweb: add note to explain t-transition limitation 2019-05-27 12:46:20 +02:00
Aaron Bohy 837dacf1cb [DOC] animations: correct link 2019-05-27 12:10:33 +02:00
Aaron Bohy 7cdd1aa9a1 [IMP] qweb: add global Component registry to qweb
Closes #112
2019-05-27 12:10:33 +02:00
Géry Debongnie 24c569e6d5 [CLEANUP] owl: remove useless dependencies (package.json) 2019-05-27 12:10:30 +02:00
Aaron Bohy 050694414c [REF] move animations tests into their own file 2019-05-27 11:50:21 +02:00
Aaron Bohy 639908366a [IMP] allow to combine t-transition and t-widget
Closes #94
2019-05-27 11:50:21 +02:00
Aaron Bohy 59f401303a [REF] transitions: remove create hook 2019-05-27 11:50:20 +02:00
Aaron Bohy 649e20b956 [IMP] improve tests of transition directive 2019-05-27 11:49:09 +02:00
Géry Debongnie c2280ff1bb [REF] qweb: small cleanup 2019-05-23 23:33:15 +02:00
Aaron Bohy 8a32d823cd [IMP] support modifiers with t-on directive
Supported modifiers are stop, prevent and self.

Closes #115.
2019-05-23 23:33:09 +02:00
Géry Debongnie 13b31458da [DOC] owl: add message when setting mode in 'prod' 2019-05-23 13:44:14 +02:00
Géry Debongnie 92b3ab32e2 [REM] playground: remove benchmarks app
It is kind of redundant with the main benchmarks app, does not bring
much extra value, and is out of date.
2019-05-22 12:29:53 +02:00
Géry Debongnie 4ca0a157e3 [IMP] playground: set owl mode to 'dev'; 2019-05-21 18:08:32 +02:00
Géry Debongnie 1dec8df715 [IMP] owl: introduce dev mode
This commit adds a static key 'dev' on QWeb (which acts as a central
point of access in owl internal code), and a 'mode' key on the value
exported by owl to let external code manipulate it.

closes #117
2019-05-21 14:11:40 +02:00
Géry Debongnie 8f29cfc996 [FIX] playground: add missing t-key in template 2019-05-21 14:11:20 +02:00
Géry Debongnie da275e74fd [REF] owl: move metadata into __info__
This differentiate better metadata and owl exported values.  Also, and
this is the goal, it gives a space to add extra keys in the future.
2019-05-21 13:53:16 +02:00
Géry Debongnie 3d9f4e028c [DOC] utils: remove patch/unpatch references 2019-05-19 08:21:22 +02:00
Géry Debongnie 13577997bc [ADD] extras: add owl 0.11.0 to list of benchmarks 2019-05-17 23:46:42 +02:00
36 changed files with 5031 additions and 574 deletions
+2 -2
View File
@@ -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.12.0.js](https://github.com/odoo/owl/releases/download/v0.12.0/owl.js)
- [owl-0.12.0.min.js](https://github.com/odoo/owl/releases/download/v0.12.0/owl.min.js)
Some npm scripts are available:
+8 -3
View File
@@ -2,7 +2,7 @@
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
@@ -12,12 +12,16 @@ 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;
}
@@ -36,4 +40,5 @@ keyboard) the button.
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.
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 (see [QWeb documentation](qweb.md#t-transition-directive)).
+101 -1
View File
@@ -7,9 +7,11 @@
- [Composition](#composition)
- [Reference](#reference)
- [Properties](#properties)
- [Static Properties](#static-properties)
- [Methods](#methods)
- [Lifecycle](#lifecycle)
- [Semantics](#semantics)
- [Props Validation](#props-validation)
- [Asynchronous rendering](#asynchronous-rendering)
## Overview
@@ -144,11 +146,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
@@ -432,6 +445,93 @@ Here is what Owl will do:
6. `patched` hooks are called on `D`, `C`
### 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
};
```
## Asynchronous rendering
Working with asynchronous code always adds a lot of complexity to a system. Whenever
+93 -8
View File
@@ -13,7 +13,6 @@
- [`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)
@@ -87,6 +86,21 @@ It's API is quite simple:
const vnode = qweb.render("App", widget);
```
- **`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>");
```
## QWeb Specification
We define in this section the specification of how `QWeb` templates should be
@@ -279,14 +293,14 @@ There is another way to format a string attribute: the `t-attf-` directive. With
it, you get string interpolation:
```xml
<div t-attf-foo="a {{value1}} is {{value2}} of {{value3}} ]"/>
<div t-attf-foo="a #{value1} is #{value2} of #{value3} ]"/>
<!-- result if values are set to 1,2 and 3: <div foo="a 0 is 1 of 2 ]"></div> -->
```
For historical reason, there is an alternate form of string interpolation:
```xml
<div t-attf-foo="a #{value1} is #{value2} of #{value3} ]"/>
<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> -->
```
@@ -361,6 +375,25 @@ is what makes a template _alive_. There are two different use cases.
The suffix (`click` in this example) is simply the name of the actual DOM
event.
In order to remove the DOM event details from the event handlers (like calls
to `event.preventDefault`) and let them focus on data logic, _modifiers_ can
be specified as additional suffixes of the `t-on` directive.
| Modifier | Description |
| ---------- | ----------------------------------------------------------------- |
| `.stop` | calls `event.stopPropagation()` before calling the method |
| `.prevent` | calls `event.preventDefault()` before calling the method |
| `.self` | calls the method only if the `event.target` is the element itself |
```xml
<button t-on-click.stop="someMethod">Do something</button>
```
Note that modifiers can be combined (ex: `t-on-click.stop.prevent`), and that
the order may matter. For instance `t-on-click.prevent.self` will prevent all
clicks while `t-on-click.self.prevent` will only prevent clicks on the
element itself.
2. Register an event handler on a component. This will not capture a DOM event,
but rather a _business_ event:
@@ -410,13 +443,39 @@ class ParentWidget {
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.
actual component class in the special `widgets` key, or the class registered in
QWeb's global registry (see `register` function of QWeb). It first looks inside
the local `widgets` key, then fallbacks on the global registry.
In this example, the child widget will receive the object `{count: 4}` in its
constructor. This will be assigned to the `props` variable, which can be accessed
on the widget (and also, in the template). Whenever the state is updated, then
the subwidget will also be updated automatically.
The `t-widget` directive also accepts dynamic values with string interpolation
(like the [`t-attf-`](#dynamic-attributes-t-att-and-t-attf-directives) directive):
```xml
<div t-name="ParentWidget">
<t t-widget="ChildWidget#{id}"/>
</div>
```
```js
class ParentWidget {
widgets = { ChildWidget1, ChildWidget2 };
state = { id: 1 };
}
```
Similarly to `t-attf-`, there is an alternate form of string interpolation:
```xml
<div t-name="ParentWidget">
<t t-widget="ChildWidget{{id}}"/>
</div>
```
### `t-ref` directive
The `t-ref` directive helps a component keep reference to some inside part of it.
@@ -444,6 +503,26 @@ some method on a sub widget.
Note: if used on a component, the reference will be set in the `refs`
variable between `willPatch` and `patched`.
The `t-ref` directive also accepts dynamic values with string interpolation
(like the [`t-attf-`](#dynamic-attributes-t-att-and-t-attf-directives) and
[`t-widget-`](#component-t-widget-t-props) directives). For example, if we have
`id` set to 44 in the rendering context,
```xml
<div t-ref="widget_#{id}"/>
```
```js
this.refs.widget_44;
```
Similarly to `t-attf-` and `t-widget`, there is an alternate form of string
interpolation:
```xml
<div t-ref="widget_{{id}}"/>
```
### `t-key` directive
Even though Owl tries to be as declarative as possible, some DOM state is still
@@ -478,10 +557,10 @@ directive is there to help.
Whenever a node has a `t-transition` directive, with a `name` value, the following
will happen:
At node creation:
At node insertion:
- the css classes `name-enter` and `name-enter-active` will be added before the
node is added to the DOM,
- 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),
@@ -516,7 +595,13 @@ For example, a simple fade in/out effect can be done with this:
}
```
Note: more information on animations are available [here](doc/animations.md).
The `t-transition` directive can be combined with `t-widget`.
Notes:
- more information on animations are available [here](animations.md).
- the value for `t-transition` must be a single class name. Owl does not support
more than one transition on a single node.
### `t-mounted` directive
+1
View File
@@ -13,4 +13,5 @@
## Miscellaneous
- [Quick Start](quick_start.md)
- [Animations](animations.md)
- [Tooling](tooling.md)
- [Comparison with React/Vue](comparison.md)
+18
View File
@@ -0,0 +1,18 @@
# 🦉 Tooling 🦉
## 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.
-2
View File
@@ -10,7 +10,6 @@ functions are all available in the `owl.utils` namespace.
- [`loadTemplates`](#loadtemplates)
- [`escape`](#escape)
- [`debounce`](#debounce)
- [`patch` and `unpatch`](#patchandunpatch)
## `whenReady`
@@ -33,7 +32,6 @@ owl.utils.whenReady(function () {
## `debounce`
## `patch` and `unpatch`
+84
View File
@@ -0,0 +1,84 @@
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();
+13
View File
@@ -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,32 @@
<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>
+30 -16
View File
@@ -3,27 +3,41 @@
<head>
<meta charset="UTF-8">
<title>OWL Extras</title>
<link rel="stylesheet" href="main.css">
</head>
<body>
<h1>🦉 OWL Extra Stuff 🦉</h1>
<div class="title">
<h1>🦉 OWL Extra Stuff 🦉</h1>
<div>Mysterious OWL: A web framework for structured, dynamic and maintainable applications</div>
</div>
<h2>Benchmarks</h2>
<p>The goal is to compare some operations on large lists</p>
<div class="section misc">
<h2>Misc</h2>
<div class="links">
<a href="playground">Playground (local)</a>
<a href="https://github.com/odoo/owl">Github</a>
<a href="https://odoo.github.io/owl/">Owl main page</a>
<a href="https://github.com/odoo/owl/blob/master/doc/readme.md">Owl Documentation</a>
</div>
</div>
<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>
<div class="section benchmarks">
<h2>Benchmarks</h2>
<ul>
<li><a href="benchmarks/odoo-widgets-12.0">Odoo Widgets (12.0)</a></li>
<li><a href="benchmarks/odoo-widgets-12.3">Odoo Widgets (12.3)</a></li>
<li><a href="benchmarks/owl-0.7.0">OWL 0.7.0</a></li>
<li><a href="benchmarks/owl-0.8.0">OWL 0.8.0</a></li>
<li><a href="benchmarks/owl-0.9.0">OWL 0.9.0</a></li>
<li><a href="benchmarks/owl-0.10.0">OWL 0.10.0</a></li>
<li><a href="benchmarks/owl-0.11.0">OWL 0.11.0</a></li>
<li><a href="benchmarks/owl-master">OWL Master</a></li>
<li><a href="benchmarks/vue">Vue</a></li>
<li><a href="benchmarks/react">React</a></li>
</ul>
</div>
<h2>Playground</h2>
<a href="playground">Playground</a>
</body>
</html>
+23
View File
@@ -0,0 +1,23 @@
body {
font-family: sans-serif;
}
.title, .section {
width: 768px;
margin: auto;
}
.title div {
color: #333333;
font-style: italic;
}
.section {
background-color: #d5d5d5;
padding: 16px;
margin-top: 10px;
margin-bottom: 10px;
}
.links a {
margin: 10px 5px;
}
+3 -3
View File
@@ -84,7 +84,7 @@ function makeCodeIframe(js, css, xml, errorHandler) {
owlScript.addEventListener("load", () => {
const script = doc.createElement("script");
script.type = "text/javascript";
const content = `window.TEMPLATES = \`${sanitizedXML}\`\n${js}`;
const content = `owl.__info__.mode = 'dev';\nwindow.TEMPLATES = \`${sanitizedXML}\`\n${js}`;
script.innerHTML = content;
iframe.contentWindow.addEventListener("error", errorHandler);
iframe.contentWindow.addEventListener("unhandledrejection", errorHandler);
@@ -153,7 +153,7 @@ owl.utils.whenReady(startApp);`;
class App extends owl.Component {
constructor(...args) {
super(...args);
this.version = owl._version;
this.version = owl.__info__.version;
this.SAMPLES = SAMPLES;
this.widgets = { TabbedEditor };
@@ -349,7 +349,7 @@ class TabbedEditor extends owl.Component {
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
document.title = `${document.title} (v${owl._version})`;
document.title = `${document.title} (v${owl.__info__.version})`;
document.addEventListener("DOMContentLoaded", async function() {
const templates = await owl.utils.loadTemplates("templates.xml");
const qweb = new owl.QWeb(templates);
+129 -221
View File
@@ -40,31 +40,34 @@ const counter = new ClickCounter({ qweb });
counter.mount(document.body);
`;
const WIDGET_COMPOSITION = `class ClickCounter extends owl.Component {
constructor(parent, props) {
super(parent, props);
this.state = { value: props.initialState || 0 };
}
const WIDGET_COMPOSITION = `// This example will not work if your browser does not support ESNext class fields
class ClickCounter extends owl.Component {
state = { value: 0 };
increment() {
this.state.value++;
}
}
let nextId = 1;
class App extends owl.Component {
constructor() {
super(...arguments);
this.state = { counters: [] }
this.widgets = { ClickCounter };
}
addCounter() {
this.state.counters.push(nextId++);
class InputWidget extends owl.Component {
state = {text: ""};
updateVal(event) {
let value = event.target.value;
if (this.props.reverse) {
value = value.split("").reverse().join("");
}
this.state.text = value
}
}
// Main root widget
class App extends owl.Component {
widgets = {ClickCounter, InputWidget};
}
// Application setup
const qweb = new owl.QWeb(TEMPLATES);
const app = new App({ qweb });
app.mount(document.body);
@@ -75,29 +78,49 @@ const WIDGET_COMPOSITION_XML = `<templates>
Click Me! [<t t-esc="state.value"/>]
</button>
<div t-name="InputWidget">
<input t-on-input="updateVal" placeholder="type here some text..."/>
<span><t t-esc="state.text"/></span>
</div>
<div t-name="App">
<div><button t-on-click="addCounter">Add a counter</button></div>
<div>
<t t-foreach="state.counters" t-as="counter">
<t t-widget="ClickCounter" t-key="counter" t-props="{initialState: counter}"/>
</t>
</div>
<t t-widget="ClickCounter"/>
<t t-widget="InputWidget" t-props="{reverse: true}"/>
</div>
</templates>`;
const WIDGET_COMPOSITION_CSS = `button {
color: darkred;
font-size: 30px;
const WIDGET_COMPOSITION_CSS = `button, input {
font-size: 20px;
width: 220px;
margin: 5px;
}`;
const ANIMATION = `// This example will not work if your browser does not support ESNext class fields
class ClickCounter extends owl.Component {
state = { value: 0 };
increment() {
this.state.value++;
}
}
class App extends owl.Component {
state = {flag: 0};
state = {flag: false, widgetFlag: false, numbers: []};
widgets = {ClickCounter};
toggle() {
this.state.flag = !this.state.flag;
}
toggleWidget() {
this.state.widgetFlag = !this.state.widgetFlag;
}
addNumber() {
const n = this.state.numbers.length + 1;
this.state.numbers.push(n);
}
}
const qweb = new owl.QWeb(TEMPLATES);
@@ -106,33 +129,82 @@ app.mount(document.body);
`;
const ANIMATION_XML = `<templates>
<div t-name="App">
<button t-on-click="toggle">
Click Me!
<button t-name="ClickCounter" t-on-click="increment" class="clickcounter">
Click Me! [<t t-esc="state.value"/>]
</button>
<div>
<div t-if="state.flag" class="square" t-transition="fade">Hello</div>
<div t-name="App">
<h2>Transition on DOM element</h2>
<div class="demo">
<button t-on-click="toggle">Toggle square</button>
<div>
<div t-if="state.flag" class="square" t-transition="fade">Hello</div>
</div>
</div>
<h2>Transition on sub widgets</h2>
<div class="demo">
<button t-on-click="toggleWidget">
Toggle widget
</button>
<div>
<t t-widget="ClickCounter" t-if="state.widgetFlag" t-transition="fade"/>
</div>
</div>
<h2>Transition on lists</h2>
<p>Transitions can also be applied on lists</p>
<div class="demo">
<button t-on-click="addNumber">Add a number</button>
<div>
<t t-foreach="state.numbers" t-as="n">
<span t-transition="fade" class="numberspan"><t t-esc="n"/></span>
</t>
</div>
</div>
<h2>Simple CSS animation</h2>
<p>Remember, normal CSS still apply: for example, a simple flash animation with pure css </p>
<div><a class="btn flash">Click</a></div>
</div>
</templates>
`;
const ANIMATION_CSS = `button {
width: 100px;
height: 30px;
font-size: 20px;
font-size: 18px;
height: 35px;
}
.btn {
cursor: pointer;
padding: 5px;
margin: 5px;
background-color: #dddddd;
}
.flash {
background-position: center;
transition: background 0.5s;
}
.flash:active {
background-color: gray;
transition: background 0s;
}
.square {
background-color: red;
width: 100px;
height: 100px;
height: 70px;
color: white;
margin: 20px;
margin: 0 20px;
font-size: 24px;
line-height: 100px;
line-height: 70px;
text-align: center;
line-height: 100px;
}
.fade-enter-active, .fade-leave-active {
@@ -141,6 +213,24 @@ const ANIMATION_CSS = `button {
.fade-enter, .fade-leave-to {
opacity: 0;
}
.demo {
display: flex;
height: 80px;
}
.clickcounter {
margin-left: 20px;
height: 50px;
background-color: blue;
color: white;
}
.numberspan {
border: 1px solid green;
margin: 5px;
padding: 5px;
}
`;
@@ -203,182 +293,6 @@ const LIFECYCLE_DEMO_XML = `<templates>
<div t-name="HookWidget" t-on-click="increment">Demo Sub Widget. Props: <t t-esc="props.n"/>. State: <t t-esc="state.n"/>. (click on me to update me)</div>
</templates>`;
const BENCHMARK_APP = `//------------------------------------------------------------------------------
// Generating demo data
//------------------------------------------------------------------------------
const messages = [];
const authors = ["Aaron", "David", "Vincent"];
const content = [
"Lorem ipsum dolor sit amet",
"Sed ut perspiciatis unde omnis iste natus error sit voluptatem",
"Excepteur sint occaecat cupidatat non proident"
];
function chooseRandomly(array) {
const index = Math.floor(Math.random() * array.length);
return array[index];
}
for (let i = 1; i < 16000; i++) {
messages.push({
id: i,
author: chooseRandomly(authors),
msg: \`\${i}: \${chooseRandomly(content)}\`,
likes: 0
});
}
//------------------------------------------------------------------------------
// Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
constructor(parent, props) {
super(parent, props);
this.state = { counter: props.initialState || 0 };
}
increment(delta) {
this.state.counter += delta;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
constructor() {
super(...arguments);
this.widgets = { Counter };
}
removeMessage() {
this.trigger("remove_message", {
id: this.props.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
constructor() {
super(...arguments);
this.widgets = { Message };
this.state = { messages: messages.slice(0, 10) };
}
setMessageCount(n) {
this.state.messages = messages.slice(0, n);
}
removeMessage(data) {
const index = messages.findIndex(m => m.id === data.id);
this.state.messages.splice(index, 1);
}
increment(delta) {
const n = this.state.messages.length + delta;
this.setMessageCount(n);
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
const env = {
qweb: new owl.QWeb(TEMPLATES)
};
const app = new App(env);
app.mount(document.body);
`;
const BENCHMARK_APP_CSS = `.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;
}
.left-thing button {
width: 100%;
}
.left-thing .counter span {
color: white;
}
.left-thing .counter button {
width: 40px;
}
.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;
}`;
const BENCHMARK_APP_XML = `<templates>
<div t-name="App" class="main">
<div class="left-thing">
<div class="counter">
<button t-on-click="increment(-1)">-</button>
<span style="font-weight:bold">Value: <t t-esc="state.messages.length"/></span>
<button t-on-click="increment(1)">+</button>
</div>
<button t-on-click="setMessageCount(10)">10 messages</button>
<button t-on-click="setMessageCount(20)">20 messages</button>
<button t-on-click="setMessageCount(500)">500 messages</button>
<button t-on-click="setMessageCount(1000)">1000 messages</button>
<button t-on-click="setMessageCount(5000)">5000 messages</button>
<button t-on-click="setMessageCount(15000)">15000 messages</button>
</div>
<div class="right-thing">
<div class="content">
<t t-foreach="state.messages" t-as="message">
<t t-widget="Message" t-att-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" t-props="{initialState: props.id}"/>
</div>
<div t-name="Counter">
<button t-on-click="increment(-1)">-</button>
<span style="font-weight:bold">Value: <t t-esc="state.counter"/></span>
<button t-on-click="increment(1)">+</button>
</div>
</templates>`;
const TODO_APP_STORE = `const ENTER_KEY = 13;
const ESC_KEY = 27;
const LOCALSTORAGE_KEY = "todos-odoo";
@@ -637,7 +551,7 @@ const TODO_APP_STORE_XML = `<templates>
</label>
<button class="destroy" t-on-click="removeTodo"></button>
</div>
<input class="edit" t-ref="'input'" t-if="state.isEditing" t-att-value="props.title" t-on-keyup="handleKeyup" t-mounted="focusInput" t-on-blur="handleBlur"/>
<input class="edit" t-ref="input" t-if="state.isEditing" t-att-value="props.title" t-on-keyup="handleKeyup" t-mounted="focusInput" t-on-blur="handleBlur"/>
</li>
</templates>`;
@@ -1205,12 +1119,6 @@ export const SAMPLES = [
code: LIFECYCLE_DEMO,
xml: LIFECYCLE_DEMO_XML
},
{
description: "Benchmark application",
code: BENCHMARK_APP,
css: BENCHMARK_APP_CSS,
xml: BENCHMARK_APP_XML
},
{
description: "Todo List App (with store)",
code: TODO_APP_STORE,
+3 -3
View File
@@ -2,12 +2,12 @@
<div t-name="TabbedEditor" class="tabbed-editor">
<div class="tabBar" t-att-class="{resizeable: props.resizeable}" t-on-mousedown="onMouseDown">
<t t-foreach="['js', 'xml', 'css']" t-as="tab">
<a t-ref="tab" t-if="props[tab]" class="tab flash" t-att-class="{active: state.currentTab===tab}" t-on-click="setTab(tab)">
<a t-ref="{{tab}}" t-if="props[tab]" t-key="tab" class="tab flash" t-att-class="{active: state.currentTab===tab}" t-on-click="setTab(tab)">
<t t-esc="tab"/>
</a>
</t>
</div>
<div class="code-editor" t-ref="'editor'"></div>
<div class="code-editor" t-ref="editor"></div>
</div>
<div t-name="App" class="playground">
@@ -48,7 +48,7 @@
<div t-if="state.error" class="error">
<t t-esc="state.error"/>
</div>
<div class="content" t-ref="'content'"/>
<div class="content" t-ref="content"/>
</div>
</div>
</templates>
+4 -7
View File
@@ -1,6 +1,6 @@
{
"name": "owl",
"version": "0.11.0",
"version": "0.12.0",
"description": "Odoo Web Library (OWL)",
"main": "src/index.ts",
"scripts": {
@@ -34,16 +34,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
View File
@@ -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';`
}
};
+192 -62
View File
@@ -68,6 +68,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;
} = {};
@@ -98,6 +103,13 @@ export class Component<
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
@@ -223,24 +235,27 @@ export class Component<
}
_callMounted() {
const children = this.__owl__.children;
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();
}
}
this.__owl__.isMounted = true;
for (let key in this.__owl__.mountedHandlers) {
this.__owl__.mountedHandlers[key]();
__owl__.isMounted = true;
const handlers = __owl__.mountedHandlers;
for (let key in handlers) {
handlers[key]();
}
this.mounted();
}
_callWillUnmount() {
this.willUnmount();
this.__owl__.isMounted = false;
const children = this.__owl__.children;
const __owl__ = this.__owl__;
__owl__.isMounted = false;
const children = __owl__.children;
for (let id in children) {
const comp = children[id];
if (comp.__owl__.isMounted) {
@@ -257,7 +272,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,26 +281,23 @@ 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]);
}
@@ -292,9 +305,10 @@ export class Component<
}
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();
}
@@ -302,23 +316,24 @@ export class Component<
}
_destroy(parent) {
const isMounted = this.__owl__.isMounted;
const __owl__ = this.__owl__;
const isMounted = __owl__.isMounted;
if (isMounted) {
this.willUnmount();
this.__owl__.isMounted = false;
__owl__.isMounted = false;
}
const children = Object.values(this.__owl__.children);
for (let child of children) {
child._destroy(this);
const children = __owl__.children;
for (let key in children) {
children[key]._destroy(this);
}
if (parent) {
let id = this.__owl__.id;
let id = __owl__.id;
delete parent.__owl__.children[id];
this.__owl__.parent = null;
__owl__.parent = null;
}
this.clear();
this.__owl__.isDestroyed = true;
delete this.__owl__.vnode;
__owl__.isDestroyed = true;
delete __owl__.vnode;
}
shouldUpdate(nextProps: Props): boolean {
@@ -335,11 +350,12 @@ 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);
}
}
@@ -359,6 +375,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 +389,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,7 +435,7 @@ 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();
}
@@ -417,25 +443,26 @@ export class Component<
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 +470,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
View File
@@ -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
View File
@@ -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;
+8 -6
View File
@@ -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);
@@ -112,10 +115,9 @@ export class Observer {
}
_observeObj<T extends { __owl__?: any }>(obj: T, parent?: any) {
const keys = Object.keys(obj);
obj.__owl__ = { rev: this.rev, deepRev: this.rev, parent };
Object.defineProperty(obj, "__owl__", { enumerable: false });
for (let key of keys) {
for (let key in obj) {
this._addProp(obj, key, obj[key]);
}
}
@@ -125,7 +127,7 @@ export class Observer {
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);
}
}
+62 -17
View File
@@ -96,7 +96,13 @@ const NODE_HOOKS_PARAMS = {
remove: "(vn, rm)"
};
export const UTILS = {
interface Utils {
h: typeof h;
objectToAttrString(obj: Object): string;
[key: string]: any;
}
export const UTILS: Utils = {
h: h,
objectToAttrString(obj: Object): string {
let classes: string[] = [];
@@ -126,6 +132,10 @@ 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
@@ -148,6 +158,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 +277,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");
@@ -338,17 +361,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 +483,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;
@@ -518,17 +541,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}`);
}
@@ -604,6 +623,8 @@ export class Context {
rootContext: Context;
caller: Element | undefined;
shouldDefineOwner: boolean = false;
shouldDefineQWeb: boolean = false;
shouldDefineUtils: boolean = false;
shouldProtectContext: boolean = false;
inLoop: boolean = false;
inPreTag: boolean = false;
@@ -696,8 +717,8 @@ export class Context {
let invarPos = 0;
let r = "";
chars.push(" ");
for (var i = 0, ilen = chars.length; i < ilen; i++) {
var c = chars[i];
for (let i = 0, ilen = chars.length; i < ilen; i++) {
let c = chars[i];
if (instring.length) {
if (c === instring && chars[i - 1] !== "\\") {
instring = "";
@@ -730,4 +751,28 @@ export class Context {
const result = r.slice(0, -1);
return result;
}
/**
* Perform string interpolation on the given string. Note that if the whole
* string is an expression, it simply returns it (formatted).
* For instance:
* 'Hello {{x}}!' -> `Hello ${x}`
* '{{x}}' -> x
*/
interpolate(s: string): string {
let matches = s.match(/\{\{.*?\}\}/g);
if (matches && matches[0].length === s.length) {
return this.formatExpression(s.slice(2, -2));
}
matches = s.match(/\#\{.*?\}/g);
if (matches && matches[0].length === s.length) {
return this.formatExpression(s.slice(2, -1));
}
let formatter = expr => "${" + this.formatExpression(expr) + "}";
let r = s
.replace(/\{\{.*?\}\}/g, s => formatter(s.slice(2, -2)))
.replace(/\#\{.*?\}/g, s => formatter(s.slice(2, -1)));
return "`" + r + "`";
}
}
+3 -2
View File
@@ -250,14 +250,15 @@ QWeb.addDirective({
ctx.addLine(
`var _${keysID} = _${arrayID} instanceof Array ? _${arrayID} : Object.keys(_${arrayID});`
);
ctx.addLine(`var _length${keysID} = _${keysID}.length;`);
let valuesID = ctx.generateID();
ctx.addLine(
`var _${valuesID} = _${arrayID} instanceof Array ? _${arrayID} : Object.values(_${arrayID});`
);
ctx.addLine(`for (let i = 0; i < _${keysID}.length; i++) {`);
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}_last = i === _length${keysID} - 1;`);
ctx.addLine(`context.${name}_parity = i % 2 === 0 ? 'even' : 'odd';`);
ctx.addLine(`context.${name}_index = i;`);
ctx.addLine(`context.${name} = _${keysID}[i];`);
+88 -38
View File
@@ -16,33 +16,56 @@ import { QWeb, UTILS } from "./qweb_core";
//------------------------------------------------------------------------------
// 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 +80,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 +90,13 @@ 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(elm: HTMLElement, name: string) {
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,7 +108,7 @@ QWeb.addDirective({
});
};
(<any>UTILS).transitionRemove = function(
UTILS.transitionRemove = function(
elm: HTMLElement,
name: string,
rm: () => void
@@ -97,7 +117,7 @@ QWeb.addDirective({
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,10 +166,9 @@ 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);`
};
@@ -173,7 +192,10 @@ 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'"
* t-props="{flag:state.flag}"
* t-transition="fade"/>
* ```
*
* ```js
@@ -233,6 +255,8 @@ QWeb.addDirective({
* }
* w4 = new W4(owner, props4);
*
* let utils = this.utils;
*
* // Whenever we rerender the parent widget, we need to be sure that we
* // are able to find the widget instance. To do that, we register it to
* // the parent cmap (children map). Note that the 'template' key is
@@ -262,15 +286,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.elm, "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.elm, "fade", finalize);
* }
* };
* // the pvnode is inserted at the correct position in the div's children
@@ -313,16 +343,21 @@ QWeb.addDirective({
atNodeEncounter({ ctx, value, node }): boolean {
ctx.addLine("//WIDGET");
ctx.rootContext.shouldDefineOwner = true;
ctx.rootContext.shouldDefineQWeb = true;
ctx.rootContext.shouldDefineUtils = true;
let props = node.getAttribute("t-props");
let keepAlive = node.getAttribute("t-keepalive") ? true : false;
// t-on- events...
// t-on- events and t-transition
const events: [string, string][] = [];
let transition: string = "";
const attributes = (<Element>node).attributes;
for (let i = 0; i < attributes.length; i++) {
const name = attributes[i].name;
if (name.startsWith("t-on-")) {
events.push([name.slice(5), attributes[i].textContent!]);
} else if (name === "t-transition") {
transition = attributes[i].textContent!;
}
}
@@ -356,16 +391,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.elm, '${transition}');`;
}
let finalizeWidgetCode = `w${widgetID}.${
keepAlive ? "unmount" : "destroy"
}()`;
}();`;
if (ref) {
finalizeWidgetCode += `;delete context.refs[${refKey}]`;
finalizeWidgetCode += `delete context.refs[${refKey}];`; // FIXME: shouldn't we keep ref if keepAlive is true?
}
if (transition) {
finalizeWidgetCode = `let finalize = () => {
${finalizeWidgetCode}
};
utils.transitionRemove(vn.elm, '${transition}', finalize);`;
}
let createHook = "";
let classAttr = node.getAttribute("class");
let tattClass = node.getAttribute("t-att-class");
@@ -414,11 +459,14 @@ QWeb.addDirective({
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(
@@ -428,8 +476,10 @@ QWeb.addDirective({
ctx.addLine(`w${widgetID}.on('${event}', owner, owner['${method}'])`);
}
ctx.addLine(`def${defID} = w${widgetID}._prepare();`);
// hack: specify empty remove hook to prevent the node from being removed from the DOM
// FIXME: click to re-add widget during remove transition -> leak
ctx.addLine(
`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn){let nvn=w${widgetID}._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}},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;});`
);
@@ -441,7 +491,7 @@ QWeb.addDirective({
);
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};${
+29 -17
View File
@@ -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;
}
}
@@ -162,6 +165,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 +197,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 +238,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 +265,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 +274,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 +405,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 +418,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 +469,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 +491,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 +658,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 +679,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 +702,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 +800,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);
}
}
+119
View File
@@ -0,0 +1,119 @@
// 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 && props4 !== w4.__owl__.renderProps) {
w4.destroy();
w4 = false
}
if (!w4) {
let widgetKey4 = \`Child\`;
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4._prepare();
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn.elm, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
w4.destroy();
};
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);
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 && props4 !== w4.__owl__.renderProps) {
w4.destroy();
w4 = false
}
if (!w4) {
let widgetKey4 = \`Child\`;
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4._prepare();
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn.elm, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
w4.destroy();
};
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);
}
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.elm, 'jupiler');
},
remove: (vn, rm) => {
this.utils.transitionRemove(vn.elm, '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.elm, 'chimay');
},
remove: (vn, rm) => {
this.utils.transitionRemove(vn.elm, 'chimay', rm);
},
};
c1.push({text: \`blue\`});
return vn1;
}"
`;
+105 -17
View File
@@ -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};
@@ -19,12 +21,13 @@ exports[`class and style attributes with t-widget dynamic t-att-style is properl
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.then(()=>{if (w4.__owl__.isDestroyed) {return};w4.el.style=_5;let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
@@ -37,6 +40,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};
@@ -53,8 +58,9 @@ exports[`class and style attributes with t-widget t-att-class is properly added/
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,7 +68,7 @@ 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.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;});
@@ -75,6 +81,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;
@@ -84,10 +92,11 @@ exports[`composition sub widgets with some state rendered in a loop 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 _length3 = _3.length;
var _4 = _2 instanceof Array ? _2 : Object.values(_2);
for (let i = 0; i < _3.length; i++) {
for (let i = 0; i < _length3; i++) {
context.number_first = i === 0;
context.number_last = i === _3.length - 1;
context.number_last = i === _length3 - 1;
context.number_parity = i % 2 === 0 ? 'even' : 'odd';
context.number_index = i;
context.number = _3[i];
@@ -104,12 +113,13 @@ exports[`composition sub widgets with some state rendered in a loop 1`] = `
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.then(()=>{if (w7.__owl__.isDestroyed) {return};let pvnode=w7.__owl__.pvnode;c1[_5_index]=pvnode;});
@@ -120,9 +130,83 @@ exports[`composition sub widgets with some state rendered in a loop 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};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
w4.destroy();
w4 = false
}
if (!w4) {
let widgetKey4 = context['state'].widget;
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4._prepare();
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
return vn1;
}"
`;
exports[`composition t-widget with dynamic value 2 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let QWeb = this.constructor;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
w4.destroy();
w4 = false
}
if (!w4) {
let widgetKey4 = \`Widget\${context['state'].widget}\`;
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4._prepare();
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
return vn1;
}"
`;
exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let QWeb = this.constructor;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
@@ -139,12 +223,13 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
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[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;});
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: key5, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
@@ -157,6 +242,8 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
exports[`random stuff/miscellaneous t-props should not be undefined (snapshotting) 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let QWeb = this.constructor;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
@@ -172,12 +259,13 @@ exports[`random stuff/miscellaneous t-props should not be undefined (snapshottin
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=>{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.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
+196 -65
View File
@@ -1,32 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`animations t-transition, on a simple node 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;
}"
`;
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 +59,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 +70,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;
@@ -335,10 +308,11 @@ exports[`foreach does not pollute the rendering context 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 _length3 = _3.length;
var _4 = _2 instanceof Array ? _2 : Object.values(_2);
for (let i = 0; i < _3.length; i++) {
for (let i = 0; i < _length3; i++) {
context.item_first = i === 0;
context.item_last = i === _3.length - 1;
context.item_last = i === _length3 - 1;
context.item_parity = i % 2 === 0 ? 'even' : 'odd';
context.item_index = i;
context.item = _3[i];
@@ -363,10 +337,11 @@ exports[`foreach iterate on items (on a element node) 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 _length3 = _3.length;
var _4 = _2 instanceof Array ? _2 : Object.values(_2);
for (let i = 0; i < _3.length; i++) {
for (let i = 0; i < _length3; i++) {
context.item_first = i === 0;
context.item_last = i === _3.length - 1;
context.item_last = i === _length3 - 1;
context.item_parity = i % 2 === 0 ? 'even' : 'odd';
context.item_index = i;
context.item = _3[i];
@@ -394,10 +369,11 @@ exports[`foreach iterate on items 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 _length3 = _3.length;
var _4 = _2 instanceof Array ? _2 : Object.values(_2);
for (let i = 0; i < _3.length; i++) {
for (let i = 0; i < _length3; i++) {
context.item_first = i === 0;
context.item_last = i === _3.length - 1;
context.item_last = i === _length3 - 1;
context.item_parity = i % 2 === 0 ? 'even' : 'odd';
context.item_index = i;
context.item = _3[i];
@@ -434,10 +410,11 @@ exports[`foreach iterate, dict param 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 _length3 = _3.length;
var _4 = _2 instanceof Array ? _2 : Object.values(_2);
for (let i = 0; i < _3.length; i++) {
for (let i = 0; i < _length3; i++) {
context.item_first = i === 0;
context.item_last = i === _3.length - 1;
context.item_last = i === _length3 - 1;
context.item_parity = i % 2 === 0 ? 'even' : 'odd';
context.item_index = i;
context.item = _3[i];
@@ -479,10 +456,11 @@ exports[`foreach iterate, integer param 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 _length3 = _3.length;
var _4 = _2 instanceof Array ? _2 : Object.values(_2);
for (let i = 0; i < _3.length; i++) {
for (let i = 0; i < _length3; i++) {
context.item_first = i === 0;
context.item_last = i === _3.length - 1;
context.item_last = i === _length3 - 1;
context.item_parity = i % 2 === 0 ? 'even' : 'odd';
context.item_index = i;
context.item = _3[i];
@@ -519,10 +497,11 @@ exports[`foreach iterate, position 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 _length3 = _3.length;
var _4 = _2 instanceof Array ? _2 : Object.values(_2);
for (let i = 0; i < _3.length; i++) {
for (let i = 0; i < _length3; i++) {
context.elem_first = i === 0;
context.elem_last = i === _3.length - 1;
context.elem_last = i === _length3 - 1;
context.elem_parity = i % 2 === 0 ? 'even' : 'odd';
context.elem_index = i;
context.elem = _3[i];
@@ -556,10 +535,11 @@ exports[`foreach warn if no key in some case 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 _length3 = _3.length;
var _4 = _2 instanceof Array ? _2 : Object.values(_2);
for (let i = 0; i < _3.length; i++) {
for (let i = 0; i < _length3; i++) {
context.item_first = i === 0;
context.item_last = i === _3.length - 1;
context.item_last = i === _length3 - 1;
context.item_parity = i % 2 === 0 ? 'even' : 'odd';
context.item_index = i;
context.item = _3[i];
@@ -616,10 +596,11 @@ exports[`misc global 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 _length3 = _3.length;
var _4 = _2 instanceof Array ? _2 : Object.values(_2);
for (let i = 0; i < _3.length; i++) {
for (let i = 0; i < _length3; i++) {
context.value_first = i === 0;
context.value_last = i === _3.length - 1;
context.value_last = i === _length3 - 1;
context.value_parity = i % 2 === 0 ? 'even' : 'odd';
context.value_index = i;
context.value = _3[i];
@@ -1125,10 +1106,11 @@ exports[`t-key t-key directive in a list 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 _length3 = _3.length;
var _4 = _2 instanceof Array ? _2 : Object.values(_2);
for (let i = 0; i < _3.length; i++) {
for (let i = 0; i < _length3; i++) {
context.beer_first = i === 0;
context.beer_last = i === _3.length - 1;
context.beer_last = i === _length3 - 1;
context.beer_parity = i % 2 === 0 ? 'even' : 'odd';
context.beer_index = i;
context.beer = _3[i];
@@ -1154,7 +1136,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,7 +1153,10 @@ 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;
}"
@@ -1181,7 +1169,10 @@ exports[`t-on can bind handlers with empty object (with non empty inner string 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;
}"
@@ -1194,7 +1185,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;
}"
@@ -1212,10 +1206,11 @@ exports[`t-on can bind handlers with loop variable as argument 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 _length3 = _3.length;
var _4 = _2 instanceof Array ? _2 : Object.values(_2);
for (let i = 0; i < _3.length; i++) {
for (let i = 0; i < _length3; i++) {
context.action_first = i === 0;
context.action_last = i === _3.length - 1;
context.action_last = i === _length3 - 1;
context.action_parity = i % 2 === 0 ? 'even' : 'odd';
context.action_index = i;
context.action = _3[i];
@@ -1226,7 +1221,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 +1238,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 +1254,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 +1276,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 +1494,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 +1513,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;
@@ -1405,10 +1534,11 @@ exports[`t-ref refs in a loop 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 _length3 = _3.length;
var _4 = _2 instanceof Array ? _2 : Object.values(_2);
for (let i = 0; i < _3.length; i++) {
for (let i = 0; i < _length3; i++) {
context.item_first = i === 0;
context.item_last = i === _3.length - 1;
context.item_last = i === _length3 - 1;
context.item_parity = i % 2 === 0 ? 'even' : 'odd';
context.item_index = i;
context.item = _3[i];
@@ -1416,7 +1546,7 @@ exports[`t-ref refs in a loop 1`] = `
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;
@@ -1563,10 +1693,11 @@ exports[`t-set t-set should reuse variable if possible 1`] = `
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 _length4 = _4.length;
var _5 = _3 instanceof Array ? _3 : Object.values(_3);
for (let i = 0; i < _4.length; i++) {
for (let i = 0; i < _length4; i++) {
context.elem_first = i === 0;
context.elem_last = i === _4.length - 1;
context.elem_last = i === _length4 - 1;
context.elem_parity = i % 2 === 0 ? 'even' : 'odd';
context.elem_index = i;
context.elem = _4[i];
+33
View File
@@ -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);
}
}
+290
View File
@@ -0,0 +1,290 @@
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">blue</span></div>'
);
cb();
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-leave-active chimay-leave-to">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();
});
});
+61 -26
View File
@@ -1,4 +1,5 @@
import { Component, Env } from "../src/component";
import { QWeb } from "../src/qweb_core";
import {
makeDeferred,
makeTestFixture,
@@ -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>`
);
@@ -1229,6 +1254,36 @@ 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)", () => {
@@ -1775,7 +1830,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",
@@ -2253,7 +2307,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 +2318,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", () => {
+50 -1
View File
@@ -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;
}
+263
View File
@@ -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);
});
});
+118 -53
View File
@@ -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
//------------------------------------------------------------------------------
@@ -969,20 +935,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 +1070,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 +1245,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();
});
});
+1 -1
View File
@@ -876,7 +876,7 @@ describe("connecting a component to store", () => {
"Parent",
`
<div>
<t t-widget="Child" t-ref="'child'" t-props="{key: props.current}"/>
<t t-widget="Child" t-props="{key: props.current}"/>
</div>
`
);