Compare commits

..

34 Commits

Author SHA1 Message Date
Géry Debongnie 671965026d [REL] bump to v0.11.0 2019-05-17 23:34:14 +02:00
Géry Debongnie bc7fe9023d [REM] utils: remove patch/unpatch
They are out of the scope for Owl.
2019-05-17 23:26:42 +02:00
Géry Debongnie c2c7997df3 [IMP] owl: add extras:watch task
closes #118
2019-05-17 23:22:54 +02:00
Géry Debongnie a1d7e6c987 [FIX] qweb: support #{} syntax in attribute string interpolation
closes #113
2019-05-17 16:20:25 +02:00
Géry Debongnie a1e982aba5 [REF] tests: rename makeTestWEnv to makeTestEnv 2019-05-17 15:52:16 +02:00
Géry Debongnie 2ce027a0a6 [REF] component: remove isStarted key 2019-05-17 15:50:55 +02:00
Géry Debongnie 61f21511a5 [DOC] component: add detailed explanation for t-widget 2019-05-17 10:49:04 +02:00
Géry Debongnie 5682577870 [REF] component: yet another simplification for t-widget 2019-05-17 09:59:02 +02:00
Géry Debongnie 25b633cc63 [REF] component: simplify again t-widget code 2019-05-16 16:25:39 +02:00
Géry Debongnie f16b267931 [REF] component: simplify the t-widget directive
a little bit...
2019-05-16 15:29:40 +02:00
Géry Debongnie 30ec952588 [FIX] playground: keep separate sessions for each tab
also, small refactoring

closes #103
2019-05-16 13:00:24 +02:00
Géry Debongnie 46ba77d0cc [REF] playground: use t-foreach to reduce duplication 2019-05-16 12:04:50 +02:00
Géry Debongnie 042e376c2b [IMP] playground: debounce some handlers 2019-05-16 11:58:04 +02:00
Géry Debongnie b9f1af2ad5 [REF] playground: small refactoring 2019-05-16 11:57:52 +02:00
Géry Debongnie 7df0fdcc0a [DOC] qweb: add some documentation on owl qweb extensions 2019-05-16 10:23:18 +02:00
Géry Debongnie 73d743a1c3 [FIX] observer: newly observed object/arrays have higher rev
Some people apparently believe that the framework should magically
solves all problems from the programmers, shielding him from thinking.

It is not possible, and maybe a bad idea, because it will only delay the
inevitable...  If one does weird stuff without knowing what is going on,
someone will get hurt.

However, in this specific case, something can be done.

closes #104
2019-05-15 17:30:01 +02:00
Géry Debongnie 8758dfcedf [IMP] component: better error if cannot find widget
closes #54
2019-05-15 17:17:39 +02:00
Géry Debongnie 0a10bbc023 [IMP] component: remove support for inlineTemplate
closes #93
2019-05-15 15:54:18 +02:00
Géry Debongnie 9c1d369e94 [FIX] qweb/component: fix scoping issue with list of widgets
In a t-foreach loop, there was a bad interaction between variables
defined with the `var` keyword and asynchronous code.

closes #108
2019-05-15 15:42:45 +02:00
Géry Debongnie 9556696fa8 [REF] qweb: rename loadTemplates into addTemplates 2019-05-15 11:04:22 +02:00
Géry Debongnie 04eb720619 [IMP] qweb: add default template in QWeb 2019-05-15 10:55:17 +02:00
Géry Debongnie 8e098e8ed6 [REF] benchmarks: use new system for finding templates 2019-05-15 10:40:23 +02:00
Géry Debongnie c836cead58 [IMP] playground: remove explicit template keys 2019-05-15 10:40:23 +02:00
Géry Debongnie 361ecbe90b [IMP] component: deduce template name from component name
closes #102
2019-05-15 10:40:23 +02:00
Géry Debongnie ae8d7d3be2 [REF] qweb_core: move context after QWeb 2019-05-15 10:40:23 +02:00
Géry Debongnie ed4f92211c [DOC] component: add section on async rendering 2019-05-11 12:11:00 +02:00
Géry Debongnie f8804d6501 [REF] playground: use t-mounted directive in todo example 2019-05-11 00:03:26 +02:00
Géry Debongnie de7c680387 [ADD] extras: add owl 0.10.0 to list of benchmarks 2019-05-11 00:03:26 +02:00
Géry Debongnie 6dd7391cdc [DOC] utils: add some basic information 2019-05-10 21:04:58 +02:00
Géry Debongnie dfa7d5932d [REM] utils: remove memoize, parseXML function 2019-05-10 20:51:34 +02:00
Géry Debongnie 53911de00a [FIX] component: make t-widget work with t-elif
closes #97
2019-05-10 19:36:44 +02:00
Aaron Bohy 1760d0d336 [FIX] allow to combine directives using node hooks
e.g. t-ref and t-transition, or t-mounted and t-transition.
2019-05-10 19:31:09 +02:00
Aaron Bohy 0aba9748c9 [IMP] add t-mounted directive
closes #45
2019-05-10 19:31:09 +02:00
Aaron Bohy 8ee0973dab [REF] autoformat some files 2019-05-10 19:31:09 +02:00
32 changed files with 5084 additions and 1604 deletions
+17 -16
View File
@@ -36,11 +36,6 @@ Here is a short example to illustrate interactive widgets:
```javascript
class ClickCounter extends owl.Component {
inlineTemplate = `
<button t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>`;
state = { value: 0 };
increment() {
@@ -48,7 +43,12 @@ class ClickCounter extends owl.Component {
}
}
const qweb = new owl.QWeb();
const TEMPLATES = `
<button t-name="ClickCounter" t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>`;
const qweb = new owl.QWeb(TEMPLATES);
const counter = new ClickCounter({ qweb });
counter.mount(document.body);
```
@@ -59,19 +59,20 @@ 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.10.0.js](https://github.com/odoo/owl/releases/download/v0.10.0/owl.js)
- [owl-0.10.0.min.js](https://github.com/odoo/owl/releases/download/v0.10.0/owl.min.js)
- [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)
Some npm scripts are available:
| Command | Description |
| -------------------- | ------------------------------------------------------------------------------- |
| `npm install` | install every dependency required for this project |
| `npm run build` | build a bundle of _owl_ in the _/dist/_ folder |
| `npm run minify` | minify the prebuilt owl.js file |
| `npm run test` | run all tests |
| `npm run test:watch` | run all tests, and keep a watcher |
| `npm run extras` | build extras applications, start a static server (see [here](extras/readme.md)) |
| Command | Description |
| ---------------------- | ------------------------------------------------------------------------------- |
| `npm install` | install every dependency required for this project |
| `npm run build` | build a bundle of _owl_ in the _/dist/_ folder |
| `npm run minify` | minify the prebuilt owl.js file |
| `npm run test` | run all tests |
| `npm run test:watch` | run all tests, and keep a watcher |
| `npm run extras` | build extras applications, start a static server (see [here](extras/readme.md)) |
| `npm run extras:watch` | same as `extras`, but with a watcher to rebuild owl |
## Documentation
+40 -15
View File
@@ -10,6 +10,7 @@
- [Methods](#methods)
- [Lifecycle](#lifecycle)
- [Semantics](#semantics)
- [Asynchronous rendering](#asynchronous-rendering)
## Overview
@@ -29,9 +30,9 @@ OWL components are the building blocks for user interface. They are designed to
and follow the QWeb specification. This is a requirement for Odoo.
OWL components are defined as a subclass of Component. The rendering is
exclusively done by a [QWeb](qweb.md) template (either defined inline or preloaded in QWeb).
The rendering is done by QWeb, which will generate a virtual dom representation
of the widget, then patch the DOM to apply the changes in an efficient way.
exclusively done by a [QWeb](qweb.md) template (which needs to be preloaded in QWeb).
Rendering a component generates a virtual dom representation
of the widget, which is then patched to the DOM, in order to apply the changes in an efficient way.
OWL components observe their states, and rerender themselves whenever it is
changed. This is done by an [observer](observer.md).
@@ -42,7 +43,6 @@ Let us have a look at a simple component:
```javascript
class ClickCounter extends owl.Component {
template = "click-counter";
state = { value: 0 };
increment() {
@@ -52,7 +52,7 @@ class ClickCounter extends owl.Component {
```
```xml
<button t-name="click-counter" t-on-click="increment">
<button t-name="ClickCounter" t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>
```
@@ -61,7 +61,8 @@ Note that this code is written in ESNext style, so it will only run on the
latest browsers without a transpilation step.
This example show how a component should be defined: it simply subclasses the
Component class. It needs to define a template (`click-counter` here). Also,
Component class. If no `template` key is defined, then
Owl will use the component's name as template name. Here,
a state object is defined. It is not mandatory to use the state object, but it
is certainly encouraged. The state object is [observed](observer.md), and any
change to it will cause a rerendering.
@@ -74,7 +75,7 @@ templates are standard [QWeb](qweb.md) templates, but with an extra directive:
widgets:
```xml
<div t-name="parent">
<div t-name="ParentWidget">
<span>some text</span>
<t t-widget="MyWidget" t-props="{info: 13}"/>
</div>
@@ -82,7 +83,6 @@ widgets:
```js
class ParentWidget extends owl.Component {
template = 'parent';
widgets = { MyWidget: MyWidget};
...
}
@@ -100,7 +100,7 @@ additional css classes or style for the sub widget: css declared in `class`, `st
root widget element.
```xml
<div t-name="parent">
<div t-name="ParentWidget">
<t t-widget="MyWidget" class="someClass" style="font-weight:bold;" t-props="{info: 13}"/>
</div>
```
@@ -112,7 +112,7 @@ between a valid css class added by the component, or some custom code, and a
class that need to be removed. This is why we only support the explicit syntax
with a class object:
```js
```xml
<t t-widget="MyWidget" t-att-class="{a: state.flagA, b: state.flagB}" />
```
@@ -123,6 +123,10 @@ It exists in the context of an environment (`env`), which is propagated from a
parent to its children. The environment needs to have a QWeb instance, which
will be used to render the component template.
Be aware that the name of the component may be significant: if a component does
not define a `template` key, then Owl will lookup in QWeb to
find a template with the component name (or one of its ancestor).
### Properties
- **`el`** (HTMLElement | null): reference to the DOM root node of the element. It is `null` when the
@@ -130,12 +134,9 @@ will be used to render the component template.
- **`env`** (Object): the component environment, which contains a QWeb instance.
- **`template`** (string): a string, which is the name of the QWeb template that will render
- **`template`** (string, optional): if given, this is the name of the QWeb template that will render
the component.
- **`inlineTemplate`** (string, optional): a string that represents a xml template. If set,
this will be loaded into QWeb and used instead of the `template` property.
- **`state`** (Object): this is the location of the component's state, if there is
any. After the willStart method, the `state` property is observed, and each
change will cause the widget to rerender itself.
@@ -235,7 +236,6 @@ implemented in most cases:
```javascript
class ClickCounter extends owl.Component {
template = "click-counter";
state = { value: 0 };
...
@@ -431,3 +431,28 @@ Here is what Owl will do:
5. patching of `D`
6. `patched` hooks are called on `D`, `C`
## Asynchronous rendering
Working with asynchronous code always adds a lot of complexity to a system. Whenever
different parts of a system are active at the same time, one needs to think
carefully about all possible interactions. Clearly, this is also true for Owl
components.
There are two different common problems with Owl asynchronous rendering model:
- any widget can delay the rendering (initial and subsequent) of the whole
application
- for a given widget, there are two independant situations that will trigger an
asynchronous rerendering: a change in the state, or a change in the props.
These changes may be done at different times, and Owl has no way of knowing
how to reconcile the resulting renderings.
Here are a few tips on how to work with asynchronous widgets:
1. minimize the use of asynchronous widgets!
2. Maybe move the asynchronous logic in a store, which then triggers (mostly)
synchronous renderings
3. Lazy loading external libraries is a good use case for async rendering. This
is mostly fine, because we can assume that it will only takes a fraction of a
second, and only once (see `owl.utils.loadJS`)
+221 -38
View File
@@ -3,25 +3,30 @@
## Content
- [Overview](#overview)
- [QWeb Engine](#qweb-engine)
- [QWeb Specification](#qweb-specification)
- [Static html nodes](#static-html-nodes)
- [`t-esc` directive](#t-esc-directive)
- [`t-raw` directive](#t-raw-directive)
- [`t-set` directive](#t-set-directive)
- [`t-if` directive](#t-if-directive)
- [Expression evaluation](#expression-evaluation)
- [`t-att` directive (dynamic attributes)](#t-att-directive-dynamic-attributes)
- [`t-call` directive (sub templates)](#t-call-directive-sub-templates)
- [Static html nodes](#static-html-nodes)
- [`t-esc` directive](#t-esc-directive)
- [`t-raw` directive](#t-raw-directive)
- [`t-set` directive](#t-set-directive)
- [`t-if` directive](#t-if-directive)
- [Expression evaluation](#expression-evaluation)
- [Dynamic attributes (`t-att` and `t-attf` directives)](#dynamic-attributes-t-att-and-t-attf-directives)
- [`t-attf` directive (dynamic attributes)](#t-att-directive-dynamic-attributes)
- [`t-call` directive (sub templates)](#t-call-directive-sub-templates)
- [JS/OWL Specific Extensions](#jsowl-specific-extensions)
- [`t-on` directive](#t-on-directive)
- [Component: `t-widget`, `t-props`](#component-t-widget-t-props)
- [`t-ref` directive](#t-ref-directive)
- [`t-key` directive](#t-key-directive)
- [`t-transition` directive](#t-transition-directive)
- [Debugging (`t-debug` and `t-log`)](#debugging-t-debug-and-t-log)
- [White spaces](#white-spaces)
- [Root nodes](#root-nodes)
- [`t-on` directive](#t-on-directive)
- [Component: `t-widget`, `t-props`](#component-t-widget-t-props)
- [`t-ref` directive](#t-ref-directive)
- [`t-key` directive](#t-key-directive)
- [`t-transition` directive](#t-transition-directive)
- [`t-mounted` directive](#t-mounted-directive)
- [Debugging (`t-debug` and `t-log`)](#debugging-t-debug-and-t-log)
- [White spaces](#white-spaces)
- [Root nodes](#root-nodes)
## Overview
@@ -38,9 +43,54 @@ templates into functions that output a virtual DOM instead of a string. This is
necessary for the component system. In addition, it has a few extra directives
(see [OWL Specific Extensions](#owlspecificextensions))
## QWeb Engine
This section is about the javascript code that implements the `QWeb` specification.
Owl exports a `QWeb` class in `owl.QWeb`. To use it, it just needs to be
instantiated:
```js
const qweb = new owl.QWeb();
```
It's API is quite simple:
- **`constructor(data)`**: constructor. Takes an optional string to add initial
templates (see `addTemplates` for more information on format of the string).
```js
const qweb = new owl.QWeb(TEMPLATES);
```
- **`addTemplate(name, xmlStr)`**: add a specific template.
```js
qweb.addTemplate("mytemplate", "<div>hello</div>");
```
- **`addTemplates(xmlStr)`**: add a list of templates (identified by `t-name`
attribute).
```js
const TEMPLATES = `
<templates>
<div t-name="App" class="main">main</div>
<div t-name="OtherWidget">other widget</div>
</templates>`;
qweb.addTemplates(TEMPLATES);
```
- **`render(name, context, extra)`**: renders a template. This returns a `vnode`,
which is a virtual representation of the DOM (see [vdom doc](vdom.md)).
```js
const vnode = qweb.render("App", widget);
```
## QWeb Specification
We define in this section the specification of how `QWeb` templates should be
rendered.
### Static html nodes
@@ -196,7 +246,7 @@ compile time.
| Word | will be replaced by |
| ----- | ------------------- |
| `and` | `&&` |
| `or` | `\|\|` |
| `or` | `\|\|` |
| `gt` | `>` |
| `gte` | `>=` |
| `lt` | `<` |
@@ -208,7 +258,7 @@ compile time.
<div><p t-if="10 + 2 gt 5">ok</p></div>
```
### `t-att` directive (dynamic attributes)
### Dynamic attributes (`t-att` and `t-attf` directives)
One can use the `t-att-` directive to add dynamic attributes. Its main use is to
evaluate an expression (at rendering time) and bind an attribute to its result:
@@ -216,7 +266,7 @@ evaluate an expression (at rendering time) and bind an attribute to its result:
For example, if we have `id` set to 32 in the rendering context,
```xml
<div t-att-data-action-id="id"/> <!-- result: <div data-action-id="32"></div> -->
<div t-att-data-action-id="id"/> <!-- result: <div data-action-id="32"></div> -->
```
If an expression evaluates to a falsy value, it will not be set at all:
@@ -225,6 +275,21 @@ If an expression evaluates to a falsy value, it will not be set at all:
<div t-att-foo="false"/> <!-- result: <div></div> -->
```
There is another way to format a string attribute: the `t-attf-` directive. With
it, you get string interpolation:
```xml
<div t-attf-foo="a {{value1}} is {{value2}} of {{value3}} ]"/>
<!-- result if values are set to 1,2 and 3: <div foo="a 0 is 1 of 2 ]"></div> -->
```
For historical reason, there is an alternate form of string interpolation:
```xml
<div t-attf-foo="a #{value1} is #{value2} of #{value3} ]"/>
<!-- result if values are set to 1,2 and 3: <div foo="a 0 is 1 of 2 ]"></div> -->
```
### `t-call` directive (sub templates)
QWeb templates can be used for top level rendering, but they can also be used
@@ -242,12 +307,12 @@ templates), using the `t-call` directive:
</div>
```
will be rendered as `<div><p>owl</p></div>`. This example shows that the sub
template is rendered with the execution context of the parent. The sub template
will be rendered as `<div><p>owl</p></div>`. This example shows that the sub
template is rendered with the execution context of the parent. The sub template
is actually inlined in the main template, but in a sub scope: variables defined
in the sub template do not escape.
Sometimes, one might want to pass information to the sub template. In that case,
Sometimes, one might want to pass information to the sub template. In that case,
the content of the body of the `t-call` directive is available as a special
magic variable `0`:
@@ -273,43 +338,141 @@ will result in :
</div>
```
## JS/OWL Specific Extensions
### `t-on` directive
In a component's template, it is useful to be able to register handlers on some
elements to some specific events. This
is what makes a template _alive_. There are two different use cases.
1. Register an event handler on a DOM node
```xml
<button t-on-click="someMethod">Do something</button>
```
This will be roughly translated in javascript like this:
```js
button.addEventListener("click", widget.someMethod.bind(widget));
```
The suffix (`click` in this example) is simply the name of the actual DOM
event.
2. Register an event handler on a component. This will not capture a DOM event,
but rather a _business_ event:
```xml
<t t-widget="MyWidget" t-on-menuLoaded="someMethod"/>
```
```js
class MyWidget {
someWhere() {
const payload = ...;
this.trigger('menuLoaded', payload);
}
}
```
Here, the parent widget will receive the payload in its `someMethod` handler,
whenever the event is triggered.
The `t-on` directive also allows to prebind some arguments. For example,
```xml
<button t-on-click="someMethod(expr)">Do something</button>
```
Here, `expr` is a valid Owl expression, so it could be `true` or some variable
from the rendering context.
### Component: `t-widget`, `t-props`
The `t-widget` and the `t-props` directives are the key to a declarative component
system. They allow a template to define where and how a sub widget is created
and/or updated. For example:
```xml
<div t-name="ParentWidget">
<t t-widget="ChildWidget" t-props="{count: state.val}"/>
</div>
```
```js
class ParentWidget {
widgets = { ChildWidget };
state = { val: 4 };
}
```
Whenever the template is rendered, it will automatically create the subwidget
`ChildWidget` at the correct place. It needs to find the reference to the
actual component class in the special `widgets` key.
In this example, the child widget will receive the object `{count: 4}` in its
constructor. This will be assigned to the `props` variable, which can be accessed
on the widget (and also, in the template). Whenever the state is updated, then
the subwidget will also be updated automatically.
### `t-ref` directive
The `t-ref` directive helps a component keep reference to some inside part of it.
Like the `t-on` directive, it can work either on a DOM node, or on a component:
```xml
<div>
<div t-ref="someDiv"/>
<t t-widget="SubWidget" t-ref="someWidget"/>
</div>
```
In this example, the widget will be able to access the `div` and the component
inside the special `refs` variable:
```js
this.refs.someDiv;
this.refs.someWidget;
```
This is useful for various usecases: for example, integrating with an external
library that needs to render itself inside an actual DOM node. Or for calling
some method on a sub widget.
Note: if used on a component, the reference will be set in the `refs`
variable between `willPatch` and `patched`.
### `t-key` directive
Even though Owl tries to be as declarative as possible, some DOM state is still
locked inside the DOM: for example, the scrolling state, the current user selection,
the focused element or the state of an input. This is why we use a virtual dom
algorithm to keep the actual DOM node as much as possible. However, this is
algorithm to keep the actual DOM node as much as possible. However, this is
sometimes not enough, and we need to help Owl decide if an element is actually
the same, or is different. The `t-key` directive is used to give an identity to an element.
There are three main use cases:
- *elements in a list*:
- _elements in a list_:
```xml
<span t-foreach="todos" t-as="todo" t-key="todo.id">
<t t-esc="todo.text"/>
</span>
```
- *`t-if`/`t-else`*
- _`t-if`/`t-else`_
- *animations*: give a different identity to a component. Ex: thread id with
animations on add/remove message.
- _animations_: give a different identity to a component. Ex: thread id with
animations on add/remove message.
### `t-transition` directive
To perform useful transition effects, whenever an element appears or disappears,
it is necessary to add/remove some css style or class at some precise moment in
the lifetime of a node. Since this is not easy to do by hand, Owl `t-transition`
the lifetime of a node. Since this is not easy to do by hand, Owl `t-transition`
directive is there to help.
Whenever a node has a `t-transition` directive, with a `name` value, the following
@@ -326,6 +489,7 @@ At node creation:
ends.
At node destruction:
- the css classes `name-leave` and `name-leave-active` will be added before the
node is removed to the DOM,
- the css class `name-leave` will be removed on the next animation frame (so it
@@ -342,16 +506,36 @@ For example, a simple fade in/out effect can be done with this:
```
```css
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
.fade-enter,
.fade-leave-to {
opacity: 0;
}
```
Note: more information on animations are available [here](doc/animations.md).
### `t-mounted` directive
The `t-mounted` directive allows to register a callback to execute whenever the node
is inserted into the DOM.
```xml
<div><input t-ref="someInput" t-mounted="focusMe"/></div>
```
```js
class MyWidget extends owl.Component {
...
focusMe() {
this.refs.someInput.focus();
}
}
```
### Debugging (`t-debug` and `t-log`)
The javascript QWeb implementation provides two useful debugging directives:
@@ -362,7 +546,7 @@ The javascript QWeb implementation provides two useful debugging directives:
<t t-if="a_test">
<t t-debug="">
</t>
````
```
will stop execution if the browser dev tools are open.
@@ -375,7 +559,6 @@ will stop execution if the browser dev tools are open.
will print 42 to the console
### White spaces
White spaces in a templates are handled in a special way:
@@ -386,7 +569,7 @@ White spaces in a templates are handled in a special way:
### Root nodes
For many reasons, Owl QWeb templates should have a single root node. More
For many reasons, Owl QWeb templates should have a single root node. More
precisely, the result of a template rendering should have a single root node:
```xml
@@ -406,6 +589,6 @@ precisely, the result of a template rendering should have a single root node:
Extra root nodes will actually be ignored (even though they will be rendered
in memory).
Note: this does not apply to subtemplates (see the `t-call` directive). In that
Note: this does not apply to subtemplates (see the `t-call` directive). In that
case, they will be inlined in the main template, and can actually have many
root nodes.
root nodes.
+1
View File
@@ -8,6 +8,7 @@
- [Store](store.md)
- [Observer](observer.md)
- [Virtual DOM](vdom.md)
- [Utils](utils.md)
## Miscellaneous
- [Quick Start](quick_start.md)
+41
View File
@@ -0,0 +1,41 @@
# 🦉 Utils 🦉
Owl export a few useful utility functions, to help with common issues. Those
functions are all available in the `owl.utils` namespace.
## Content
- [`whenReady`](#whenready)
- [`loadJS`](#loadjs)
- [`loadTemplates`](#loadtemplates)
- [`escape`](#escape)
- [`debounce`](#debounce)
- [`patch` and `unpatch`](#patchandunpatch)
## `whenReady`
The function `whenReady` is useful to register some code that need to be executed
as soon as the document (page) is ready:
```js
owl.utils.whenReady(function () {
const qweb = new owl.QWeb();
const app = new App({ qweb });
app.mount(document.body);
});
```
## `loadJS`
## `loadTemplates`
## `escape`
## `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.10.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>
-3
View File
@@ -4,7 +4,6 @@ import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
template = "counter";
state = { counter: 0 };
increment() {
@@ -16,7 +15,6 @@ class Counter extends owl.Component {
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
template = "message";
widgets = { Counter };
shouldUpdate(nextProps) {
@@ -33,7 +31,6 @@ class Message extends owl.Component {
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
template = "root";
widgets = { Message };
state = { messages: [] };
+3 -3
View File
@@ -1,5 +1,5 @@
<templates>
<div t-name="root" class="main">
<div t-name="App" class="main">
<div class="left-thing">
<div>Number of msg: <t t-esc="state.messages.length"/></div>
<button t-on-click="addMessages(100)">Add 100 messages</button>
@@ -18,14 +18,14 @@
</div>
</div>
<div t-name="message" class="message">
<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">
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
+1
View File
@@ -17,6 +17,7 @@
<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>
+170 -138
View File
@@ -1,5 +1,8 @@
import { SAMPLES } from "./samples.js";
//------------------------------------------------------------------------------
// Constants, helpers, utils
//------------------------------------------------------------------------------
let owlJS;
async function owlSourceCode() {
@@ -58,82 +61,90 @@ while True:
sys.exit(0)
`;
//------------------------------------------------------------------------------
// Tabbed editor
//------------------------------------------------------------------------------
class TabbedEditor extends owl.Component {
constructor() {
super(...arguments);
this.template = "tabbed-editor";
this.state = {
currentTab: this.props.display.split("|")[0]
};
this.tabs = {
js: this.props.display.includes("js"),
xml: this.props.display.includes("xml"),
css: this.props.display.includes("css")
};
}
/**
* Make an iframe, with all the js, css and xml properly injected.
*/
function makeCodeIframe(js, css, xml, errorHandler) {
// check templates
var qweb = new owl.QWeb();
const sanitizedXML = xml.replace(/<!--[\s\S]*?-->/g, "");
mounted() {
this.editor = ace.edit(this.refs.editor);
// will throw error if there is something wrong with xml
qweb.addTemplates(sanitizedXML);
// remove this for xml/css (?)
this.editor.session.setOption("useWorker", false);
this.editor.setValue(this.props[this.state.currentTab], -1);
this.editor.setFontSize("12px");
this.editor.setTheme("ace/theme/monokai");
this.editor.session.setMode(MODES[this.state.currentTab]);
const tabSize = this.state.currentTab === "xml" ? 2 : 4;
this.editor.session.setOption("tabSize", tabSize);
this.editor.on("blur", () => {
const editorValue = this.editor.getValue();
const propsValue = this.props[this.state.currentTab];
if (editorValue !== propsValue) {
this.trigger("updateCode", {
type: this.state.currentTab,
value: editorValue
});
}
// create iframe
const iframe = document.createElement("iframe");
iframe.onload = () => {
const doc = iframe.contentDocument;
// inject js
const owlScript = doc.createElement("script");
owlScript.type = "text/javascript";
owlScript.src = "../owl.js";
owlScript.addEventListener("load", () => {
const script = doc.createElement("script");
script.type = "text/javascript";
const content = `window.TEMPLATES = \`${sanitizedXML}\`\n${js}`;
script.innerHTML = content;
iframe.contentWindow.addEventListener("error", errorHandler);
iframe.contentWindow.addEventListener("unhandledrejection", errorHandler);
setTimeout(function() {
if (iframe.contentWindow) {
iframe.contentWindow.removeEventListener("error", errorHandler);
iframe.contentWindow.removeEventListener(
"unhandledrejection",
errorHandler
);
}
}, 200);
doc.body.appendChild(script);
});
doc.head.appendChild(owlScript);
// inject css
const style = document.createElement("style");
style.innerHTML = css;
doc.head.appendChild(style);
};
return iframe;
}
/**
* Make a zip file containing a functioning application
*/
async function makeApp(js, css, xml) {
await owl.utils.loadJS("libs/jszip.min.js");
const zip = new JSZip();
const processedJS = js
.split("\n")
.map(l => (l === "" ? "" : " " + l))
.join("\n");
const JS = `async function startApp() {
// Loading templates
let TEMPLATES;
try {
TEMPLATES = await owl.utils.loadTemplates('app.xml');
} catch(e) {
document.write(\`This app requires a static server. If you have python installed, try 'python app.py'\`);
return;
}
patched() {
if (this.editor) {
window.dispatchEvent(new Event("resize"));
this.editor.setValue(this.props[this.state.currentTab], -1);
}
}
// Application code
${processedJS}
}
willUnmount() {
this.editor.destroy();
delete this.editor;
}
// wait for DOM ready before starting
owl.utils.whenReady(startApp);`;
setTab(tab) {
this.editor.setValue(this.props[tab], -1);
const mode = MODES[tab];
this.editor.session.setMode(mode);
const tabSize = tab === "xml" ? 2 : 4;
this.editor.session.setOption("tabSize", tabSize);
this.state.currentTab = tab;
}
onMouseDown(ev) {
if (ev.target.tagName === "DIV") {
let y = ev.clientY;
const resizer = ev => {
const delta = ev.clientY - y;
y = ev.clientY;
this.trigger("updatePanelHeight", { delta });
};
document.body.addEventListener("mousemove", resizer);
document.body.addEventListener("mouseup", () => {
document.body.removeEventListener("mousemove", resizer);
});
}
}
zip.file("app.js", JS);
zip.file("app.css", css);
zip.file("app.py", APP_PY);
zip.file("app.xml", xml);
zip.file("index.html", DEFAULT_HTML);
zip.file("owl.js", owlSourceCode());
return zip.generateAsync({ type: "blob" });
}
//------------------------------------------------------------------------------
@@ -142,7 +153,6 @@ class TabbedEditor extends owl.Component {
class App extends owl.Component {
constructor(...args) {
super(...args);
this.template = "playground";
this.version = owl._version;
this.SAMPLES = SAMPLES;
this.widgets = { TabbedEditor };
@@ -157,6 +167,10 @@ class App extends owl.Component {
leftPaneWidth: Math.ceil(window.innerWidth / 2),
topPanelHeight: null
};
this.toggleLayout = owl.utils.debounce(this.toggleLayout, 250, true);
this.runCode = owl.utils.debounce(this.runCode, 250, true);
this.downloadCode = owl.utils.debounce(this.downloadCode, 250, true);
}
displayError(error) {
@@ -169,61 +183,26 @@ class App extends owl.Component {
}
}
async runCode() {
runCode() {
this.state.displayWelcome = false;
// check templates
var qweb = new owl.QWeb();
var error = false;
const sanitizedXML = this.state.xml.replace(/<!--[\s\S]*?-->/g, "");
let subiframe;
let error = false;
const errorHandler = e => this.displayError(e.message || e.reason.message);
try {
qweb.loadTemplates(sanitizedXML);
const { js, css, xml } = this.state;
subiframe = makeCodeIframe(js, css, xml, errorHandler);
} catch (e) {
//probably problem with the templates
error = e;
}
if (error) {
this.displayError(error.message);
return;
} else {
this.state.error = false;
}
// create iframe
const iframe = document.createElement("iframe");
iframe.onload = () => {
const doc = iframe.contentDocument;
// inject js
const owlScript = doc.createElement("script");
owlScript.type = "text/javascript";
owlScript.src = "../owl.js";
owlScript.addEventListener("load", () => {
const script = doc.createElement("script");
script.type = "text/javascript";
const content = `window.TEMPLATES = \`${sanitizedXML}\`\n${
this.state.js
}`;
script.innerHTML = content;
const errorHandler = e => this.displayError(e.message || e.reason.message);
iframe.contentWindow.addEventListener("error", errorHandler);
iframe.contentWindow.addEventListener("unhandledrejection", errorHandler);
setTimeout(function() {
if (iframe.contentWindow) {
iframe.contentWindow.removeEventListener("error", errorHandler);
iframe.contentWindow.removeEventListener("unhandledrejection", errorHandler);
}
}, 200);
doc.body.appendChild(script);
});
doc.head.appendChild(owlScript);
// inject css
const style = document.createElement("style");
style.innerHTML = this.state.css;
doc.head.appendChild(style);
};
this.refs.content.innerHTML = "";
this.refs.content.appendChild(iframe);
this.refs.content.appendChild(subiframe);
}
setSample(ev) {
@@ -280,38 +259,91 @@ class App extends owl.Component {
}
async downloadCode() {
const { js, css, xml } = this.state;
const content = await makeApp(js, css, xml);
await owl.utils.loadJS("libs/FileSaver.min.js");
await owl.utils.loadJS("libs/jszip.min.js");
const zip = new JSZip();
const JS = `async function startApp() {
// Loading templates
let TEMPLATES;
try {
TEMPLATES = await owl.utils.loadTemplates('app.xml');
} catch(e) {
document.write(\`This app requires a static server. If you have python installed, try 'python app.py'\`);
return;
saveAs(content, "app.zip");
}
// Application code
${this.state.js.split('\n').map(l => l === '' ? '' : ' ' + l).join('\n')}
}
// wait for DOM ready before starting
owl.utils.whenReady(startApp);`;
//------------------------------------------------------------------------------
// Tabbed editor
//------------------------------------------------------------------------------
class TabbedEditor extends owl.Component {
constructor(parent, props) {
super(parent, props);
this.state = {
currentTab: props.js ? "js" : props.xml ? "xml" : "css"
};
this.setTab = owl.utils.debounce(this.setTab, 250, true);
zip.file("app.js", JS);
zip.file("app.css", this.state.css);
zip.file("app.py", APP_PY);
zip.file("app.xml", this.state.xml);
zip.file("index.html", DEFAULT_HTML);
zip.file("owl.js", owlSourceCode());
zip.generateAsync({ type: "blob" }).then(function(content) {
saveAs(content, "app.zip");
this.sessions = {};
for (let tab of ["js", "xml", "css"]) {
if (props[tab]) {
this.sessions[tab] = new ace.EditSession(props[tab], MODES[tab]);
this.sessions[tab].setOption("useWorker", false);
const tabSize = tab === "xml" ? 2 : 4;
this.sessions[tab].setOption("tabSize", tabSize);
this.sessions[tab].setUndoManager(new ace.UndoManager());
}
}
}
mounted() {
this.editor = ace.edit(this.refs.editor);
this.editor.setValue(this.props[this.state.currentTab], -1);
this.editor.setFontSize("12px");
this.editor.setTheme("ace/theme/monokai");
this.editor.setSession(this.sessions[this.state.currentTab]);
const tabSize = this.state.currentTab === "xml" ? 2 : 4;
this.editor.session.setOption("tabSize", tabSize);
this.editor.on("blur", () => {
const editorValue = this.editor.getValue();
const propsValue = this.props[this.state.currentTab];
if (editorValue !== propsValue) {
this.trigger("updateCode", {
type: this.state.currentTab,
value: editorValue
});
}
});
}
patched() {
const session = this.sessions[this.state.currentTab];
session.setValue(this.props[this.state.currentTab], -1);
this.editor.setSession(session);
}
willUnmount() {
this.editor.destroy();
delete this.editor;
}
setTab(tab) {
if (this.state.currentTab !== tab) {
this.state.currentTab = tab;
const session = this.sessions[this.state.currentTab];
session.doc.setValue(this.props[tab], -1);
this.editor.setSession(session);
}
}
onMouseDown(ev) {
if (ev.target.tagName === "DIV") {
let y = ev.clientY;
const resizer = ev => {
const delta = ev.clientY - y;
y = ev.clientY;
this.trigger("updatePanelHeight", { delta });
};
document.body.addEventListener("mousemove", resizer);
document.body.addEventListener("mouseup", () => {
document.body.removeEventListener("mousemove", resizer);
});
}
}
}
//------------------------------------------------------------------------------
+2 -1
View File
@@ -22,7 +22,7 @@ body {
display: grid;
height: 100%;
width: 100%;
grid-template-columns: auto 6px 100%;
grid-template-columns: auto 8px 100%;
}
/* LEFT BAR ****************************************/
@@ -115,6 +115,7 @@ body {
width: 34px;
text-align: center;
border-bottom: 2px solid transparent;
text-transform: uppercase;
}
.tab.active {
+42 -67
View File
@@ -1,7 +1,6 @@
const CLICK_COUNTER = `class ClickCounter extends owl.Component {
constructor() {
super(...arguments);
this.template = "clickcounter";
this.state = { value: 0 };
}
@@ -16,7 +15,7 @@ counter.mount(document.body);
`;
const CLICK_COUNTER_XML = `<templates>
<button t-name="clickcounter" t-on-click="increment">
<button t-name="ClickCounter" t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>
</templates>`;
@@ -29,7 +28,6 @@ const CLICK_COUNTER_CSS = `button {
const CLICK_COUNTER_ESNEXT = `// This example will not work if your browser does not support ESNext class fields
class ClickCounter extends owl.Component {
template = "clickcounter";
state = { value: 0 };
increment() {
@@ -45,7 +43,6 @@ counter.mount(document.body);
const WIDGET_COMPOSITION = `class ClickCounter extends owl.Component {
constructor(parent, props) {
super(parent, props);
this.template = "clickcounter";
this.state = { value: props.initialState || 0 };
}
@@ -59,7 +56,6 @@ let nextId = 1;
class App extends owl.Component {
constructor() {
super(...arguments);
this.template = "app";
this.state = { counters: [] }
this.widgets = { ClickCounter };
}
@@ -75,11 +71,11 @@ app.mount(document.body);
`;
const WIDGET_COMPOSITION_XML = `<templates>
<button t-name="clickcounter" t-on-click="increment">
<button t-name="ClickCounter" t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>
<div t-name="app">
<div t-name="App">
<div><button t-on-click="addCounter">Add a counter</button></div>
<div>
<t t-foreach="state.counters" t-as="counter">
@@ -97,7 +93,6 @@ const WIDGET_COMPOSITION_CSS = `button {
const ANIMATION = `// This example will not work if your browser does not support ESNext class fields
class App extends owl.Component {
template = "app";
state = {flag: 0};
toggle() {
@@ -111,7 +106,7 @@ app.mount(document.body);
`;
const ANIMATION_XML = `<templates>
<div t-name="app">
<div t-name="App">
<button t-on-click="toggle">
Click Me!
</button>
@@ -152,7 +147,6 @@ const ANIMATION_CSS = `button {
const LIFECYCLE_DEMO = `class HookWidget extends owl.Component {
constructor() {
super(...arguments);
this.template = "demo.hookwidget";
this.state = { n: 0 };
console.log("constructor");
}
@@ -183,7 +177,6 @@ class ParentWidget extends owl.Component {
constructor() {
super(...arguments);
this.widgets = { HookWidget };
this.template = "demo.parentwidget";
this.state = { n: 0, flag: true };
}
increment() {
@@ -200,14 +193,14 @@ widget.mount(document.body);
`;
const LIFECYCLE_DEMO_XML = `<templates>
<div t-name="demo.parentwidget">
<div t-name="ParentWidget">
<button t-on-click="increment">Increment</button>
<button t-on-click="toggleSubWidget">ToggleSubWidget</button>
<div t-if="state.flag">
<t t-widget="HookWidget" t-props="{n:state.n}"/>
</div>
</div>
<div t-name="demo.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>
</div>
<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 = `//------------------------------------------------------------------------------
@@ -241,7 +234,6 @@ for (let i = 1; i < 16000; i++) {
class Counter extends owl.Component {
constructor(parent, props) {
super(parent, props);
this.template = "counter";
this.state = { counter: props.initialState || 0 };
}
@@ -256,7 +248,6 @@ class Counter extends owl.Component {
class Message extends owl.Component {
constructor() {
super(...arguments);
this.template = "message";
this.widgets = { Counter };
}
@@ -273,7 +264,6 @@ class Message extends owl.Component {
class App extends owl.Component {
constructor() {
super(...arguments);
this.template = "root";
this.widgets = { Message };
this.state = { messages: messages.slice(0, 10) };
}
@@ -351,7 +341,7 @@ const BENCHMARK_APP_CSS = `.main {
}`;
const BENCHMARK_APP_XML = `<templates>
<div t-name="root" class="main">
<div t-name="App" class="main">
<div class="left-thing">
<div class="counter">
<button t-on-click="increment(-1)">-</button>
@@ -374,14 +364,14 @@ const BENCHMARK_APP_XML = `<templates>
</div>
</div>
<div t-name="message" class="message">
<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">
<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>
@@ -477,7 +467,6 @@ function makeStore() {
// TodoItem
//------------------------------------------------------------------------------
class TodoItem extends owl.Component {
template = "todoitem";
state = { isEditing: false };
removeTodo() {
@@ -490,11 +479,12 @@ class TodoItem extends owl.Component {
async editTodo() {
this.state.isEditing = true;
setTimeout(() => {
this.refs.input.value = "";
this.refs.input.focus();
this.refs.input.value = this.props.title;
});
}
focusInput() {
this.refs.input.value = "";
this.refs.input.focus();
this.refs.input.value = this.props.title;
}
handleKeyup(ev) {
@@ -535,7 +525,6 @@ function mapStateToProps(state) {
}
class TodoApp extends owl.Component {
template = "todoapp";
widgets = { TodoItem };
state = { filter: "all" };
@@ -602,7 +591,7 @@ app.mount(document.body);
`;
const TODO_APP_STORE_XML = `<templates>
<section t-name="todoapp" class="todoapp">
<section t-name="TodoApp" class="todoapp">
<header class="header">
<h1>todos</h1>
<input class="new-todo" autofocus="true" autocomplete="off" placeholder="What needs to be done?" t-on-keyup="addTodo"/>
@@ -640,7 +629,7 @@ const TODO_APP_STORE_XML = `<templates>
</footer>
</section>
<li t-name="todoitem" class="todo" t-att-class="{completed: props.completed, editing: state.isEditing}">
<li t-name="TodoItem" class="todo" t-att-class="{completed: props.completed, editing: state.isEditing}">
<div class="view">
<input class="toggle" type="checkbox" t-on-change="toggleTodo" t-att-checked="props.completed"/>
<label t-on-dblclick="editTodo">
@@ -648,7 +637,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-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>`;
@@ -1034,39 +1023,25 @@ html .clear-completed:active {
}
`;
const RESPONSIVE = `class Navbar extends owl.Component {
template="navbar";
}
const RESPONSIVE = `class Navbar extends owl.Component {}
class ControlPanel extends owl.Component {
template="controlpanel";
widgets = { MobileSearchView };
}
class FormView extends owl.Component {
template="formview";
widgets = { AdvancedWidget };
}
class AdvancedWidget extends owl.Component {
template="advancedwidget";
}
class AdvancedWidget extends owl.Component {}
class Chatter extends owl.Component {
template="chatter";
}
class Chatter extends owl.Component {}
class MobileSearchView extends owl.Component {
template="mobilesearchview";
}
class MobileSearchView extends owl.Component {}
class App extends owl.Component {
constructor() {
super(...arguments);
this.template = "app";
this.widgets = { Navbar, ControlPanel, FormView, Chatter };
}
widgets = { Navbar, ControlPanel, FormView, Chatter };
}
function isMobile() {
@@ -1081,39 +1056,39 @@ const env = {
const app = new App(env);
app.mount(document.body);
window.addEventListener(
"resize",
owl.utils.debounce(function() {
const _isMobile = isMobile();
if (_isMobile !== env.isMobile) {
app.updateEnv({
isMobile: _isMobile
});
}
}, 20)
);`;
function updateEnv() {
const _isMobile = isMobile();
if (_isMobile !== env.isMobile) {
app.updateEnv({
isMobile: _isMobile
});
}
}
window.addEventListener("resize", owl.utils.debounce(updateEnv, 20));
`;
const RESPONSIVE_XML = `<templates>
<div t-name="navbar" class="navbar">Navbar</div>
<div t-name="Navbar" class="navbar">Navbar</div>
<div t-name="controlpanel" class="controlpanel">
<div t-name="ControlPanel" class="controlpanel">
<h2>controlpanel</h2>
<t t-if="env.isMobile" t-widget="MobileSearchView"/>
</div>
<div t-name="formview" class="formview">
<div t-name="FormView" class="formview">
<h2>formview</h2>
<t t-if="!env.isMobile" t-widget="AdvancedWidget"/>
</div>
<div t-name="chatter" class="chatter">
<div t-name="Chatter" class="chatter">
<h2>Chatter</h2>
<t t-foreach="100" t-as="item"><div>Message <t t-esc="item"/></div></t>
</div>
<div t-name="mobilesearchview">MOBILE searchview</div>
<div t-name="MobileSearchView">MOBILE searchview</div>
<div t-name="app" class="app" t-att-class="{mobile: env.isMobile, desktop: !env.isMobile}">
<div t-name="App" class="app" t-att-class="{mobile: env.isMobile, desktop: !env.isMobile}">
<t t-widget="Navbar"/>
<t t-widget="ControlPanel"/>
<div class="content-wrapper" t-if="!env.isMobile">
@@ -1128,7 +1103,7 @@ const RESPONSIVE_XML = `<templates>
</t>
</div>
<div t-name="advancedwidget">
<div t-name="AdvancedWidget">
This widget is only created in desktop mode.
<button>Button!</button>
</div>
+18 -10
View File
@@ -1,14 +1,16 @@
<templates>
<div t-name="tabbed-editor" class="tabbed-editor" t-att-style="props.style">
<div t-name="TabbedEditor" class="tabbed-editor">
<div class="tabBar" t-att-class="{resizeable: props.resizeable}" t-on-mousedown="onMouseDown">
<a t-if="tabs.js" class="tab flash" t-att-class="{active: state.currentTab==='js'}" t-on-click="setTab('js')">JS</a>
<a t-if="tabs.xml" class="tab flash" t-att-class="{active: state.currentTab==='xml'}" t-on-click="setTab('xml')">XML</a>
<a t-if="tabs.css" class="tab flash" t-att-class="{active: state.currentTab==='css'}" t-on-click="setTab('css')">CSS</a>
<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)">
<t t-esc="tab"/>
</a>
</t>
</div>
<div class="code-editor" t-ref="'editor'"></div>
</div>
<div t-name="playground" class="playground">
<div t-name="App" class="playground">
<div class="left-bar" t-att-style="leftPaneStyle" t-att-class="{split: state.splitLayout}">
<div class="menubar">
<a class="btn run-code flash" t-on-click="runCode" title="Execute this Code">▶ Run</a>
@@ -20,13 +22,19 @@
<a class="btn flash" t-on-click="downloadCode" title="Download a Zip with this Code"><i class="fas fa-download"></i></a>
<a class="layout-selector flash" t-on-click="toggleLayout" title="Toggle Layout"><i class="fas" t-att-class="state.splitLayout ? 'fa-toggle-on' : 'fa-toggle-off'"></i></a>
</div>
<t t-if="!state.splitLayout">
<t t-widget="TabbedEditor" t-props="{js:state.js, css:state.css, xml: state.xml, display: 'js|xml|css'}" t-on-updateCode="updateCode"/>
<t t-if="state.splitLayout">
<t t-widget="TabbedEditor"
t-props="{js:state.js, css:false, xml: false}"
t-on-updateCode="updateCode"
t-att-style="topEditorStyle"/>
<div class="separator horizontal"/>
<t t-widget="TabbedEditor" t-keepalive="1"
t-props="{js:false, css:state.css, xml: state.xml, resizeable: true}"
t-on-updateCode="updateCode"
t-on-updatePanelHeight="updatePanelHeight"/>
</t>
<t t-else="1">
<t t-widget="TabbedEditor" t-props="{js:state.js, css:state.css, xml: state.xml, display: 'js', style:topEditorStyle}" t-on-updateCode="updateCode"/>
<div class="separator horizontal"/>
<t t-widget="TabbedEditor" t-keepalive="1" t-props="{js:state.js, css:state.css, xml: state.xml, display: 'xml|css', resizeable: true}" t-on-updateCode="updateCode" t-on-updatePanelHeight="updatePanelHeight"/>
<t t-widget="TabbedEditor" t-props="{js:state.js, css:state.css, xml: state.xml, display: 'js|xml|css'}" t-on-updateCode="updateCode"/>
</t>
</div>
<div class="separator vertical" t-on-mousedown="onMouseDown"/>
+1 -1
View File
@@ -7,7 +7,7 @@ To help work/improve/learn with OWL, we have here:
Both of them can be accessed by using a static http server. A simple python
server is available in `server.py`. There is also a npm script to start it:
`npm run extras`.
`npm run extras` (and its version with a watcher: `npm run extras:watch`).
## Benchmarks
+4 -6
View File
@@ -1,20 +1,18 @@
{
"name": "owl",
"version": "0.10.0",
"version": "0.11.0",
"description": "Odoo Web Library (OWL)",
"main": "src/index.ts",
"scripts": {
"build:js": "tsc --target esnext --module es6 --outDir dist/owl src/*",
"build:js:es5": "tsc --target es5 --module es6 --outDir dist/owl src/*",
"build:bundle": "rollup -c",
"build": "npm run build:js && npm run build:bundle",
"build:es5": "npm run build:js:es5 && npm run build:bundle",
"minify": "uglifyjs dist/owl.js -o dist/owl.min.js --compress --mangle",
"dev": "npm-run-all --parallel \"build:* -- --watch\"",
"test": "jest",
"test:watch": "jest --watch",
"preextras": "npm run build",
"extras": "python extras/server.py"
"extras:serve": "python extras/server.py",
"extras": "npm run build && npm run extras:serve",
"extras:watch": "npm-run-all --parallel extras:serve \"build:* -- --watch\""
},
"repository": {
"type": "git",
+43 -14
View File
@@ -25,7 +25,6 @@ export interface Env {
export interface Meta<T extends Env, Props> {
readonly id: number;
vnode: VNode | null;
isStarted: boolean;
isMounted: boolean;
isDestroyed: boolean;
parent: Component<T, any, any> | null;
@@ -40,8 +39,15 @@ export interface Meta<T extends Env, Props> {
boundHandlers: { [key: number]: any };
observer?: Observer;
render?: CompiledTemplate;
mountedHandlers: { [key: number]: Function };
}
// If a component does not define explicitely a template
// key, it needs to find a template with its name (or a parent's). This is
// qweb dependant, so we need a place to store this information indexed by
// qweb instances.
const TEMPLATE_MAP: { [key: number]: { [name: string]: string } } = {};
//------------------------------------------------------------------------------
// Widget
//------------------------------------------------------------------------------
@@ -53,8 +59,7 @@ export class Component<
State extends {}
> extends EventBus {
readonly __owl__: Meta<Env, Props>;
template: string = "default";
inlineTemplate: string | null = null;
template?: string;
get el(): HTMLElement | null {
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
@@ -110,7 +115,6 @@ export class Component<
this.__owl__ = {
id: id,
vnode: null,
isStarted: false,
isMounted: false,
isDestroyed: false,
parent: p,
@@ -119,7 +123,8 @@ export class Component<
renderId: 1,
renderPromise: null,
renderProps: props || null,
boundHandlers: {}
boundHandlers: {},
mountedHandlers: {}
};
}
@@ -226,6 +231,9 @@ export class Component<
}
}
this.__owl__.isMounted = true;
for (let key in this.__owl__.mountedHandlers) {
this.__owl__.mountedHandlers[key]();
}
this.mounted();
}
@@ -373,14 +381,35 @@ export class Component<
if (this.__owl__.isDestroyed) {
return Promise.resolve(h("div"));
}
this.__owl__.isStarted = true;
if (this.inlineTemplate) {
this.env.qweb.addTemplate(this.inlineTemplate, this.inlineTemplate, true);
const qweb = this.env.qweb;
if (!this.template) {
let tmap = TEMPLATE_MAP[qweb.id];
if (!tmap) {
tmap = {};
TEMPLATE_MAP[qweb.id] = tmap;
}
let p = (<any>this).constructor;
let name: string = p.name;
let template = tmap[name];
if (template) {
this.template = template;
} else {
while (
(template = p.name) &&
!(template in qweb.templates) &&
p !== Component
) {
p = p.__proto__;
}
if (p === Component) {
this.template = "default";
} else {
tmap[name] = template;
this.template = template;
}
}
}
this.__owl__.render = this.env.qweb.render.bind(
this.env.qweb,
this.inlineTemplate || this.template
);
this.__owl__.render = qweb.render.bind(qweb, this.template);
this._observeState();
return this._render();
}
@@ -400,6 +429,7 @@ export class Component<
let vnode = this.__owl__.render!(this, {
promises,
handlers: this.__owl__.boundHandlers,
mountedHandlers: this.__owl__.mountedHandlers,
forceUpdate: force,
patchQueue
});
@@ -425,8 +455,7 @@ export class Component<
_mount(vnode: VNode, elm: HTMLElement): VNode {
this.__owl__.vnode = patch(elm, vnode);
if (
this.__owl__.parent &&
this.__owl__.parent.__owl__.isMounted &&
this.__owl__.parent!.__owl__.isMounted &&
!this.__owl__.isMounted
) {
this._callMounted();
+5 -3
View File
@@ -39,6 +39,7 @@ for (let method of methodsToPatch) {
if (!this.__observer__.allowMutations) {
throw new Error(`Array cannot be changed here")`);
}
this.__observer__.rev++;
this.__observer__.notifyChange();
this.__owl__.rev++;
let parent = this;
@@ -74,7 +75,6 @@ export class Observer {
notifyCB() {}
notifyChange() {
this.rev++;
this.dirty = true;
Promise.resolve().then(() => {
if (this.dirty) {
@@ -105,6 +105,7 @@ export class Observer {
}
set(target: any, key: number | string, value: any) {
this.rev++;
this._addProp(target, key, value);
target.__owl__.rev++;
this.notifyChange();
@@ -112,7 +113,7 @@ export class Observer {
_observeObj<T extends { __owl__?: any }>(obj: T, parent?: any) {
const keys = Object.keys(obj);
obj.__owl__ = { rev: 1, deepRev: 1, parent };
obj.__owl__ = { rev: this.rev, deepRev: this.rev, parent };
Object.defineProperty(obj, "__owl__", { enumerable: false });
for (let key of keys) {
this._addProp(obj, key, obj[key]);
@@ -120,7 +121,7 @@ export class Observer {
}
_observeArr(arr: Array<any>, parent?: any) {
(<any>arr).__owl__ = { rev: 1, deepRev: 1, parent };
(<any>arr).__owl__ = { rev: this.rev, deepRev: this.rev, parent };
Object.defineProperty(arr, "__owl__", { enumerable: false });
(<any>arr).__proto__ = Object.create(ModifiedArrayProto);
(<any>arr).__proto__.__observer__ = this;
@@ -142,6 +143,7 @@ export class Observer {
},
set(newVal) {
if (newVal !== value) {
self.rev++;
if (!self.allowMutations) {
throw new Error(
`Observed state cannot be changed here! (key: "${key}", val: "${newVal}")`
+492 -452
View File
@@ -1,5 +1,4 @@
import { VNode, h } from "./vdom";
import { parseXML } from "./utils";
/**
* Owl QWeb Engine
@@ -31,7 +30,6 @@ interface Template {
}
interface CompilationInfo {
nodeID?: number;
node: Element;
qweb: QWeb;
ctx: Context;
@@ -39,6 +37,11 @@ interface CompilationInfo {
value: string;
}
interface NodeCreationCompilationInfo extends CompilationInfo {
nodeID: number;
addNodeHook: Function;
}
export interface Directive {
name: string;
extraNames?: string[];
@@ -46,7 +49,7 @@ export interface Directive {
// if return true, then directive is fully applied and there is no need to
// keep processing node. Otherwise, we keep going.
atNodeEncounter?(info: CompilationInfo): boolean | void;
atNodeCreation?(info: CompilationInfo): void;
atNodeCreation?(info: NodeCreationCompilationInfo): void;
finalize?(info: CompilationInfo): void;
}
@@ -87,6 +90,12 @@ const DIRECTIVE_NAMES = {
const DIRECTIVES: Directive[] = [];
const NODE_HOOKS_PARAMS = {
create: "(_, n)",
insert: "vn",
remove: "(vn, rm)"
};
export const UTILS = {
h: h,
objectToAttrString(obj: Object): string {
@@ -100,6 +109,486 @@ export const UTILS = {
}
};
function parseXML(xml: string): Document {
const parser = new DOMParser();
const doc = parser.parseFromString(xml, "text/xml");
if (doc.getElementsByTagName("parsererror").length) {
throw new Error("Invalid XML in template");
}
return doc;
}
let nextID = 1;
//------------------------------------------------------------------------------
// QWeb rendering engine
//------------------------------------------------------------------------------
export class QWeb {
templates: { [name: string]: Template } = {};
utils = UTILS;
// the id field is useful to be able to hash qweb instances. The current
// use case is that component's templates are qweb dependant, and need to be
// able to map a qweb instance to a template name.
id = nextID++;
constructor(data?: string) {
if (data) {
this.addTemplates(data);
}
this.addTemplate("default", "<div></div>");
}
static addDirective(directive: Directive) {
DIRECTIVES.push(directive);
DIRECTIVE_NAMES[directive.name] = 1;
DIRECTIVES.sort((d1, d2) => d1.priority - d2.priority);
if (directive.extraNames) {
directive.extraNames.forEach(n => (DIRECTIVE_NAMES[n] = 1));
}
}
/**
* Add a template to the internal template map. Note that it is not
* immediately compiled.
*/
addTemplate(name: string, xmlString: string) {
const doc = parseXML(xmlString);
if (!doc.firstChild) {
throw new Error("Invalid template (should not be empty)");
}
this._addTemplate(name, <Element>doc.firstChild);
}
/**
* Load templates from a xml (as a string). This will look up for the first
* <templates> tag, and will consider each child of this as a template, with
* the name given by the t-name attribute.
*/
addTemplates(xmlstr: string) {
const doc = parseXML(xmlstr);
const templates = doc.getElementsByTagName("templates")[0];
if (!templates) {
return;
}
for (let elem of <any>templates.children) {
const name = elem.getAttribute("t-name");
this._addTemplate(name, elem);
}
}
_addTemplate(name: string, elem: Element) {
if (name in this.templates) {
throw new Error(`Template ${name} already defined`);
}
this._processTemplate(elem);
const template = {
elem,
fn: (context, extra) => {
const compiledFunction = this._compile(name, elem);
template.fn = compiledFunction;
return compiledFunction.call(this, context, extra);
}
};
this.templates[name] = template;
}
_processTemplate(elem: Element) {
let tbranch = elem.querySelectorAll("[t-elif], [t-else]");
for (let i = 0, ilen = tbranch.length; i < ilen; i++) {
let node = tbranch[i];
let prevElem = node.previousElementSibling!;
let pattr = function(name) {
return prevElem.getAttribute(name);
};
let nattr = function(name) {
return +!!node.getAttribute(name);
};
if (prevElem && (pattr("t-if") || pattr("t-elif"))) {
if (pattr("t-foreach")) {
throw new Error(
"t-if cannot stay at the same level as t-foreach when using t-elif or t-else"
);
}
if (
["t-if", "t-elif", "t-else"].map(nattr).reduce(function(a, b) {
return a + b;
}) > 1
) {
throw new Error(
"Only one conditional branching directive is allowed per node"
);
}
// All text nodes between branch nodes are removed
let textNode;
while ((textNode = node.previousSibling) !== prevElem) {
if (textNode.nodeValue.trim().length) {
throw new Error("text is not allowed between branching directives");
}
textNode.remove();
}
} else {
throw new Error(
"t-elif and t-else directives must be preceded by a t-if or t-elif directive"
);
}
}
}
/**
* Render a template
*
* @param {string} name the template should already have been added
*/
render(name: string, context: EvalContext = {}, extra: any = null): VNode {
const template = this.templates[name];
if (!template) {
throw new Error(`Template ${name} does not exist`);
}
return template.fn.call(this, context, extra);
}
_compile(name: string, elem: Element): CompiledTemplate {
const isDebug = elem.attributes.hasOwnProperty("t-debug");
const ctx = new Context(name);
this._compileNode(elem, ctx);
if (ctx.shouldProtectContext) {
ctx.code.unshift(" context = Object.create(context);");
}
if (ctx.shouldDefineOwner) {
// this is necessary to prevent some directives (t-forach for ex) to
// pollute the rendering context by adding some keys in it.
ctx.code.unshift(" let owner = context;");
}
if (!ctx.rootNode) {
throw new Error("A template should have one root node");
}
ctx.addLine(`return vn${ctx.rootNode};`);
let template;
try {
template = new Function(
"context",
"extra",
ctx.code.join("\n")
) as CompiledTemplate;
} catch (e) {
throw new Error(
`Invalid generated code while compiling template '${ctx.templateName.replace(
/`/g,
"'"
)}': ${e.message}`
);
}
if (isDebug) {
console.log(
`Template: ${this.templates[name].elem.outerHTML}\nCompiled code:\n` +
template.toString()
);
}
return template;
}
/**
* Generate code from an xml node
*
*/
_compileNode(node: ChildNode, ctx: Context) {
if (!(node instanceof Element)) {
// this is a text node, there are no directive to apply
let text = node.textContent!;
if (!ctx.inPreTag) {
if (lineBreakRE.test(text) && !text.trim()) {
return;
}
text = text.replace(whitespaceRE, " ");
}
if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`);
} else {
// this is an unusual situation: this text node is the result of the
// template rendering.
let nodeID = ctx.generateID();
ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`);
ctx.rootContext.rootNode = nodeID;
ctx.rootContext.parentNode = nodeID;
}
return;
}
const attributes = (<Element>node).attributes;
const validDirectives: {
directive: Directive;
value: string;
fullName: string;
}[] = [];
let withHandlers = false;
// maybe this is not optimal: we iterate on all attributes here, and again
// just after for each directive.
for (let i = 0; i < attributes.length; i++) {
let attrName = attributes[i].name;
if (attrName.startsWith("t-")) {
let dName = attrName.slice(2).split("-")[0];
if (!(dName in DIRECTIVE_NAMES)) {
throw new Error(`Unknown QWeb directive: '${attrName}'`);
}
}
}
for (let directive of DIRECTIVES) {
let fullName;
let value;
for (let i = 0; i < attributes.length; i++) {
const name = attributes[i].name;
if (
name === "t-" + directive.name ||
name.startsWith("t-" + directive.name + "-")
) {
fullName = name;
value = attributes[i].textContent;
validDirectives.push({ directive, value, fullName });
if (directive.name === "on") {
withHandlers = true;
}
}
}
}
for (let { directive, value, fullName } of validDirectives) {
if (directive.atNodeEncounter) {
const isDone = directive.atNodeEncounter({
node,
qweb: this,
ctx,
fullName,
value
});
if (isDone) {
return;
}
}
}
if (node.nodeName !== "t") {
let nodeID = this._compileGenericNode(node, ctx, withHandlers);
ctx = ctx.withParent(nodeID);
let nodeHooks = {};
let addNodeHook = function(hook, handler) {
nodeHooks[hook] = nodeHooks[hook] || [];
nodeHooks[hook].push(handler);
};
for (let { directive, value, fullName } of validDirectives) {
if (directive.atNodeCreation) {
directive.atNodeCreation({
node,
qweb: this,
ctx,
fullName,
value,
nodeID,
addNodeHook
});
}
}
if (Object.keys(nodeHooks).length) {
ctx.addLine(`p${nodeID}.hook = {`);
for (let hook in nodeHooks) {
ctx.addLine(` ${hook}: ${NODE_HOOKS_PARAMS[hook]} => {`);
for (let handler of nodeHooks[hook]) {
ctx.addLine(` ${handler}`);
}
ctx.addLine(` },`);
}
ctx.addLine(`};`);
}
}
if (node.nodeName === "pre") {
ctx = ctx.subContext("inPreTag", true);
}
this._compileChildren(node, ctx);
for (let { directive, value, fullName } of validDirectives) {
if (directive.finalize) {
directive.finalize({ node, qweb: this, ctx, fullName, value });
}
}
}
_compileGenericNode(
node: ChildNode,
ctx: Context,
withHandlers: boolean = true
): number {
// nodeType 1 is generic tag
if (node.nodeType !== 1) {
throw new Error("unsupported node type");
}
const attributes = (<Element>node).attributes;
const attrs: string[] = [];
const props: string[] = [];
const tattrs: number[] = [];
function handleBooleanProps(key, val) {
let isProp = false;
if (node.nodeName === "input" && key === "checked") {
let type = (<Element>node).getAttribute("type");
if (type === "checkbox" || type === "radio") {
isProp = true;
}
}
if (node.nodeName === "option" && key === "selected") {
isProp = true;
}
if (key === "disabled" && DISABLED_TAGS.indexOf(node.nodeName) > -1) {
isProp = true;
}
if (
(key === "readonly" && node.nodeName === "input") ||
node.nodeName === "textarea"
) {
isProp = true;
}
if (isProp) {
props.push(`${key}: _${val}`);
}
}
function formatter(expr) {
return "${" + ctx.formatExpression(expr) + "}";
}
for (let i = 0; i < attributes.length; i++) {
let name = attributes[i].name;
const value = attributes[i].textContent!;
// regular attributes
if (
!name.startsWith("t-") &&
!(<Element>node).getAttribute("t-attf-" + name)
) {
const attID = ctx.generateID();
ctx.addLine(`var _${attID} = '${value}';`);
if (!name.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
name = '"' + name + '"';
}
attrs.push(`${name}: _${attID}`);
handleBooleanProps(name, attID);
}
// dynamic attributes
if (name.startsWith("t-att-")) {
let attName = name.slice(6);
let formattedValue = ctx.formatExpression(ctx.getValue(value!));
if (
formattedValue[0] === "{" &&
formattedValue[formattedValue.length - 1] === "}"
) {
formattedValue = `this.utils.objectToAttrString(${formattedValue})`;
}
const attID = ctx.generateID();
if (!attName.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
attName = '"' + attName + '"';
}
// we need to combine dynamic with non dynamic attributes:
// class="a" t-att-class="'yop'" should be rendered as class="a yop"
const attValue = (<Element>node).getAttribute(attName);
if (attValue) {
const attValueID = ctx.generateID();
ctx.addLine(`var _${attValueID} = ${formattedValue};`);
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
const attrIndex = attrs.findIndex(att =>
att.startsWith(attName + ":")
);
attrs.splice(attrIndex, 1);
}
ctx.addLine(`var _${attID} = ${formattedValue};`);
attrs.push(`${attName}: _${attID}`);
handleBooleanProps(attName, attID);
}
if (name.startsWith("t-attf-")) {
let attName = name.slice(7);
if (!attName.match(/^[a-zA-Z]+$/)) {
// 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 attID = ctx.generateID();
let staticVal = (<Element>node).getAttribute(attName);
if (staticVal) {
ctx.addLine(
`var _${attID} = '${staticVal} ' + \`${formattedExpr}\`;`
);
} else {
ctx.addLine(`var _${attID} = \`${formattedExpr}\`;`);
}
attrs.push(`${attName}: _${attID}`);
}
// t-att= attributes
if (name === "t-att") {
let id = ctx.generateID();
ctx.addLine(`var _${id} = ${ctx.formatExpression(value!)};`);
tattrs.push(id);
}
}
let nodeID = ctx.generateID();
let nodeKey: any = (<Element>node).getAttribute("t-key");
if (nodeKey) {
nodeKey = ctx.formatExpression(nodeKey);
} else {
nodeKey = nodeID;
}
const parts = [`key:${nodeKey}`];
if (attrs.length + tattrs.length > 0) {
parts.push(`attrs:{${attrs.join(",")}}`);
}
if (props.length > 0) {
parts.push(`props:{${props.join(",")}}`);
}
if (withHandlers) {
parts.push(`on:{}`);
}
ctx.addLine(`let c${nodeID} = [], p${nodeID} = {${parts.join(",")}};`);
for (let id of tattrs) {
ctx.addIf(`_${id} instanceof Array`);
ctx.addLine(`p${nodeID}.attrs[_${id}[0]] = _${id}[1];`);
ctx.addElse();
ctx.addLine(`for (let key in _${id}) {`);
ctx.indent();
ctx.addLine(`p${nodeID}.attrs[key] = _${id}[key];`);
ctx.dedent();
ctx.addLine(`}`);
ctx.closeIf();
}
ctx.addLine(
`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`
);
if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`);
}
return nodeID;
}
_compileChildren(node: ChildNode, ctx: Context) {
if (node.childNodes.length > 0) {
for (let child of Array.from(node.childNodes)) {
this._compileNode(child, ctx);
}
}
}
}
//------------------------------------------------------------------------------
// Compilation Context
//------------------------------------------------------------------------------
@@ -242,452 +731,3 @@ export class Context {
return result;
}
}
//------------------------------------------------------------------------------
// QWeb rendering engine
//------------------------------------------------------------------------------
export class QWeb {
templates: { [name: string]: Template } = {};
utils = UTILS;
constructor(data?: string) {
if (data) {
this.loadTemplates(data);
}
}
static addDirective(directive: Directive) {
DIRECTIVES.push(directive);
DIRECTIVE_NAMES[directive.name] = 1;
DIRECTIVES.sort((d1, d2) => d1.priority - d2.priority);
if (directive.extraNames) {
directive.extraNames.forEach(n => (DIRECTIVE_NAMES[n] = 1));
}
}
/**
* Add a template to the internal template map. Note that it is not
* immediately compiled.
*/
addTemplate(
name: string,
xmlString: string,
allowDuplicates: boolean = false
) {
if (name in this.templates && allowDuplicates) {
return;
}
const doc = parseXML(xmlString);
if (!doc.firstChild) {
throw new Error("Invalid template (should not be empty)");
}
this._addTemplate(name, <Element>doc.firstChild);
}
_addTemplate(name: string, elem: Element) {
if (name in this.templates) {
throw new Error(`Template ${name} already defined`);
}
this._processTemplate(elem);
const template = {
elem,
fn: (context, extra) => {
const compiledFunction = this._compile(name, elem);
template.fn = compiledFunction;
return compiledFunction.call(this, context, extra);
}
};
this.templates[name] = template;
}
_processTemplate(elem: Element) {
let tbranch = elem.querySelectorAll("[t-elif], [t-else]");
for (let i = 0, ilen = tbranch.length; i < ilen; i++) {
let node = tbranch[i];
let prevElem = node.previousElementSibling!;
let pattr = function(name) {
return prevElem.getAttribute(name);
};
let nattr = function(name) {
return +!!node.getAttribute(name);
};
if (prevElem && (pattr("t-if") || pattr("t-elif"))) {
if (pattr("t-foreach")) {
throw new Error(
"t-if cannot stay at the same level as t-foreach when using t-elif or t-else"
);
}
if (
["t-if", "t-elif", "t-else"].map(nattr).reduce(function(a, b) {
return a + b;
}) > 1
) {
throw new Error(
"Only one conditional branching directive is allowed per node"
);
}
// All text nodes between branch nodes are removed
let textNode;
while ((textNode = node.previousSibling) !== prevElem) {
if (textNode.nodeValue.trim().length) {
throw new Error("text is not allowed between branching directives");
}
textNode.remove();
}
} else {
throw new Error(
"t-elif and t-else directives must be preceded by a t-if or t-elif directive"
);
}
}
}
/**
* Load templates from a xml (as a string). This will look up for the first
* <templates> tag, and will consider each child of this as a template, with
* the name given by the t-name attribute.
*/
loadTemplates(xmlstr: string) {
const doc = parseXML(xmlstr);
const templates = doc.getElementsByTagName("templates")[0];
if (!templates) {
return;
}
for (let elem of <any>templates.children) {
const name = elem.getAttribute("t-name");
this._addTemplate(name, elem);
}
}
/**
* Render a template
*
* @param {string} name the template should already have been added
*/
render(name: string, context: EvalContext = {}, extra: any = null): VNode {
const template = this.templates[name];
if (!template) {
throw new Error(`Template ${name} does not exist`);
}
return template.fn.call(this, context, extra);
}
_compile(name: string, elem: Element): CompiledTemplate {
const isDebug = elem.attributes.hasOwnProperty("t-debug");
const ctx = new Context(name);
this._compileNode(elem, ctx);
if (ctx.shouldProtectContext) {
ctx.code.unshift(" context = Object.create(context);");
}
if (ctx.shouldDefineOwner) {
// this is necessary to prevent some directives (t-forach for ex) to
// pollute the rendering context by adding some keys in it.
ctx.code.unshift(" let owner = context;");
}
if (!ctx.rootNode) {
throw new Error("A template should have one root node");
}
ctx.addLine(`return vn${ctx.rootNode};`);
let template;
try {
template = new Function(
"context",
"extra",
ctx.code.join("\n")
) as CompiledTemplate;
} catch (e) {
throw new Error(
`Invalid generated code while compiling template '${ctx.templateName.replace(
/`/g,
"'"
)}': ${e.message}`
);
}
if (isDebug) {
console.log(
`Template: ${this.templates[name].elem.outerHTML}\nCompiled code:\n` +
template.toString()
);
}
return template;
}
/**
* Generate code from an xml node
*
*/
_compileNode(node: ChildNode, ctx: Context) {
if (!(node instanceof Element)) {
// this is a text node, there are no directive to apply
let text = node.textContent!;
if (!ctx.inPreTag) {
if (lineBreakRE.test(text) && !text.trim()) {
return;
}
text = text.replace(whitespaceRE, " ");
}
if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`);
} else {
// this is an unusual situation: this text node is the result of the
// template rendering.
let nodeID = ctx.generateID();
ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`);
ctx.rootContext.rootNode = nodeID;
ctx.rootContext.parentNode = nodeID;
}
return;
}
const attributes = (<Element>node).attributes;
const validDirectives: {
directive: Directive;
value: string;
fullName: string;
}[] = [];
let withHandlers = false;
// maybe this is not optimal: we iterate on all attributes here, and again
// just after for each directive.
for (let i = 0; i < attributes.length; i++) {
let attrName = attributes[i].name;
if (attrName.startsWith("t-")) {
let dName = attrName.slice(2).split("-")[0];
if (!(dName in DIRECTIVE_NAMES)) {
throw new Error(`Unknown QWeb directive: '${attrName}'`);
}
}
}
for (let directive of DIRECTIVES) {
let fullName;
let value;
for (let i = 0; i < attributes.length; i++) {
const name = attributes[i].name;
if (
name === "t-" + directive.name ||
name.startsWith("t-" + directive.name + "-")
) {
fullName = name;
value = attributes[i].textContent;
validDirectives.push({ directive, value, fullName });
if (directive.name === "on") {
withHandlers = true;
}
}
}
}
for (let { directive, value, fullName } of validDirectives) {
if (directive.atNodeEncounter) {
const isDone = directive.atNodeEncounter({
node,
qweb: this,
ctx,
fullName,
value
});
if (isDone) {
return;
}
}
}
if (node.nodeName !== "t") {
let nodeID = this._compileGenericNode(node, ctx, withHandlers);
ctx = ctx.withParent(nodeID);
for (let { directive, value, fullName } of validDirectives) {
if (directive.atNodeCreation) {
directive.atNodeCreation({
node,
qweb: this,
ctx,
fullName,
value,
nodeID
});
}
}
}
if (node.nodeName === "pre") {
ctx = ctx.subContext("inPreTag", true);
}
this._compileChildren(node, ctx);
for (let { directive, value, fullName } of validDirectives) {
if (directive.finalize) {
directive.finalize({ node, qweb: this, ctx, fullName, value });
}
}
}
_compileGenericNode(
node: ChildNode,
ctx: Context,
withHandlers: boolean = true
): number {
// nodeType 1 is generic tag
if (node.nodeType !== 1) {
throw new Error("unsupported node type");
}
const attributes = (<Element>node).attributes;
const attrs: string[] = [];
const props: string[] = [];
const tattrs: number[] = [];
function handleBooleanProps(key, val) {
let isProp = false;
if (node.nodeName === "input" && key === "checked") {
let type = (<Element>node).getAttribute("type");
if (type === "checkbox" || type === "radio") {
isProp = true;
}
}
if (node.nodeName === "option" && key === "selected") {
isProp = true;
}
if (key === "disabled" && DISABLED_TAGS.indexOf(node.nodeName) > -1) {
isProp = true;
}
if (
(key === "readonly" && node.nodeName === "input") ||
node.nodeName === "textarea"
) {
isProp = true;
}
if (isProp) {
props.push(`${key}: _${val}`);
}
}
for (let i = 0; i < attributes.length; i++) {
let name = attributes[i].name;
const value = attributes[i].textContent!;
// regular attributes
if (
!name.startsWith("t-") &&
!(<Element>node).getAttribute("t-attf-" + name)
) {
const attID = ctx.generateID();
ctx.addLine(`var _${attID} = '${value}';`);
if (!name.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
name = '"' + name + '"';
}
attrs.push(`${name}: _${attID}`);
handleBooleanProps(name, attID);
}
// dynamic attributes
if (name.startsWith("t-att-")) {
let attName = name.slice(6);
let formattedValue = ctx.formatExpression(ctx.getValue(value!));
if (
formattedValue[0] === "{" &&
formattedValue[formattedValue.length - 1] === "}"
) {
formattedValue = `this.utils.objectToAttrString(${formattedValue})`;
}
const attID = ctx.generateID();
if (!attName.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
attName = '"' + attName + '"';
}
// we need to combine dynamic with non dynamic attributes:
// class="a" t-att-class="'yop'" should be rendered as class="a yop"
const attValue = (<Element>node).getAttribute(attName);
if (attValue) {
const attValueID = ctx.generateID();
ctx.addLine(`var _${attValueID} = ${formattedValue};`);
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
const attrIndex = attrs.findIndex(att =>
att.startsWith(attName + ":")
);
attrs.splice(attrIndex, 1);
}
ctx.addLine(`var _${attID} = ${formattedValue};`);
attrs.push(`${attName}: _${attID}`);
handleBooleanProps(attName, attID);
}
if (name.startsWith("t-attf-")) {
let attName = name.slice(7);
if (!attName.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
attName = '"' + attName + '"';
}
const formattedExpr = value!.replace(
/\{\{.*?\}\}/g,
s => "${" + ctx.formatExpression(s.slice(2, -2)) + "}"
);
const attID = ctx.generateID();
let staticVal = (<Element>node).getAttribute(attName);
if (staticVal) {
ctx.addLine(
`var _${attID} = '${staticVal} ' + \`${formattedExpr}\`;`
);
} else {
ctx.addLine(`var _${attID} = \`${formattedExpr}\`;`);
}
attrs.push(`${attName}: _${attID}`);
}
// t-att= attributes
if (name === "t-att") {
let id = ctx.generateID();
ctx.addLine(`var _${id} = ${ctx.formatExpression(value!)};`);
tattrs.push(id);
}
}
let nodeID = ctx.generateID();
let nodeKey: any = (<Element>node).getAttribute("t-key");
if (nodeKey) {
nodeKey = ctx.formatExpression(nodeKey);
} else {
nodeKey = nodeID;
}
const parts = [`key:${nodeKey}`];
if (attrs.length + tattrs.length > 0) {
parts.push(`attrs:{${attrs.join(",")}}`);
}
if (props.length > 0) {
parts.push(`props:{${props.join(",")}}`);
}
if (withHandlers) {
parts.push(`on:{}`);
}
ctx.addLine(`var c${nodeID} = [], p${nodeID} = {${parts.join(",")}};`);
for (let id of tattrs) {
ctx.addIf(`_${id} instanceof Array`);
ctx.addLine(`p${nodeID}.attrs[_${id}[0]] = _${id}[1];`);
ctx.addElse();
ctx.addLine(`for (let key in _${id}) {`);
ctx.indent();
ctx.addLine(`p${nodeID}.attrs[key] = _${id}[key];`);
ctx.dedent();
ctx.addLine(`}`);
ctx.closeIf();
}
ctx.addLine(
`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`
);
if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`);
}
return nodeID;
}
_compileChildren(node: ChildNode, ctx: Context) {
if (node.childNodes.length > 0) {
for (let child of Array.from(node.childNodes)) {
this._compileNode(child, ctx);
}
}
}
}
+242 -63
View File
@@ -10,6 +10,7 @@ import { QWeb, UTILS } from "./qweb_core";
* - t-ref
* - t-transition
* - t-widget/t-props/t-keepalive
* - t-mounted
*/
//------------------------------------------------------------------------------
@@ -56,12 +57,10 @@ QWeb.addDirective({
QWeb.addDirective({
name: "ref",
priority: 95,
atNodeCreation({ ctx, nodeID, value }) {
atNodeCreation({ ctx, nodeID, value, addNodeHook }) {
const refKey = `ref${ctx.generateID()}`;
ctx.addLine(`const ${refKey} = ${ctx.formatExpression(value)}`);
ctx.addLine(`p${nodeID}.hook = {
create: (_, n) => context.refs[${refKey}] = n.elm,
};`);
addNodeHook("create", `context.refs[${refKey}] = n.elm;`);
}
});
@@ -147,25 +146,166 @@ function whenTransitionEnd(elm: HTMLElement, cb) {
QWeb.addDirective({
name: "transition",
priority: 96,
atNodeCreation({ ctx, value, nodeID }) {
atNodeCreation({ ctx, value, addNodeHook }) {
let name = value;
ctx.addLine(`p${nodeID}.hook = {
create: (_, n) => {
this.utils.transitionCreate(n.elm, '${name}');
},
insert: vn => {
this.utils.transitionInsert(vn.elm, '${name}');
},
remove: (vn, rm) => {
this.utils.transitionRemove(vn.elm, '${name}', rm);
}
};`);
const hooks = {
create: `this.utils.transitionCreate(n.elm, '${name}');`,
insert: `this.utils.transitionInsert(vn.elm, '${name}');`,
remove: `this.utils.transitionRemove(vn.elm, '${name}', rm);`
};
for (let hookName in hooks) {
addNodeHook(hookName, hooks[hookName]);
}
}
});
//------------------------------------------------------------------------------
// t-widget
//------------------------------------------------------------------------------
/**
* The t-widget directive is certainly a complicated and hard to maintain piece
* of code. To help you, fellow developer, if you have to maintain it, I offer
* you this advice: Good luck...
*
* Since it is not 'direct' code, but rather code that generates other code, it
* is not easy to understand. To help you, here is a detailed and commented
* explanation of the code generated by the t-widget directive for the following
* situation:
* ```xml
* <t t-widget="child" t-key="'somestring'" t-props="{flag:state.flag}"/>
* ```
*
* ```js
* // this is the virtual node representing the parent div
* let c1 = [], p1 = { key: 1 };
* var vn1 = h("div", p1, c1);
*
* // t-widget directive: we start by evaluating the expression given by t-key:
* let key5 = "somestring";
*
* // We keep the index of the position of the widget in the closure. We push
* // null to reserve the slot, and will replace it later by the widget vnode,
* // when it will be ready (do not forget that preparing/rendering a widget is
* // asynchronous)
* let _2_index = c1.length;
* c1.push(null);
*
* // def3 is the deferred that will contain later either the new widget
* // creation, or the props update...
* let def3;
*
* // this is kind of tricky: we need here to find if the widget was already
* // created by a previous rendering. This is done by checking the internal
* // `cmap` (children map) of the parent widget: it maps keys to widget ids,
* // and, then, if there is an id, we look into the children list to get the
* // instance
* let w4 =
* key5 in context.__owl__.cmap
* ? context.__owl__.children[context.__owl__.cmap[key5]]
* : false;
*
* // we evaluate here the props given to the component. It is done here to be
* // able to easily reference it later, and also, it might be an expensive
* // computation, so it is certainly better to do it only once
* let props4 = { flag: context["state"].flag };
*
* // If we have a widget, currently rendering, but not ready yet, and which was
* // rendered with different props, we do not want to wait for it to be ready,
* // then update it. We simply destroy it, and start anew.
* if (
* w4 &&
* w4.__owl__.renderPromise &&
* !w4.__owl__.isStarted &&
* props4 !== w4.__owl__.renderProps
* ) {
* w4.destroy();
* w4 = false;
* }
*
* if (!w4) {
* // in this situation, we need to create a new widget. First step is
* // to get a reference to the class, then create an instance with
* // current context as parent, and the props.
* let W4 = context.widgets["child"];
* if (!W4) {
* throw new Error("Cannot find the definition of widget 'child'");
* }
* w4 = new W4(owner, props4);
*
* // Whenever we rerender the parent widget, we need to be sure that we
* // are able to find the widget instance. To do that, we register it to
* // the parent cmap (children map). Note that the 'template' key is
* // used here, since this is what identify the widget from the template
* // perspective.
* context.__owl__.cmap[key5] = w4.__owl__.id;
*
* // _prepare is called, to basically call willStart, then render the
* // widget
* def3 = w4._prepare();
*
* def3 = def3.then(vnode => {
* // we create here a virtual node for the parent (NOT the widget). This
* // means that the vdom of the parent will be stopped here, and from
* // the parent's perspective, it simply is a vnode with no children.
* // However, it shares the same dom element with the component root
* // vnode.
* let pvnode = h(vnode.sel, { key: key5 });
*
* // we add hooks to the parent vnode so we can interact with the new
* // widget at the proper time
* pvnode.data.hook = {
* insert(vn) {
* // the _mount method will patch the widget vdom into the elm vn.elm,
* // then call the mounted hooks. However, suprisingly, the snabbdom
* // patch method actually replace the elm by a new elm, so we need
* // to synchronise the pvnode elm with the resulting elm
* let nvn = w4._mount(vnode, vn.elm);
* pvnode.elm = nvn.elm;
* },
* remove() {
* // apparently, in some cases, it is necessary to call the destroy
* // method here
* w4.destroy();
* },
* destroy() {
* // and here...
* w4.destroy();
* }
* };
* // the pvnode is inserted at the correct position in the div's children
* c1[_2_index] = pvnode;
*
* // we keep here a reference to the parent vnode (representing the
* // widget, so we can reuse it later whenever we update the widget
* w4.__owl__.pvnode = pvnode;
* });
* } else {
* // this is the 'update' path of the directive.
* // the call to _updateProps is the actual widget update
* def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
* def3 = def3.then(() => {
* // if widget was destroyed in the meantime, we do nothing (so, this
* // means that the parent's element children list will have a null in
* // the widget's position, which will cause the pvnode to be removed
* // when it is patched.
* if (w4.__owl__.isDestroyed) {
* return;
* }
* // like above, we register the pvnode to the children list, so it
* // will not be patched out of the dom.
* let pvnode = w4.__owl__.pvnode;
* c1[_2_index] = pvnode;
* });
* }
*
* // we register the deferred here so the parent can coordinate its patch operation
* // with all the children.
* extra.promises.push(def3);
* return vn1;
* ```
*/
QWeb.addDirective({
name: "widget",
extraNames: ["props", "keepalive"],
@@ -210,47 +350,7 @@ QWeb.addDirective({
: ctx.inLoop
? `String(-${widgetID} - i)`
: String(widgetID);
ctx.addLine(
`let w${widgetID} = ${templateID} in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[${templateID}]] : false;`
);
ctx.addLine(`let props${widgetID} = ${props || "{}"};`);
ctx.addLine(`let isNew${widgetID} = !w${widgetID};`);
// check if we can reuse current rendering promise
ctx.addIf(`w${widgetID} && w${widgetID}.__owl__.renderPromise`);
ctx.addIf(`w${widgetID}.__owl__.isStarted`);
ctx.addLine(
`def${defID} = w${widgetID}._updateProps(props${widgetID}, extra.forceUpdate, extra.patchQueue);`
);
ctx.addElse();
ctx.addLine(`isNew${widgetID} = true`);
ctx.addIf(`props${widgetID} === w${widgetID}.__owl__.renderProps`);
ctx.addLine(`def${defID} = w${widgetID}.__owl__.renderPromise;`);
ctx.addElse();
ctx.addLine(`w${widgetID}.destroy();`);
ctx.addLine(`w${widgetID} = false`);
ctx.closeIf();
ctx.closeIf();
ctx.closeIf();
ctx.addIf(`!def${defID}`);
ctx.addIf(`w${widgetID}`);
ctx.addLine(
`def${defID} = w${widgetID}._updateProps(props${widgetID}, extra.forceUpdate, extra.patchQueue);`
);
ctx.addElse();
ctx.addLine(
`w${widgetID} = new context.widgets['${value}'](owner, props${widgetID});`
);
ctx.addLine(
`context.__owl__.cmap[${templateID}] = w${widgetID}.__owl__.id;`
);
for (let [event, method] of events) {
ctx.addLine(`w${widgetID}.on('${event}', owner, owner['${method}'])`);
}
ctx.addLine(`def${defID} = w${widgetID}._prepare();`);
ctx.closeIf();
ctx.closeIf();
let ref = node.getAttribute("t-ref");
let refExpr = "";
let refKey: string = "";
@@ -300,28 +400,107 @@ QWeb.addDirective({
const styleCode = styleExpr ? `vn.elm.style = ${styleExpr}` : "";
createHook = `vnode.data.hook = {create(_, vn){${classCode}${styleCode}}};`;
}
ctx.addIf(`isNew${widgetID}`);
ctx.addLine(
`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}});c${
ctx.parentNode
}[_${dummyID}_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=w${widgetID}._mount(vnode, vn.elm);pvnode.elm=nvn.elm;${refExpr}},remove(){${finalizeWidgetCode}},destroy(){${finalizeWidgetCode}}}; w${widgetID}.__owl__.pvnode = pvnode;});`
`let w${widgetID} = ${templateID} in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[${templateID}]] : false;`
);
ctx.addLine(`let props${widgetID} = ${props || "{}"};`);
ctx.addIf(
`w${widgetID} && w${widgetID}.__owl__.renderPromise && !w${widgetID}.__owl__.vnode && props${widgetID} !== w${widgetID}.__owl__.renderProps`
);
ctx.addLine(`w${widgetID}.destroy();`);
ctx.addLine(`w${widgetID} = false`);
ctx.closeIf();
ctx.addIf(`!w${widgetID}`);
// new widget
ctx.addLine(`let W${widgetID} = context.widgets['${value}'];`);
// maybe only do this in dev mode...
ctx.addLine(
`if (!W${widgetID}) {throw new Error(\`Cannot find the definition of widget "${value}"\`)}`
);
ctx.addLine(`w${widgetID} = new W${widgetID}(owner, props${widgetID});`);
ctx.addLine(
`context.__owl__.cmap[${templateID}] = w${widgetID}.__owl__.id;`
);
for (let [event, method] of events) {
ctx.addLine(`w${widgetID}.on('${event}', owner, owner['${method}'])`);
}
ctx.addLine(`def${defID} = w${widgetID}._prepare();`);
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${
ctx.parentNode
}[_${dummyID}_index]=pvnode;w${widgetID}.__owl__.pvnode = pvnode;});`
);
ctx.addElse();
// need to update widget
ctx.addLine(
`def${defID} = w${widgetID}._updateProps(props${widgetID}, extra.forceUpdate, extra.patchQueue);`
);
let keepAliveCode = "";
if (keepAlive) {
keepAliveCode = `pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w${widgetID}.el,vn.elm);vn.elm=w${widgetID}.el;w${widgetID}.__mount();};`;
}
ctx.addLine(
`def${defID} = def${defID}.then(()=>{if (w${widgetID}.__owl__.isDestroyed) {return};${
tattStyle ? `w${widgetID}.el.style=${tattStyle};` : ""
}${updateClassCode}let vnode;if (!w${widgetID}.__owl__.vnode){vnode=w${widgetID}.__owl__.pvnode} else { vnode=h(w${widgetID}.__owl__.vnode.sel, {key: ${templateID}});vnode.elm=w${widgetID}.el;vnode.data.hook = {insert(a){a.elm.parentNode.replaceChild(w${widgetID}.el,a.elm);a.elm=w${widgetID}.el;w${widgetID}.__mount();},remove(){${finalizeWidgetCode}}, destroy() {${finalizeWidgetCode}}}}c${
}${updateClassCode}let pvnode=w${widgetID}.__owl__.pvnode;${keepAliveCode}c${
ctx.parentNode
}[_${dummyID}_index]=vnode;});`
}[_${dummyID}_index]=pvnode;});`
);
ctx.closeIf();
ctx.addLine(`extra.promises.push(def${defID});`);
if (node.hasAttribute("t-if") || node.hasAttribute("t-else")) {
if (
node.hasAttribute("t-if") ||
node.hasAttribute("t-else") ||
node.hasAttribute("t-elif")
) {
ctx.closeIf();
}
return true;
}
});
//------------------------------------------------------------------------------
// t-mounted
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "mounted",
priority: 97,
atNodeCreation({ ctx, fullName, value, nodeID, addNodeHook }) {
ctx.rootContext.shouldDefineOwner = true;
const eventName = fullName.slice(5);
if (!eventName) {
throw new Error("Missing event name with t-on directive");
}
let extraArgs;
let handler = value.replace(/\(.*\)/, function(args) {
extraArgs = args.slice(1, -1);
return "";
});
let error = `(function () {throw new Error('Missing handler \\'' + '${handler}' + \`\\' when evaluating template '${ctx.templateName.replace(
/`/g,
"'"
)}'\`)})()`;
if (extraArgs) {
ctx.addLine(
`extra.mountedHandlers[${nodeID}] = (context['${handler}'] || ${error}).bind(owner, ${ctx.formatExpression(
extraArgs
)});`
);
} else {
ctx.addLine(
`extra.mountedHandlers[${nodeID}] = extra.mountedHandlers[${nodeID}] || (context['${handler}'] || ${error}).bind(owner);`
);
}
addNodeHook(
"insert",
`if (context.__owl__.isMounted) { extra.mountedHandlers[${nodeID}](); }`
);
}
});
+12 -2
View File
@@ -72,7 +72,7 @@ export class Store extends EventBus {
const func: (...any) => any = entry[1];
this.getters[name] = payload => {
return func({ state: this.state, getters: this.getters }, payload);
}
};
}
}
@@ -154,6 +154,8 @@ interface EnvWithStore extends Env {
store: Store;
}
let nextID = 1;
export function connect(mapStateToProps, options: any = {}) {
let hashFunction = options.hashFunction || null;
@@ -184,7 +186,7 @@ export function connect(mapStateToProps, options: any = {}) {
return function<E extends EnvWithStore, P, S>(
Comp: Constructor<Component<E, P, S>>
) {
return class extends Comp {
const Result = class extends Comp {
constructor(parent, props?: any) {
const env = parent instanceof Component ? parent.env : parent;
const ownProps = Object.assign({}, props || {});
@@ -269,5 +271,13 @@ export function connect(mapStateToProps, options: any = {}) {
return super._updateProps(mergedProps, forceUpdate, patchQueue);
}
};
// we assign here a unique name to the resulting anonymous class.
// this is necessary for Owl to be able to properly deduce templates.
// Otherwise, all connected components would have the same name, and then
// each component after the first will necessarily have the same template.
let name = `ConnectedComponent${nextID++}`;
Object.defineProperty(Result, "name", { value: name });
return Result;
};
}
+45 -129
View File
@@ -3,17 +3,54 @@
*
* We have here a small collection of utility functions:
*
* - escape
* - memoize
* - debounce
* - patch
* - unpatch
* - loadTemplates
* - loadJS
* - whenReady
* - parseXML
* - loadJS
* - loadTemplates
* - escape
* - debounce
*/
export function whenReady(fn) {
if (document.readyState === "complete") {
fn();
} else {
document.addEventListener("DOMContentLoaded", fn, false);
}
}
const loadedScripts: { [key: string]: Promise<void> } = {};
export function loadJS(url: string): Promise<void> {
if (url in loadedScripts) {
return loadedScripts[url];
}
const promise: Promise<void> = new Promise(function(resolve, reject) {
const script = document.createElement("script");
script.type = "text/javascript";
script.src = url;
script.onload = function() {
resolve();
};
script.onerror = function() {
reject(`Error loading file '${url}'`);
};
const head = document.head || document.getElementsByTagName("head")[0];
head.appendChild(script);
});
loadedScripts[url] = promise;
return promise;
}
export async function loadTemplates(url: string): Promise<string> {
const result = await fetch(url);
if (!result.ok) {
throw new Error("Error while fetching xml templates");
}
let templates = await result.text();
templates = templates.replace(/<!--[\s\S]*?-->/g, "");
return templates;
}
export function escape(str: string | number | undefined): string {
if (str === undefined) {
return "";
@@ -29,26 +66,6 @@ export function escape(str: string | number | undefined): string {
.replace(/`/g, "&#x60;");
}
export type HashFn = (args: any[]) => string;
export function memoize<R, T extends (...args: any[]) => R>(
f: T,
hash?: HashFn
): T {
if (!hash) {
hash = args => args.map(a => String(a)).join(",");
}
let cache: { [key: string]: R } = {};
function memoizedFunction(...args: any[]) {
let hashValue = hash!(args);
if (!(hashValue in cache)) {
cache[hashValue] = f(...args);
}
return cache[hashValue];
}
return memoizedFunction as T;
}
/**
* Returns a function, that, as long as it continues to be invoked, will not
* be triggered. The function will be called after it stops being called for
@@ -80,104 +97,3 @@ export function debounce(
}
};
}
export function patch(C: any, patchName: string, patch: any) {
const proto = C.prototype;
if (!proto.__patches) {
proto.__patches = {
origMethods: {},
patches: {},
current: []
};
}
if (proto.__patches.patches[patchName]) {
throw new Error(`Patch [${patchName}] already exists`);
}
proto.__patches.patches[patchName] = patch;
applyPatch(proto, patch);
proto.__patches.current.push(patchName);
function applyPatch(proto, patch) {
Object.keys(patch).forEach(function(methodName) {
const method = patch[methodName];
if (typeof method === "function") {
const original = proto[methodName];
if (!(methodName in proto.__patches.origMethods)) {
proto.__patches.origMethods[methodName] = original;
}
proto[methodName] = function(...args) {
this._super = original;
return method.call(this, ...args);
};
}
});
}
}
export function unpatch(C: any, patchName: string) {
const proto = C.prototype;
const patchInfo = proto.__patches;
delete proto.__patches;
// reset to original
for (let k in patchInfo.origMethods) {
proto[k] = patchInfo.origMethods[k];
}
// apply other patches
for (let name of patchInfo.current) {
if (name !== patchName) {
patch(C, name, patchInfo.patches[name]);
}
}
}
export async function loadTemplates(url: string): Promise<string> {
const result = await fetch(url);
if (!result.ok) {
throw new Error("Error while fetching xml templates");
}
let templates = await result.text();
templates = templates.replace(/<!--[\s\S]*?-->/g, "");
return templates;
}
const loadedScripts: { [key: string]: Promise<void> } = {};
export function loadJS(url: string): Promise<void> {
if (url in loadedScripts) {
return loadedScripts[url];
}
const promise: Promise<void> = new Promise(function(resolve, reject) {
const script = document.createElement("script");
script.type = "text/javascript";
script.src = url;
script.onload = function() {
resolve();
};
script.onerror = function() {
reject(`Error loading file '${url}'`);
};
const head = document.head || document.getElementsByTagName("head")[0];
head.appendChild(script);
});
loadedScripts[url] = promise;
return promise;
}
export function whenReady(fn) {
if (document.readyState === "complete") {
fn();
} else {
document.addEventListener("DOMContentLoaded", fn, false);
}
}
export function parseXML(xml: string): Document {
const parser = new DOMParser();
const doc = parser.parseFromString(xml, "text/xml");
if (doc.getElementsByTagName("parsererror").length) {
throw new Error("Invalid XML in template");
}
return doc;
}
+67 -132
View File
@@ -5,42 +5,29 @@ exports[`class and style attributes with t-widget dynamic t-att-style is properl
) {
let owner = context;
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
const _5 = context['state'].style;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {};
let isNew4 = !w4;
if (w4 && w4.__owl__.renderPromise) {
if (w4.__owl__.isStarted) {
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
} else {
isNew4 = true
if (props4 === w4.__owl__.renderProps) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false
}
}
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
w4.destroy();
w4 = false
}
if (!def3) {
if (w4) {
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
} else {
w4 = new context.widgets['child'](owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4._prepare();
}
}
const _5 = context['state'].style;
if (isNew4) {
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.style = _5}};let pvnode=h(vnode.sel, {key: 4});c1[_2_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=w4._mount(vnode, vn.elm);pvnode.elm=nvn.elm;},remove(){w4.destroy()},destroy(){w4.destroy()}}; w4.__owl__.pvnode = pvnode;});
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=>{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;});
} else {
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};w4.el.style=_5;let vnode;if (!w4.__owl__.vnode){vnode=w4.__owl__.pvnode} else { vnode=h(w4.__owl__.vnode.sel, {key: 4});vnode.elm=w4.el;vnode.data.hook = {insert(a){a.elm.parentNode.replaceChild(w4.el,a.elm);a.elm=w4.el;w4.__mount();},remove(){w4.destroy()}, destroy() {w4.destroy()}}}c1[_2_index]=vnode;});
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};w4.el.style=_5;let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
return vn1;
@@ -52,46 +39,33 @@ exports[`class and style attributes with t-widget t-att-class is properly added/
) {
let owner = context;
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
const _5 = {a: context['state'].a,b: context['state'].b};
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {};
let isNew4 = !w4;
if (w4 && w4.__owl__.renderPromise) {
if (w4.__owl__.isStarted) {
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
} else {
isNew4 = true
if (props4 === w4.__owl__.renderProps) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false
}
}
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
w4.destroy();
w4 = false
}
if (!def3) {
if (w4) {
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
} else {
w4 = new context.widgets['child'](owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4._prepare();
}
}
const _5 = {a: context['state'].a,b: context['state'].b};
if (isNew4) {
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=>{vnode.data.hook = {create(_, vn){for (let k in _5) {
if (_5[k]) {
vn.elm.classList.add(k);
}
}}};let pvnode=h(vnode.sel, {key: 4});c1[_2_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=w4._mount(vnode, vn.elm);pvnode.elm=nvn.elm;},remove(){w4.destroy()},destroy(){w4.destroy()}}; 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(){w4.destroy()},destroy(){w4.destroy()}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
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 vnode;if (!w4.__owl__.vnode){vnode=w4.__owl__.pvnode} else { vnode=h(w4.__owl__.vnode.sel, {key: 4});vnode.elm=w4.el;vnode.data.hook = {insert(a){a.elm.parentNode.replaceChild(w4.el,a.elm);a.elm=w4.el;w4.__mount();},remove(){w4.destroy()}, destroy() {w4.destroy()}}}c1[_2_index]=vnode;});
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let cl=w4.el.classList;for (let k in _5) {if (_5[k]) {cl.add(k)} else {cl.remove(k)}}let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
return vn1;
@@ -104,7 +78,7 @@ exports[`composition sub widgets with some state rendered in a loop 1`] = `
let owner = context;
context = Object.create(context);
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = context['state'].numbers;
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
@@ -125,33 +99,20 @@ exports[`composition sub widgets with some state rendered in a loop 1`] = `
let def6;
let w7 = key8 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[key8]] : false;
let props7 = {};
let isNew7 = !w7;
if (w7 && w7.__owl__.renderPromise) {
if (w7.__owl__.isStarted) {
def6 = w7._updateProps(props7, extra.forceUpdate, extra.patchQueue);
} else {
isNew7 = true
if (props7 === w7.__owl__.renderProps) {
def6 = w7.__owl__.renderPromise;
} else {
w7.destroy();
w7 = false
}
}
if (w7 && w7.__owl__.renderPromise && !w7.__owl__.vnode && props7 !== w7.__owl__.renderProps) {
w7.destroy();
w7 = false
}
if (!def6) {
if (w7) {
def6 = w7._updateProps(props7, extra.forceUpdate, extra.patchQueue);
} else {
w7 = new context.widgets['ChildWidget'](owner, props7);
context.__owl__.cmap[key8] = w7.__owl__.id;
def6 = w7._prepare();
}
}
if (isNew7) {
def6 = def6.then(vnode=>{let pvnode=h(vnode.sel, {key: key8});c1[_5_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=w7._mount(vnode, vn.elm);pvnode.elm=nvn.elm;},remove(){w7.destroy()},destroy(){w7.destroy()}}; w7.__owl__.pvnode = pvnode;});
if (!w7) {
let W7 = context.widgets['ChildWidget'];
if (!W7) {throw new Error(\`Cannot find the definition of widget \\"ChildWidget\\"\`)}
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;});
} else {
def6 = def6.then(()=>{if (w7.__owl__.isDestroyed) {return};let vnode;if (!w7.__owl__.vnode){vnode=w7.__owl__.pvnode} else { vnode=h(w7.__owl__.vnode.sel, {key: key8});vnode.elm=w7.el;vnode.data.hook = {insert(a){a.elm.parentNode.replaceChild(w7.el,a.elm);a.elm=w7.el;w7.__mount();},remove(){w7.destroy()}, destroy() {w7.destroy()}}}c1[_5_index]=vnode;});
def6 = w7._updateProps(props7, extra.forceUpdate, extra.patchQueue);
def6 = def6.then(()=>{if (w7.__owl__.isDestroyed) {return};let pvnode=w7.__owl__.pvnode;c1[_5_index]=pvnode;});
}
extra.promises.push(def6);
}
@@ -164,7 +125,7 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
) {
let owner = context;
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let key5 = 'somestring';
@@ -173,33 +134,20 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
let def3;
let w4 = key5 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[key5]] : false;
let props4 = {flag: context['state'].flag};
let isNew4 = !w4;
if (w4 && w4.__owl__.renderPromise) {
if (w4.__owl__.isStarted) {
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
} else {
isNew4 = true
if (props4 === w4.__owl__.renderProps) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false
}
}
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
w4.destroy();
w4 = false
}
if (!def3) {
if (w4) {
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
} else {
w4 = new context.widgets['child'](owner, props4);
context.__owl__.cmap[key5] = w4.__owl__.id;
def3 = w4._prepare();
}
}
if (isNew4) {
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: key5});c1[_2_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=w4._mount(vnode, vn.elm);pvnode.elm=nvn.elm;},remove(){w4.destroy()},destroy(){w4.destroy()}}; w4.__owl__.pvnode = pvnode;});
if (!w4) {
let W4 = context.widgets['child'];
if (!W4) {throw new Error(\`Cannot find the definition of widget \\"child\\"\`)}
w4 = new W4(owner, props4);
context.__owl__.cmap[key5] = w4.__owl__.id;
def3 = w4._prepare();
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: key5, hook: {insert(vn){let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove(){w4.destroy()},destroy(){w4.destroy()}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let vnode;if (!w4.__owl__.vnode){vnode=w4.__owl__.pvnode} else { vnode=h(w4.__owl__.vnode.sel, {key: key5});vnode.elm=w4.el;vnode.data.hook = {insert(a){a.elm.parentNode.replaceChild(w4.el,a.elm);a.elm=w4.el;w4.__mount();},remove(){w4.destroy()}, destroy() {w4.destroy()}}}c1[_2_index]=vnode;});
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;
@@ -211,7 +159,7 @@ exports[`random stuff/miscellaneous t-props should not be undefined (snapshottin
) {
let owner = context;
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
@@ -219,33 +167,20 @@ exports[`random stuff/miscellaneous t-props should not be undefined (snapshottin
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {};
let isNew4 = !w4;
if (w4 && w4.__owl__.renderPromise) {
if (w4.__owl__.isStarted) {
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
} else {
isNew4 = true
if (props4 === w4.__owl__.renderProps) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false
}
}
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
w4.destroy();
w4 = false
}
if (!def3) {
if (w4) {
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
} else {
w4 = new context.widgets['child'](owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4._prepare();
}
}
if (isNew4) {
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4});c1[_2_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=w4._mount(vnode, vn.elm);pvnode.elm=nvn.elm;},remove(){w4.destroy()},destroy(){w4.destroy()}}; w4.__owl__.pvnode = pvnode;});
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;},remove(){w4.destroy()},destroy(){w4.destroy()}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let vnode;if (!w4.__owl__.vnode){vnode=w4.__owl__.pvnode} else { vnode=h(w4.__owl__.vnode.sel, {key: 4});vnode.elm=w4.el;vnode.data.hook = {insert(a){a.elm.parentNode.replaceChild(w4.el,a.elm);a.elm=w4.el;w4.__mount();},remove(){w4.destroy()}, destroy() {w4.destroy()}}}c1[_2_index]=vnode;});
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;
+158 -141
View File
@@ -4,22 +4,22 @@ exports[`animations t-transition, on a simple node 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var c2 = [], p2 = {key:2};
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);
}
};
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;
}"
@@ -32,7 +32,7 @@ exports[`attributes dynamic attribute falsy variable 1`] = `
) {
var h = this.utils.h;
var _1 = context['value'];
var c2 = [], p2 = {key:2,attrs:{foo: _1}};
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
var vn2 = h('div', p2, c2);
return vn2;
}"
@@ -43,7 +43,7 @@ exports[`attributes dynamic attribute with a dash 1`] = `
) {
var h = this.utils.h;
var _1 = context['id'];
var c2 = [], p2 = {key:2,attrs:{\\"data-action-id\\": _1}};
let c2 = [], p2 = {key:2,attrs:{\\"data-action-id\\": _1}};
var vn2 = h('div', p2, c2);
return vn2;
}"
@@ -54,7 +54,7 @@ exports[`attributes dynamic attributes 1`] = `
) {
var h = this.utils.h;
var _1 = 'bar';
var c2 = [], p2 = {key:2,attrs:{foo: _1}};
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
var vn2 = h('div', p2, c2);
return vn2;
}"
@@ -65,7 +65,7 @@ exports[`attributes dynamic formatted attributes with a dash 1`] = `
) {
var h = this.utils.h;
var _1 = \`Some text \${context['id']}\`;
var c2 = [], p2 = {key:2,attrs:{\\"aria-label\\": _1}};
let c2 = [], p2 = {key:2,attrs:{\\"aria-label\\": _1}};
var vn2 = h('div', p2, c2);
return vn2;
}"
@@ -76,7 +76,7 @@ exports[`attributes fixed variable 1`] = `
) {
var h = this.utils.h;
var _1 = context['value'];
var c2 = [], p2 = {key:2,attrs:{foo: _1}};
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
var vn2 = h('div', p2, c2);
return vn2;
}"
@@ -87,7 +87,18 @@ exports[`attributes format expression 1`] = `
) {
var h = this.utils.h;
var _1 = \`\${context['value'] + 37}\`;
var c2 = [], p2 = {key:2,attrs:{foo: _1}};
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
var vn2 = h('div', p2, c2);
return vn2;
}"
`;
exports[`attributes format expression, other format 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var _1 = \`\${context['value'] + 37}\`;
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
var vn2 = h('div', p2, c2);
return vn2;
}"
@@ -98,7 +109,7 @@ exports[`attributes format literal 1`] = `
) {
var h = this.utils.h;
var _1 = \`bar\`;
var c2 = [], p2 = {key:2,attrs:{foo: _1}};
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
var vn2 = h('div', p2, c2);
return vn2;
}"
@@ -109,7 +120,7 @@ exports[`attributes format multiple 1`] = `
) {
var h = this.utils.h;
var _1 = \`a \${context['value1']} is \${context['value2']} of \${context['value3']} ]\`;
var c2 = [], p2 = {key:2,attrs:{foo: _1}};
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
var vn2 = h('div', p2, c2);
return vn2;
}"
@@ -120,7 +131,7 @@ exports[`attributes format value 1`] = `
) {
var h = this.utils.h;
var _1 = \`b\${context['value']}r\`;
var c2 = [], p2 = {key:2,attrs:{foo: _1}};
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
var vn2 = h('div', p2, c2);
return vn2;
}"
@@ -130,11 +141,11 @@ exports[`attributes from object variables set previously 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = {a: 'b'};
var _3 = _2.a;
var c4 = [], p4 = {key:4,attrs:{class: _3}};
let c4 = [], p4 = {key:4,attrs:{class: _3}};
var vn4 = h('span', p4, c4);
c1.push(vn4);
return vn1;
@@ -145,11 +156,11 @@ exports[`attributes from variables set previously 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = 'def';
var _3 = _2;
var c4 = [], p4 = {key:4,attrs:{class: _3}};
let c4 = [], p4 = {key:4,attrs:{class: _3}};
var vn4 = h('span', p4, c4);
c1.push(vn4);
return vn1;
@@ -161,7 +172,7 @@ exports[`attributes object 1`] = `
) {
var h = this.utils.h;
var _1 = context['value'];
var c2 = [], p2 = {key:2,attrs:{}};
let c2 = [], p2 = {key:2,attrs:{}};
if (_1 instanceof Array) {
p2.attrs[_1[0]] = _1[1];
} else {
@@ -181,7 +192,7 @@ exports[`attributes static attributes 1`] = `
var _1 = 'a';
var _2 = 'b';
var _3 = 'c';
var c4 = [], p4 = {key:4,attrs:{foo: _1,bar: _2,baz: _3}};
let c4 = [], p4 = {key:4,attrs:{foo: _1,bar: _2,baz: _3}};
var vn4 = h('div', p4, c4);
return vn4;
}"
@@ -193,7 +204,7 @@ exports[`attributes static attributes on void elements 1`] = `
var h = this.utils.h;
var _1 = '/test.jpg';
var _2 = 'Test';
var c3 = [], p3 = {key:3,attrs:{src: _1,alt: _2}};
let c3 = [], p3 = {key:3,attrs:{src: _1,alt: _2}};
var vn3 = h('img', p3, c3);
return vn3;
}"
@@ -204,7 +215,7 @@ exports[`attributes static attributes with dashes 1`] = `
) {
var h = this.utils.h;
var _1 = 'Close';
var c2 = [], p2 = {key:2,attrs:{\\"aria-label\\": _1}};
let c2 = [], p2 = {key:2,attrs:{\\"aria-label\\": _1}};
var vn2 = h('div', p2, c2);
return vn2;
}"
@@ -217,7 +228,7 @@ exports[`attributes t-att-class and class should combine together 1`] = `
var _1 = 'hello';
var _3 = context['value'];
var _2 = 'hello' + (_3 ? ' ' + _3 : '');
var c4 = [], p4 = {key:4,attrs:{class: _2}};
let c4 = [], p4 = {key:4,attrs:{class: _2}};
var vn4 = h('div', p4, c4);
return vn4;
}"
@@ -230,7 +241,7 @@ exports[`attributes t-att-class with object 1`] = `
var _1 = 'static';
var _3 = this.utils.objectToAttrString({a: context['b'],c: context['d'],e: context['f']});
var _2 = 'static' + (_3 ? ' ' + _3 : '');
var c4 = [], p4 = {key:4,attrs:{class: _2}};
let c4 = [], p4 = {key:4,attrs:{class: _2}};
var vn4 = h('div', p4, c4);
return vn4;
}"
@@ -241,7 +252,7 @@ exports[`attributes t-attf-class should combine with class 1`] = `
) {
var h = this.utils.h;
var _1 = 'hello ' + \`world\`;
var c2 = [], p2 = {key:2,attrs:{class: _1}};
let c2 = [], p2 = {key:2,attrs:{class: _1}};
var vn2 = h('div', p2, c2);
return vn2;
}"
@@ -252,7 +263,7 @@ exports[`attributes tuple literal 1`] = `
) {
var h = this.utils.h;
var _1 = ['foo', 'bar'];
var c2 = [], p2 = {key:2,attrs:{}};
let c2 = [], p2 = {key:2,attrs:{}};
if (_1 instanceof Array) {
p2.attrs[_1[0]] = _1[1];
} else {
@@ -270,7 +281,7 @@ exports[`attributes tuple variable 1`] = `
) {
var h = this.utils.h;
var _1 = context['value'];
var c2 = [], p2 = {key:2,attrs:{}};
let c2 = [], p2 = {key:2,attrs:{}};
if (_1 instanceof Array) {
p2.attrs[_1[0]] = _1[1];
} else {
@@ -288,11 +299,11 @@ exports[`debugging t-debug 1`] = `
) {
var h = this.utils.h;
debugger;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (true) {
debugger;
var c2 = [], p2 = {key:2};
let c2 = [], p2 = {key:2};
var vn2 = h('span', p2, c2);
c1.push(vn2);
c2.push({text: \`hey\`});
@@ -305,7 +316,7 @@ exports[`debugging t-log 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = 42;
console.log(_2 + 3)
@@ -318,7 +329,7 @@ exports[`foreach does not pollute the rendering context 1`] = `
) {
context = Object.create(context);
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = [1];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
@@ -346,7 +357,7 @@ exports[`foreach iterate on items (on a element node) 1`] = `
) {
context = Object.create(context);
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = [1, 2];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
@@ -360,7 +371,7 @@ exports[`foreach iterate on items (on a element node) 1`] = `
context.item_index = i;
context.item = _3[i];
context.item_value = _4[i];
var c5 = [], p5 = {key:context['item']};
let c5 = [], p5 = {key:context['item']};
var vn5 = h('span', p5, c5);
c1.push(vn5);
var _6 = context['item'];
@@ -377,7 +388,7 @@ exports[`foreach iterate on items 1`] = `
) {
context = Object.create(context);
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = [3, 2, 1];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
@@ -417,7 +428,7 @@ exports[`foreach iterate, dict param 1`] = `
) {
context = Object.create(context);
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = context['value'];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
@@ -462,7 +473,7 @@ exports[`foreach iterate, integer param 1`] = `
) {
context = Object.create(context);
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = 3;
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
@@ -502,7 +513,7 @@ exports[`foreach iterate, position 1`] = `
) {
context = Object.create(context);
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = 5;
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
@@ -539,7 +550,7 @@ exports[`foreach warn if no key in some case 1`] = `
) {
context = Object.create(context);
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = [1, 2];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
@@ -553,7 +564,7 @@ exports[`foreach warn if no key in some case 1`] = `
context.item_index = i;
context.item = _3[i];
context.item_value = _4[i];
var c5 = [], p5 = {key:5};
let c5 = [], p5 = {key:5};
var vn5 = h('span', p5, c5);
c1.push(vn5);
var _6 = context['item'];
@@ -569,7 +580,7 @@ exports[`loading templates can initialize qweb with a string 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
c1.push({text: \`jupiler\`});
return vn1;
@@ -580,13 +591,13 @@ exports[`loading templates can load a few templates from a xml string 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('ul', p1, c1);
var c2 = [], p2 = {key:2};
let c2 = [], p2 = {key:2};
var vn2 = h('li', p2, c2);
c1.push(vn2);
c2.push({text: \`ok\`});
var c3 = [], p3 = {key:3};
let c3 = [], p3 = {key:3};
var vn3 = h('li', p3, c3);
c1.push(vn3);
c3.push({text: \`foo\`});
@@ -599,7 +610,7 @@ exports[`misc global 1`] = `
) {
context = Object.create(context);
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = [4,5,6];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
@@ -613,7 +624,7 @@ exports[`misc global 1`] = `
context.value_index = i;
context.value = _3[i];
context.value_value = _4[i];
var c5 = [], p5 = {key:5,on:{}};
let c5 = [], p5 = {key:5,on:{}};
var vn5 = h('span', p5, c5);
c1.push(vn5);
var _6 = context['value'];
@@ -623,12 +634,12 @@ exports[`misc global 1`] = `
{
let _11 = 'bbb'
var _13 = 'agüero';
var c14 = [], p14 = {key:14,attrs:{\\"falló\\": _13},on:{}};
let c14 = [], p14 = {key:14,attrs:{\\"falló\\": _13},on:{}};
var vn14 = h('Año', p14, c14);
c1.push(vn14);
{
let _15 = 'aaa'
var c16 = [], p16 = {key:16,on:{}};
let c16 = [], p16 = {key:16,on:{}};
var vn16 = h('span', p16, c16);
c14.push(vn16);
if (_15 || _15 === 0) {
@@ -637,7 +648,7 @@ exports[`misc global 1`] = `
c16.push({text: \`foo default\`});
}
}
var c17 = [], p17 = {key:17,on:{}};
let c17 = [], p17 = {key:17,on:{}};
var vn17 = h('span', p17, c17);
c14.push(vn17);
var _18 = context['foo'];
@@ -649,7 +660,7 @@ exports[`misc global 1`] = `
var _19 = 'bbb';
{
let _19 = 'bbb'
var c20 = [], p20 = {key:20,on:{}};
let c20 = [], p20 = {key:20,on:{}};
var vn20 = h('span', p20, c20);
c14.push(vn20);
if (_19 || _19 === 0) {
@@ -660,7 +671,7 @@ exports[`misc global 1`] = `
}
}
}
var c21 = [], p21 = {key:21,on:{}};
let c21 = [], p21 = {key:21,on:{}};
var vn21 = h('div', p21, c21);
c1.push(vn21);
var _22 = context['toto'];
@@ -684,7 +695,7 @@ exports[`special cases for some boolean html attributes/properties input type= c
var h = this.utils.h;
var _1 = 'checkbox';
var _2 = context['flag'];
var c3 = [], p3 = {key:3,attrs:{type: _1,checked: _2},props:{checked: _2}};
let c3 = [], p3 = {key:3,attrs:{type: _1,checked: _2},props:{checked: _2}};
var vn3 = h('input', p3, c3);
return vn3;
}"
@@ -694,36 +705,36 @@ exports[`special cases for some boolean html attributes/properties various boole
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = 'checkbox';
var _3 = 'checked';
var c4 = [], p4 = {key:4,attrs:{type: _2,checked: _3},props:{checked: _3}};
let c4 = [], p4 = {key:4,attrs:{type: _2,checked: _3},props:{checked: _3}};
var vn4 = h('input', p4, c4);
c1.push(vn4);
var _5 = 'checked';
var c6 = [], p6 = {key:6,attrs:{checked: _5}};
let c6 = [], p6 = {key:6,attrs:{checked: _5}};
var vn6 = h('input', p6, c6);
c1.push(vn6);
var _7 = 'checked';
var c8 = [], p8 = {key:8,attrs:{checked: _7}};
let c8 = [], p8 = {key:8,attrs:{checked: _7}};
var vn8 = h('div', p8, c8);
c1.push(vn8);
var _9 = 'selected';
var c10 = [], p10 = {key:10,attrs:{selected: _9}};
let c10 = [], p10 = {key:10,attrs:{selected: _9}};
var vn10 = h('div', p10, c10);
c1.push(vn10);
var _11 = 'selected';
var _12 = '1';
var c13 = [], p13 = {key:13,attrs:{selected: _11,other: _12},props:{selected: _11}};
let c13 = [], p13 = {key:13,attrs:{selected: _11,other: _12},props:{selected: _11}};
var vn13 = h('option', p13, c13);
c1.push(vn13);
var _14 = 'readonly';
var c15 = [], p15 = {key:15,attrs:{readonly: _14},props:{readonly: _14}};
let c15 = [], p15 = {key:15,attrs:{readonly: _14},props:{readonly: _14}};
var vn15 = h('input', p15, c15);
c1.push(vn15);
var _16 = 'disabled';
var c17 = [], p17 = {key:17,attrs:{disabled: _16},props:{disabled: _16}};
let c17 = [], p17 = {key:17,attrs:{disabled: _16},props:{disabled: _16}};
var vn17 = h('button', p17, c17);
c1.push(vn17);
return vn1;
@@ -734,9 +745,9 @@ exports[`static templates div with a span child node 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var c2 = [], p2 = {key:2};
let c2 = [], p2 = {key:2};
var vn2 = h('span', p2, c2);
c1.push(vn2);
c2.push({text: \`word\`});
@@ -748,7 +759,7 @@ exports[`static templates div with a text node 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
c1.push({text: \`word\`});
return vn1;
@@ -759,7 +770,7 @@ exports[`static templates empty div 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
return vn1;
}"
@@ -778,7 +789,7 @@ exports[`t-call (template calling basic caller 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
c1.push({text: \`ok\`});
return vn1;
@@ -789,7 +800,7 @@ exports[`t-call (template calling inherit context 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = 1;
{
@@ -806,7 +817,7 @@ exports[`t-call (template calling scoped parameters 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
{
let _2 = 42
@@ -824,7 +835,7 @@ exports[`t-call (template calling with unused body 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c2 = [], p2 = {key:2};
let c2 = [], p2 = {key:2};
var vn2 = h('div', p2, c2);
c2.push({text: \`ok\`});
return vn2;
@@ -837,7 +848,7 @@ exports[`t-call (template calling with unused setbody 1`] = `
var h = this.utils.h;
{
let _1 = 3
var c2 = [], p2 = {key:2};
let c2 = [], p2 = {key:2};
var vn2 = h('div', p2, c2);
c2.push({text: \`ok\`});
}
@@ -849,7 +860,7 @@ exports[`t-call (template calling with used body 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c2 = [], p2 = {key:2};
let c2 = [], p2 = {key:2};
var vn2 = h('h1', p2, c2);
c2.push({text: \`ok\`});
return vn2;
@@ -860,7 +871,7 @@ exports[`t-call (template calling with used set body 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1);
{
let _2 = 'ok'
@@ -876,7 +887,7 @@ exports[`t-esc escaping on a node 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1,on:{}};
let c1 = [], p1 = {key:1,on:{}};
var vn1 = h('span', p1, c1);
var _2 = 'ok';
if (_2 || _2 === 0) {
@@ -890,7 +901,7 @@ exports[`t-esc escaping on a node with a body 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1,on:{}};
let c1 = [], p1 = {key:1,on:{}};
var vn1 = h('span', p1, c1);
var _2 = 'ok';
if (_2 || _2 === 0) {
@@ -906,7 +917,7 @@ exports[`t-esc escaping on a node with a body, as a default 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1,on:{}};
let c1 = [], p1 = {key:1,on:{}};
var vn1 = h('span', p1, c1);
var _2 = context['var'];
if (_2 || _2 === 0) {
@@ -922,7 +933,7 @@ exports[`t-esc literal 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1);
var _2 = 'ok';
if (_2 || _2 === 0) {
@@ -936,7 +947,7 @@ exports[`t-esc variable 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1);
var _2 = context['var'];
if (_2 || _2 === 0) {
@@ -950,7 +961,7 @@ exports[`t-if boolean value condition elif 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['color'] == 'black') {
c1.push({text: \`black pearl\`});
@@ -972,9 +983,9 @@ exports[`t-if boolean value condition else 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var c2 = [], p2 = {key:2};
let c2 = [], p2 = {key:2};
var vn2 = h('span', p2, c2);
c1.push(vn2);
c2.push({text: \`begin\`});
@@ -984,7 +995,7 @@ exports[`t-if boolean value condition else 1`] = `
else {
c1.push({text: \`ok-else\`});
}
var c3 = [], p3 = {key:3};
let c3 = [], p3 = {key:3};
var vn3 = h('span', p3, c3);
c1.push(vn3);
c3.push({text: \`end\`});
@@ -996,9 +1007,9 @@ exports[`t-if boolean value condition false else 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var c2 = [], p2 = {key:2};
let c2 = [], p2 = {key:2};
var vn2 = h('span', p2, c2);
c1.push(vn2);
c2.push({text: \`begin\`});
@@ -1008,7 +1019,7 @@ exports[`t-if boolean value condition false else 1`] = `
else {
c1.push({text: \`fail-else\`});
}
var c3 = [], p3 = {key:3};
let c3 = [], p3 = {key:3};
var vn3 = h('span', p3, c3);
c1.push(vn3);
c3.push({text: \`end\`});
@@ -1020,7 +1031,7 @@ exports[`t-if boolean value condition missing 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1);
if (context['condition']) {
c1.push({text: \`fail\`});
@@ -1033,7 +1044,7 @@ exports[`t-if boolean value false condition 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['condition']) {
c1.push({text: \`ok\`});
@@ -1046,7 +1057,7 @@ exports[`t-if boolean value true condition 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['condition']) {
c1.push({text: \`ok\`});
@@ -1059,7 +1070,7 @@ exports[`t-if can use some boolean operators in expressions 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['cond1'] && context['cond2']) {
c1.push({text: \`and\`});
@@ -1093,7 +1104,7 @@ exports[`t-key can use t-key directive on a node 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:context['beer'].id};
let c1 = [], p1 = {key:context['beer'].id};
var vn1 = h('div', p1, c1);
var _2 = context['beer'].name;
if (_2 || _2 === 0) {
@@ -1108,7 +1119,7 @@ exports[`t-key t-key directive in a list 1`] = `
) {
context = Object.create(context);
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('ul', p1, c1);
var _2 = context['beers'];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
@@ -1122,7 +1133,7 @@ exports[`t-key t-key directive in a list 1`] = `
context.beer_index = i;
context.beer = _3[i];
context.beer_value = _4[i];
var c5 = [], p5 = {key:context['beer'].id};
let c5 = [], p5 = {key:context['beer'].id};
var vn5 = h('li', p5, c5);
c1.push(vn5);
var _6 = context['beer'].name;
@@ -1141,7 +1152,7 @@ exports[`t-on can bind event handler 1`] = `
) {
let owner = context;
var h = this.utils.h;
var c1 = [], p1 = {key:1,on:{}};
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);
p1.on['click'] = extra.handlers['click' + 1];
@@ -1155,7 +1166,7 @@ exports[`t-on can bind handlers with arguments 1`] = `
) {
let owner = context;
var h = this.utils.h;
var c1 = [], p1 = {key:1,on:{}};
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);
c1.push({text: \`Click\`});
@@ -1168,7 +1179,7 @@ exports[`t-on can bind handlers with empty object (with non empty inner string 1
) {
let owner = context;
var h = this.utils.h;
var c1 = [], p1 = {key:1,on:{}};
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, {});
c1.push({text: \`Click\`});
@@ -1181,7 +1192,7 @@ exports[`t-on can bind handlers with empty object 1`] = `
) {
let owner = context;
var h = this.utils.h;
var c1 = [], p1 = {key:1,on:{}};
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, {});
c1.push({text: \`Click\`});
@@ -1195,7 +1206,7 @@ exports[`t-on can bind handlers with loop variable as argument 1`] = `
let owner = context;
context = Object.create(context);
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('ul', p1, c1);
var _2 = ['someval'];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
@@ -1209,10 +1220,10 @@ exports[`t-on can bind handlers with loop variable as argument 1`] = `
context.action_index = i;
context.action = _3[i];
context.action_value = _4[i];
var c5 = [], p5 = {key:context['action_index']};
let c5 = [], p5 = {key:context['action_index']};
var vn5 = h('li', p5, c5);
c1.push(vn5);
var c6 = [], p6 = {key:6,on:{}};
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']);
@@ -1227,7 +1238,7 @@ exports[`t-on can bind handlers with object arguments 1`] = `
) {
let owner = context;
var h = this.utils.h;
var c1 = [], p1 = {key:1,on:{}};
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});
c1.push({text: \`Click\`});
@@ -1240,7 +1251,7 @@ exports[`t-on can bind two event handlers 1`] = `
) {
let owner = context;
var h = this.utils.h;
var c1 = [], p1 = {key:1,on:{}};
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);
p1.on['click'] = extra.handlers['click' + 1];
@@ -1256,7 +1267,7 @@ exports[`t-on handler is bound to proper owner 1`] = `
) {
let owner = context;
var h = this.utils.h;
var c1 = [], p1 = {key:1,on:{}};
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);
p1.on['click'] = extra.handlers['click' + 1];
@@ -1269,7 +1280,7 @@ exports[`t-raw literal 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1);
var _2 = 'ok';
if (_2 || _2 === 0) {
@@ -1288,7 +1299,7 @@ exports[`t-raw not escaping 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = context['var'];
if (_2 || _2 === 0) {
@@ -1307,9 +1318,9 @@ exports[`t-raw t-raw and another sibling node 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1);
var c2 = [], p2 = {key:2};
let c2 = [], p2 = {key:2};
var vn2 = h('span', p2, c2);
c1.push(vn2);
c2.push({text: \`hello\`});
@@ -1330,7 +1341,7 @@ exports[`t-raw variable 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1);
var _2 = context['var'];
if (_2 || _2 === 0) {
@@ -1349,15 +1360,17 @@ exports[`t-ref can get a dynamic ref on a node 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var c2 = [], p2 = {key:2};
let c2 = [], p2 = {key:2};
var vn2 = h('span', p2, c2);
c1.push(vn2);
const ref3 = 'myspan' + 3
p2.hook = {
create: (_, n) => context.refs[ref3] = n.elm,
};
create: (_, n) => {
context.refs[ref3] = n.elm;
},
};
return vn1;
}"
`;
@@ -1366,15 +1379,17 @@ exports[`t-ref can get a ref on a node 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var c2 = [], p2 = {key:2};
let c2 = [], p2 = {key:2};
var vn2 = h('span', p2, c2);
c1.push(vn2);
const ref3 = 'myspan'
p2.hook = {
create: (_, n) => context.refs[ref3] = n.elm,
};
create: (_, n) => {
context.refs[ref3] = n.elm;
},
};
return vn1;
}"
`;
@@ -1384,7 +1399,7 @@ exports[`t-ref refs in a loop 1`] = `
) {
context = Object.create(context);
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = context['items'];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
@@ -1398,12 +1413,14 @@ exports[`t-ref refs in a loop 1`] = `
context.item_index = i;
context.item = _3[i];
context.item_value = _4[i];
var c5 = [], p5 = {key:context['item']};
let c5 = [], p5 = {key:context['item']};
var vn5 = h('div', p5, c5);
c1.push(vn5);
const ref6 = context['item']
p5.hook = {
create: (_, n) => context.refs[ref6] = n.elm,
create: (_, n) => {
context.refs[ref6] = n.elm;
},
};
var _7 = context['item'];
if (_7 || _7 === 0) {
@@ -1418,7 +1435,7 @@ exports[`t-set evaluate value expression 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = 1 + 2;
if (_2 || _2 === 0) {
@@ -1432,7 +1449,7 @@ exports[`t-set evaluate value expression, part 2 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = context['somevariable'] + 2;
if (_2 || _2 === 0) {
@@ -1446,7 +1463,7 @@ exports[`t-set set from attribute literal 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = 'ok';
if (_2 || _2 === 0) {
@@ -1460,7 +1477,7 @@ exports[`t-set set from attribute lookup 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = context['value'];
if (_2 || _2 === 0) {
@@ -1483,7 +1500,7 @@ exports[`t-set set from body lookup 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = context['value'];
if (_2 || _2 === 0) {
@@ -1497,7 +1514,7 @@ exports[`t-set set from empty body 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
return vn1;
}"
@@ -1507,7 +1524,7 @@ exports[`t-set t-set and t-if 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = context['value'];
if (_2 === 'ok') {
@@ -1521,7 +1538,7 @@ exports[`t-set t-set evaluates an expression only once 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = context['value'] + ' artois';
if (_2 || _2 === 0) {
@@ -1539,7 +1556,7 @@ exports[`t-set t-set should reuse variable if possible 1`] = `
) {
context = Object.create(context);
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = 1;
var _3 = context['list'];
@@ -1554,10 +1571,10 @@ exports[`t-set t-set should reuse variable if possible 1`] = `
context.elem_index = i;
context.elem = _4[i];
context.elem_value = _5[i];
var c6 = [], p6 = {key:context['elem_index']};
let c6 = [], p6 = {key:context['elem_index']};
var vn6 = h('div', p6, c6);
c1.push(vn6);
var c7 = [], p7 = {key:7};
let c7 = [], p7 = {key:7};
var vn7 = h('span', p7, c7);
c6.push(vn7);
c7.push({text: \`v\`});
@@ -1574,7 +1591,7 @@ exports[`t-set value priority 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = 1;
if (_2 || _2 === 0) {
@@ -1588,7 +1605,7 @@ exports[`whitespace handling consecutives whitespaces are condensed into a singl
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
c1.push({text: \` abc \`});
return vn1;
@@ -1599,7 +1616,7 @@ exports[`whitespace handling nothing is done in pre tags 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('pre', p1, c1);
c1.push({text: \` \`});
return vn1;
@@ -1610,7 +1627,7 @@ exports[`whitespace handling nothing is done in pre tags 2`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('pre', p1, c1);
c1.push({text: \`
some text
@@ -1623,7 +1640,7 @@ exports[`whitespace handling nothing is done in pre tags 3`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('pre', p1, c1);
c1.push({text: \`
@@ -1636,7 +1653,7 @@ exports[`whitespace handling white space only text nodes are condensed into a si
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
c1.push({text: \` \`});
return vn1;
@@ -1647,9 +1664,9 @@ exports[`whitespace handling whitespace only text nodes with newlines are remove
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var c2 = [], p2 = {key:2};
let c2 = [], p2 = {key:2};
var vn2 = h('span', p2, c2);
c1.push(vn2);
c2.push({text: \`abc\`});
+506 -191
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -37,7 +37,7 @@ export function makeDeferred(): Deferred {
return <Deferred>def;
}
export function makeTestWEnv(): Env {
export function makeTestEnv(): Env {
return {
qweb: new QWeb()
};
+6 -4
View File
@@ -126,12 +126,12 @@ describe("observer", () => {
arr.push({ kriek: 5 });
expect(observer.rev).toBe(2);
expect(arr.__owl__.rev).toBe(2);
expect(arr[0].__owl__.rev).toBe(1);
expect(arr[0].__owl__.rev).toBe(2);
arr[0].kriek = 6;
expect(observer.rev).toBe(3);
expect(arr.__owl__.rev).toBe(2);
expect(arr[0].__owl__.rev).toBe(2);
expect(arr[0].__owl__.rev).toBe(3);
});
test("properly observe arrays in object", () => {
@@ -180,12 +180,14 @@ describe("observer", () => {
obj.a = { b: 2 };
expect(observer.rev).toBe(2);
expect(obj.__owl__.rev).toBe(2);
expect(obj.a.__owl__.rev).toBe(1);
// we start at 2 because it is the new observer rev number
expect(obj.a.__owl__.rev).toBe(2);
obj.a.b = 3;
expect(observer.rev).toBe(3);
expect(obj.__owl__.rev).toBe(2);
expect(obj.a.__owl__.rev).toBe(2);
expect(obj.a.__owl__.rev).toBe(3);
});
test("deep observe misc changes", () => {
+16 -8
View File
@@ -100,7 +100,7 @@ describe("error handling", () => {
test("loadTemplates throw if parser error", () => {
expect(() => {
qweb.loadTemplates("<templates><abc>></templates>");
qweb.addTemplates("<templates><abc>></templates>");
}).toThrow("Invalid XML in template");
});
@@ -511,6 +511,12 @@ describe("attributes", () => {
expect(result).toBe(`<div foo="42"></div>`);
});
test("format expression, other format", () => {
qweb.addTemplate("test", `<div t-attf-foo="#{value + 37}"/>`);
const result = renderToString(qweb, "test", { value: 5 });
expect(result).toBe(`<div foo="42"></div>`);
});
test("format multiple", () => {
qweb.addTemplate(
"test",
@@ -748,7 +754,6 @@ describe("foreach", () => {
expect(console.warn).toHaveBeenCalledTimes(1);
console.warn = consoleWarn;
});
});
describe("misc", () => {
@@ -982,14 +987,17 @@ describe("t-ref", () => {
});
test("refs in a loop", () => {
qweb.addTemplate("test", `
qweb.addTemplate(
"test",
`
<div>
<t t-foreach="items" t-as="item">
<div t-ref="item" t-key="item"><t t-esc="item"/></div>
</t>
</div>`);
</div>`
);
let refs: any = {};
renderToDOM(qweb, "test", { refs, items: [1,2,3] });
renderToDOM(qweb, "test", { refs, items: [1, 2, 3] });
expect(Object.keys(refs)).toEqual(["1", "2", "3"]);
});
});
@@ -1014,15 +1022,15 @@ describe("loading templates", () => {
<ul t-name="main"><t t-call="items"/></ul>
</templates>`;
qweb.loadTemplates(data);
qweb.addTemplates(data);
const result = renderToString(qweb, "main");
expect(result).toBe("<ul><li>ok</li><li>foo</li></ul>");
});
test("does not crash if string does not have templates", () => {
const data = "";
qweb.loadTemplates(data);
expect(qweb.templates).toEqual({});
qweb.addTemplates(data);
expect(Object.keys(qweb.templates)).toEqual(["default"]);
});
});
+115 -86
View File
@@ -2,7 +2,7 @@ import { Component, Env } from "../src/component";
import { connect, Store } from "../src/store";
import {
makeTestFixture,
makeTestWEnv,
makeTestEnv,
nextMicroTick,
nextTick
} from "./helpers";
@@ -176,15 +176,15 @@ describe("basic use", () => {
1: {
id: 1,
name: "bertinchamps",
tasterID: 1,
},
tasterID: 1
}
},
tasters: {
1: {
id: 1,
name: 'aaron',
name: "aaron"
}
},
}
};
const getters = {
beerTasterName({ state }, beerID) {
@@ -207,15 +207,15 @@ describe("basic use", () => {
1: {
id: 1,
name: "bertinchamps",
tasterID: 1,
},
tasterID: 1
}
},
tasters: {
1: {
id: 1,
name: 'aaron',
name: "aaron"
}
},
}
};
const getters = {
beerTasterName({ state }, beerID) {
@@ -243,15 +243,15 @@ describe("basic use", () => {
1: {
id: 1,
name: "bertinchamps",
tasterID: 1,
},
tasterID: 1
}
},
tasters: {
1: {
id: 1,
name: 'aaron',
name: "aaron"
}
},
}
};
const getters = {
beerTasterName({ state }, beerID) {
@@ -278,15 +278,15 @@ describe("basic use", () => {
return `${getters.b()}${getters.c(1)}`;
},
b() {
return 'b';
return "b";
},
c({}, i) {
return `c${i}`;
},
}
};
const store = new Store({ getters });
expect(store.getters.a()).toBe('bc1');
expect(store.getters.a()).toBe("bc1");
});
});
@@ -401,27 +401,28 @@ describe("connecting a component to store", () => {
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestWEnv();
env = makeTestEnv();
});
afterEach(() => {
fixture.remove();
});
class App extends Component<any, any, any> {
inlineTemplate = `
test("connecting a component works", async () => {
env.qweb.addTemplate(
"App",
`
<div>
<t t-foreach="props.todos" t-as="todo" t-key="todo">
<t t-widget="Todo" t-props="todo"/>
</t>
</div>`;
widgets = { Todo };
}
class Todo extends Component<any, any, any> {
inlineTemplate = `<span><t t-esc="props.msg"/></span>`;
}
test("connecting a component works", async () => {
</div>`
);
env.qweb.addTemplate("Todo", `<span><t t-esc="props.msg"/></span>`);
class App extends Component<any, any, any> {
widgets = { Todo };
}
class Todo extends Component<any, any, any> {}
const state = { todos: [] };
const mutations = {
addTodo({ state }, msg) {
@@ -456,14 +457,16 @@ describe("connecting a component to store", () => {
}
const store = new Store({ state, mutations });
class App extends Component<any, any, any> {
inlineTemplate = `
env.qweb.addTemplate(
"App",
`
<div>
<span t-foreach="props.todos" t-as="todo" t-key="todo">
<t t-esc="todo.title"/>
</span>
</div>`;
}
</div>`
);
class App extends Component<any, any, any> {}
const DeepTodoApp = connect(
mapStateToProps,
@@ -493,8 +496,8 @@ describe("connecting a component to store", () => {
test("connected child components with custom hooks", async () => {
let steps: any = [];
env.qweb.addTemplate("Child", `<div/>`);
class Child extends Component<any, any, any> {
inlineTemplate = `<div/>`;
mounted() {
steps.push("child:mounted");
}
@@ -505,11 +508,14 @@ describe("connecting a component to store", () => {
const ConnectedChild = connect(s => s)(Child);
class Parent extends Component<any, any, any> {
inlineTemplate = `
env.qweb.addTemplate(
"Parent",
`
<div>
<t t-if="state.child" t-widget="ConnectedChild"/>
</div>`;
</div>`
);
class Parent extends Component<any, any, any> {
widgets = { ConnectedChild };
constructor(env: Env) {
@@ -540,20 +546,22 @@ describe("connecting a component to store", () => {
};
const store = new Store({ state, mutations });
class TodoItem extends Component<any, any, any> {
inlineTemplate = `<span><t t-esc="props.text"/></span>`;
}
env.qweb.addTemplate("TodoItem", `<span><t t-esc="props.text"/></span>`);
class TodoItem extends Component<any, any, any> {}
const ConnectedTodo = connect((state, props) => {
const todo = state.todos.find(t => t.id === props.id);
return todo;
})(TodoItem);
class TodoList extends Component<any, any, any> {
inlineTemplate = `<div>
env.qweb.addTemplate(
"TodoList",
`<div>
<t t-foreach="props.todos" t-as="todo">
<t t-widget="ConnectedTodo" t-props="todo"/>
</t>
</div>`;
</div>`
);
class TodoList extends Component<any, any, any> {
widgets = { ConnectedTodo };
}
@@ -578,10 +586,7 @@ describe("connecting a component to store", () => {
test("connect receives store getters as third argument", async () => {
const state = {
importantID: 1,
todos: [
{ id: 1, text: "jupiler" },
{ id: 2, text: "bertinchamps" },
],
todos: [{ id: 1, text: "jupiler" }, { id: 2, text: "bertinchamps" }]
};
const getters = {
importantTodoText({ state }) {
@@ -589,30 +594,35 @@ describe("connecting a component to store", () => {
},
text({ state }, id) {
return state.todos.find(todo => todo.id === id).text;
},
}
};
const store = new Store({ state, getters });
class TodoItem extends Component<any, any, any> {
inlineTemplate = `<div>
env.qweb.addTemplate(
"TodoItem",
`<div>
<span><t t-esc="props.activeTodoText"/></span>
<span><t t-esc="props.importantTodoText"/></span>
</div>`;
}
</div>`
);
class TodoItem extends Component<any, any, any> {}
const ConnectedTodo = connect((state, props, getters) => {
const todo = state.todos.find(t => t.id === props.id);
return {
activeTodoText: getters.text(todo.id),
importantTodoText: getters.importantTodoText(),
importantTodoText: getters.importantTodoText()
};
})(TodoItem);
class TodoList extends Component<any, any, any> {
inlineTemplate = `<div>
env.qweb.addTemplate(
"TodoList",
`<div>
<t t-foreach="props.todos" t-as="todo">
<t t-widget="ConnectedTodo" t-props="todo"/>
</t>
</div>`;
</div>`
);
class TodoList extends Component<any, any, any> {
widgets = { ConnectedTodo };
}
@@ -625,21 +635,25 @@ describe("connecting a component to store", () => {
const app = new ConnectedTodoList(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><span>jupiler</span><span>jupiler</span></div><div><span>bertinchamps</span><span>jupiler</span></div></div>");
expect(fixture.innerHTML).toBe(
"<div><div><span>jupiler</span><span>jupiler</span></div><div><span>bertinchamps</span><span>jupiler</span></div></div>"
);
});
test("connected component is updated when props are updated", async () => {
class Beer extends Component<any, any, any> {
inlineTemplate = `<span><t t-esc="props.name"/></span>`;
}
env.qweb.addTemplate("Beer", `<span><t t-esc="props.name"/></span>`);
class Beer extends Component<any, any, any> {}
const ConnectedBeer = connect((state, props) => {
return state.beers[props.id];
})(Beer);
env.qweb.addTemplate(
"App",
`<div>
<t t-widget="ConnectedBeer" t-props="{id: state.beerId}"/>
</div>`
);
class App extends Component<any, any, any> {
inlineTemplate = `<div>
<t t-widget="ConnectedBeer" t-props="{id: state.beerId}"/>
</div>`;
widgets = { ConnectedBeer };
state = { beerId: 1 };
}
@@ -658,12 +672,14 @@ describe("connecting a component to store", () => {
});
test("connected component is updated when store is changed", async () => {
class App extends Component<any, any, any> {
inlineTemplate = `
env.qweb.addTemplate(
"App",
`
<div>
<span t-foreach="props.beers" t-as="beer" t-key="beer.name"><t t-esc="beer.name"/></span>
</div>`;
}
</div>`
);
class App extends Component<any, any, any> {}
const mutations = {
addBeer({ state }, name) {
@@ -692,13 +708,15 @@ describe("connecting a component to store", () => {
});
test("connected component with undefined, null and string props", async () => {
class Beer extends Component<any, any, any> {
inlineTemplate = `<div>
env.qweb.addTemplate(
"Beer",
`<div>
<span>taster:<t t-esc="props.taster"/></span>
<span t-if="props.selected">selected:<t t-esc="props.selected.name"/></span>
<span t-if="props.consumed">consumed:<t t-esc="props.consumed.name"/></span>
</div>`;
}
</div>`
);
class Beer extends Component<any, any, any> {}
const ConnectedBeer = connect((state, props) => {
return {
selected: state.beers[props.id],
@@ -707,10 +725,13 @@ describe("connecting a component to store", () => {
};
})(Beer);
class App extends Component<any, any, any> {
inlineTemplate = `<div>
env.qweb.addTemplate(
"App",
`<div>
<t t-widget="ConnectedBeer" t-props="{id: state.beerId}"/>
</div>`;
</div>`
);
class App extends Component<any, any, any> {
widgets = { ConnectedBeer };
state = { beerId: 0 };
}
@@ -756,13 +777,15 @@ describe("connecting a component to store", () => {
});
test("connected component deeply reactive with undefined, null and string props", async () => {
class Beer extends Component<any, any, any> {
inlineTemplate = `<div>
env.qweb.addTemplate(
"Beer",
`<div>
<span>taster:<t t-esc="props.taster"/></span>
<span t-if="props.selected">selected:<t t-esc="props.selected.name"/></span>
<span t-if="props.consumed">consumed:<t t-esc="props.consumed.name"/></span>
</div>`;
}
</div>`
);
class Beer extends Component<any, any, any> {}
const ConnectedBeer = connect((state, props) => {
return {
selected: state.beers[props.id],
@@ -771,10 +794,13 @@ describe("connecting a component to store", () => {
};
})(Beer);
class App extends Component<any, any, any> {
inlineTemplate = `<div>
env.qweb.addTemplate(
"App",
`<div>
<t t-widget="ConnectedBeer" t-props="{id: state.beerId}"/>
</div>`;
</div>`
);
class App extends Component<any, any, any> {
widgets = { ConnectedBeer };
state = { beerId: 0 };
}
@@ -845,12 +871,16 @@ describe("connecting a component to store", () => {
test("correct update order when parent/children are connected", async () => {
const steps: string[] = [];
class Parent extends Component<any, any, any> {
inlineTemplate = `
env.qweb.addTemplate(
"Parent",
`
<div>
<t t-widget="Child" t-ref="'child'" t-props="{key: props.current}"/>
</div>
`;
`
);
class Parent extends Component<any, any, any> {
widgets = { Child: ConnectedChild };
}
const ConnectedParent = connect(function(s) {
@@ -858,9 +888,8 @@ describe("connecting a component to store", () => {
return { current: s.current, isvisible: s.isvisible };
})(Parent);
class Child extends Component<any, any, any> {
inlineTemplate = `<span><t t-esc="props.msg"/></span>`;
}
env.qweb.addTemplate("Child", `<span><t t-esc="props.msg"/></span>`);
class Child extends Component<any, any, any> {}
const ConnectedChild = connect(function(s, props) {
steps.push("child");
@@ -889,8 +918,8 @@ describe("connecting a component to store", () => {
test("connected component willpatch/patch hooks are called on store updates", async () => {
const steps: string[] = [];
env.qweb.addTemplate("App", `<div><t t-esc="props.msg"/></div>`);
class App extends Component<any, any, any> {
inlineTemplate = `<div><t t-esc="props.msg"/></div>`;
willPatch() {
steps.push("willpatch");
}
+1 -80
View File
@@ -1,10 +1,4 @@
import {
escape,
memoize,
debounce,
patch,
unpatch
} from "../src/utils";
import { escape, debounce } from "../src/utils";
describe("escape", () => {
test("normal strings", () => {
@@ -18,26 +12,6 @@ describe("escape", () => {
});
});
describe("memoize", () => {
test("return correct value", () => {
const f = memoize((a, b) => a + b);
expect(f(1, 3)).toBe(4);
});
test("does not recompute if not needed", () => {
let nCalls = 0;
function origFunction(a: number, b: number): number {
nCalls++;
return a + b;
}
const memoized = memoize(origFunction);
expect(memoized(1, 3)).toBe(4);
expect(memoized(1, 3)).toBe(4);
expect(nCalls).toBe(1);
});
});
describe("debounce", () => {
test("works as expected", () => {
jest.useFakeTimers();
@@ -54,56 +28,3 @@ describe("debounce", () => {
expect(n).toBe(1);
});
});
describe("patch/unpatch", () => {
test("can monkey patch a class", () => {
class Test {
n = 1;
doSomething(): string {
return "hey";
}
}
patch(Test, "some_custo", {
doSomething(): string {
this.n = this.n + 1;
return this._super();
}
});
const t = new Test();
expect(t.n).toBe(1);
expect(t.doSomething()).toBe("hey");
expect(t.n).toBe(2);
});
test("cannot patch a class twice with same patch name", () => {
class Test {}
patch(Test, "some_custo", {});
expect(() => {
patch(Test, "some_custo", {});
}).toThrow();
});
test("can unpatch a class", () => {
class Test {
doSomething(): number {
return 1;
}
}
patch(Test, "some_custo", {
doSomething(): number {
return this._super() + 2;
}
});
const t = new Test();
expect(t.doSomething()).toBe(3);
unpatch(Test, "some_custo");
expect(t.doSomething()).toBe(1);
});
});