Compare commits

..

84 Commits

Author SHA1 Message Date
Géry Debongnie 7d3c374b78 [REL] bump to v0.13.0 2019-06-05 10:47:39 +02:00
Géry Debongnie 1dd257f33c [DOC] owl: add comment on updating the doc 2019-06-05 10:44:58 +02:00
Vincent Schippefilt 1206afe6be [FIX] base: compile correctly on windows
Fix package.json and tsconfig.json to build correctly on windows:
the extention scr/* doesn't expand on windows, so I removed it.

Added a precompilation step on npm run extras:watch
2019-06-05 10:31:18 +02:00
Vincent Schippefilt 5842ed51b0 [IMP] extras: update server to python3, disable cache 2019-06-05 10:31:18 +02:00
Géry Debongnie d22219f084 [DOC] reorganize qweb/component doc
Also, document t-foreach

closes #146
2019-06-05 10:28:30 +02:00
Aaron Bohy 0be1a54e03 [IMP] playground: use custom events bubbling 2019-06-04 11:59:21 +02:00
Aaron Bohy 32921ecc20 [IMP] qweb: t-on with t-widget: allow to bind args 2019-06-04 11:59:21 +02:00
Aaron Bohy 1c290c1172 [IMP] qweb: handle t-on modifiers with t-widget 2019-06-04 11:59:21 +02:00
Aaron Bohy 2f9d7ea58f [REF] qweb: revamp event management
Closes #111
2019-06-04 11:59:21 +02:00
Géry Debongnie fe23e76341 [FIX] observer: allow reobserving values twice
closes #139
2019-06-04 11:18:12 +02:00
Géry Debongnie 73023d4869 [IMP] extras: add url in log when starting server 2019-06-04 10:25:50 +02:00
Géry Debongnie ff51443b8e [FIX] playground: keep ace editor alive between mounting/remounting
closes #136
2019-06-04 09:43:58 +02:00
Géry Debongnie f8e07f6d30 [DOC] *: remove all remaining references to t-props
closes #135
2019-06-03 13:34:01 +02:00
Vincent Schippefilt 00da778753 [FIX] extras remove references to t-props 2019-06-03 13:34:01 +02:00
Géry Debongnie 187eb922c0 [IMP] component: define props as attribute (remove t-props) 2019-06-03 13:34:01 +02:00
Géry Debongnie 466c12a0e6 [FIX] component: reuse widget if possible
Previous code always destroy and recreate widgets.
2019-06-03 13:34:01 +02:00
Géry Debongnie 8dc3ec94bf [IMP] qweb: log invalid code generated by template 2019-05-31 13:56:45 +02:00
Géry Debongnie a554436d03 [FIX] playground: properly resize editors when resizing panels 2019-05-31 13:11:08 +02:00
Géry Debongnie 34b5049dee [DOC] doc: small misc fixes 2019-05-31 12:40:02 +02:00
Géry Debongnie 0eccfd08fe [REF] component: add some docstrings 2019-05-31 10:26:37 +02:00
Vincent Schippefilt 98b063fd70 [FIX] owl: ignore idea ide configuration 2019-05-31 09:51:37 +02:00
Géry Debongnie e9ba94ae55 [IMP] playground: add some padding in error window
closes #134
2019-05-29 16:13:29 +02:00
Aaron Bohy 4d72a4f240 [FIX] qweb: enclose interpolated expr in parentheses
Since rev 53e53e9b,
 <div class="o_app" t-attf-class="{{ true ? 'o_extra' : '' }}"/>
produced
 <div class="o_extra"/>
whereas it should have produced
 <div class="o_app o_extra"/>

This is because there were missing parentheses around the string
returned by `interpolate`, which produced instructions like

 var _1 = 'o_app ' + true ? 'o_extra' : '';

Fixes #132
2019-05-29 13:31:48 +02:00
Géry Debongnie a08deb895d [ADD] benchmarks: add 0.12.0 to the list 2019-05-29 13:24:58 +02:00
Géry Debongnie 60cea14b01 [IMP] extras: improve landing page 2019-05-29 13:16:35 +02:00
Géry Debongnie f60904b740 [REL] bump to v0.12.0 2019-05-29 11:26:21 +02:00
Aaron Bohy 376f801541 [IMP] qweb: t-widget: accept dyn values 2019-05-29 11:22:12 +02:00
Aaron Bohy 26f14180e2 [IMP] qweb: t-ref: dyn values with string interpolation
Closes #100
2019-05-29 11:22:12 +02:00
Géry Debongnie bced9f3d04 [IMP] *: use lower level loops in hot code
The goal is to gain a few % of perf improvement.

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

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

closes #117
2019-05-21 14:11:40 +02:00
Géry Debongnie 8f29cfc996 [FIX] playground: add missing t-key in template 2019-05-21 14:11:20 +02:00
Géry Debongnie da275e74fd [REF] owl: move metadata into __info__
This differentiate better metadata and owl exported values.  Also, and
this is the goal, it gives a space to add extra keys in the future.
2019-05-21 13:53:16 +02:00
Géry Debongnie 3d9f4e028c [DOC] utils: remove patch/unpatch references 2019-05-19 08:21:22 +02:00
Géry Debongnie 13577997bc [ADD] extras: add owl 0.11.0 to list of benchmarks 2019-05-17 23:46:42 +02:00
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
55 changed files with 14190 additions and 2178 deletions
+5
View File
@@ -1,5 +1,6 @@
/node_modules /node_modules
/dist /dist
npm-debug.log npm-debug.log
# misc # misc
@@ -14,7 +15,11 @@ yarn-debug.log*
yarn-error.log* yarn-error.log*
package-lock.json package-lock.json
#ide's
.vscode .vscode
.idea
node_modules node_modules
# Extras temp file # Extras temp file
+20 -16
View File
@@ -36,11 +36,6 @@ Here is a short example to illustrate interactive widgets:
```javascript ```javascript
class ClickCounter extends owl.Component { class ClickCounter extends owl.Component {
inlineTemplate = `
<button t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>`;
state = { value: 0 }; state = { value: 0 };
increment() { 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 }); const counter = new ClickCounter({ qweb });
counter.mount(document.body); 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: 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.13.0.js](https://github.com/odoo/owl/releases/download/v0.13.0/owl.js)
- [owl-0.10.0.min.js](https://github.com/odoo/owl/releases/download/v0.10.0/owl.min.js) - [owl-0.13.0.min.js](https://github.com/odoo/owl/releases/download/v0.13.0/owl.min.js)
Some npm scripts are available: Some npm scripts are available:
| Command | Description | | Command | Description |
| -------------------- | ------------------------------------------------------------------------------- | | ---------------------- | ------------------------------------------------------------------------------- |
| `npm install` | install every dependency required for this project | | `npm install` | install every dependency required for this project |
| `npm run build` | build a bundle of _owl_ in the _/dist/_ folder | | `npm run build` | build a bundle of _owl_ in the _/dist/_ folder |
| `npm run minify` | minify the prebuilt owl.js file | | `npm run minify` | minify the prebuilt owl.js file |
| `npm run test` | run all tests | | `npm run test` | run all tests |
| `npm run test:watch` | run all tests, and keep a watcher | | `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` | 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 ## Documentation
@@ -81,6 +82,9 @@ The complete documentation can be found [here](doc/readme.md). The most importan
- [Component](doc/component.md) - [Component](doc/component.md)
- [QWeb](doc/qweb.md) - [QWeb](doc/qweb.md)
Found an issue in the documentation? A broken link? Some outdated information?
Submit a PR!
## License ## License
OWL is [GPL licensed](./LICENSE). OWL is [GPL licensed](./LICENSE).
+64 -8
View File
@@ -1,23 +1,25 @@
# 🦉 Animations 🦉 # 🦉 Animations 🦉
Animation is a complex topic. There are many different use cases, and many Animation is a complex topic. There are many different use cases, and many
solutions and technologies. solutions and technologies. Owl only supports some basic use cases.
## Simple CSS effects ## Simple CSS effects
Sometimes, using pure CSS is enough. For these use cases, Owl is not really Sometimes, using pure CSS is enough. For these use cases, Owl is not really
necessary: it just needs to render a DOM element with a specific class. For necessary: it just needs to render a DOM element with a specific class. For
example: example:
```xml ```xml
<button class="flash" t-on-click="doSomething">Click</button> <a class="btn flash" t-on-click="doSomething">Click</a>
``` ```
with the following CSS: with the following CSS:
```css ```css
btn {
background-color: gray;
}
.flash { .flash {
transition: background 0.5s; transition: background 0.5s;
} }
@@ -31,9 +33,63 @@ with the following CSS:
will produce a nice flash effect whenever the user click (or activate with the will produce a nice flash effect whenever the user click (or activate with the
keyboard) the button. keyboard) the button.
## CSS Transitions (single element) ## CSS Transitions
A more complex situation occurs when we want to transition an element in or out A more complex situation occurs when we want to transition an element in or out
of the page. For example, we may want a fade-in and fade-out effect. of the page. For example, we may want a fade-in and fade-out effect.
The `t-transition` directive is here to help us (see [QWeb documentation](doc/qweb.md#t-transition-directive)). The `t-transition` directive is here to help us (see [QWeb documentation](qweb.md#t-transition-directive)).
To perform useful transition effects, whenever an element appears or disappears,
it is necessary to add/remove some css style or class at some precise moment in
the lifetime of a node. Since this is not easy to do by hand, Owl `t-transition`
directive is there to help.
Whenever a node has a `t-transition` directive, with a `name` value, the following
will happen:
At node insertion:
- the css classes `name-enter` and `name-enter-active` will be added directly
when the node is inserted into the DOM,
- on the next animation frame: the css class `name-enter` will be removed and the
class `name-enter-to` will be added (so they can be used to trigger css
transition effects),
- the css class `name-enter-active` will be removed whenever a css transition
ends.
At node destruction:
- the css classes `name-leave` and `name-leave-active` will be added before the
node is removed to the DOM,
- the css class `name-leave` will be removed on the next animation frame (so it
can be used to trigger css transition effects),
- the css class `name-leave-active` will be removed whenever a css transition
ends. Only then will the element be removed from the DOM.
For example, a simple fade in/out effect can be done with this:
```xml
<div>
<div t-if="state.flag" class="square" t-transition="fade">Hello</div>
</div>
```
```css
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
```
The `t-transition` directive can be combined with `t-widget`.
Notes:
- more information on animations are available [here](animations.md).
- Owl does not support more than one transition on a single node, so the
`t-transition` expression must be a single value (i.e. no space allowed)
+425 -64
View File
@@ -4,12 +4,18 @@
- [Overview](#overview) - [Overview](#overview)
- [Example](#example) - [Example](#example)
- [Composition](#composition)
- [Reference](#reference) - [Reference](#reference)
- [Properties](#properties) - [Properties](#properties)
- [Static Properties](#static-properties)
- [Methods](#methods) - [Methods](#methods)
- [Lifecycle](#lifecycle) - [Lifecycle](#lifecycle)
- [Semantics](#semantics) - [Composition](#composition)
- [Event Handling](#event-handling)
- [`t-key` directive](#t-key-directive)
- [`t-mounted` directive](#t-mounted-directive)
- [Props Validation](#props-validation)
- [Keeping References](#keeping-references])
- [Asynchronous rendering](#asynchronous-rendering)
## Overview ## Overview
@@ -29,9 +35,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. and follow the QWeb specification. This is a requirement for Odoo.
OWL components are defined as a subclass of Component. The rendering is OWL components are defined as a subclass of Component. The rendering is
exclusively done by a [QWeb](qweb.md) template (either defined inline or preloaded in QWeb). exclusively done by a [QWeb](qweb.md) template (which needs to be preloaded in QWeb).
The rendering is done by QWeb, which will generate a virtual dom representation Rendering a component generates a virtual dom representation
of the widget, then patch the DOM to apply the changes in an efficient way. 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 OWL components observe their states, and rerender themselves whenever it is
changed. This is done by an [observer](observer.md). changed. This is done by an [observer](observer.md).
@@ -42,7 +48,6 @@ Let us have a look at a simple component:
```javascript ```javascript
class ClickCounter extends owl.Component { class ClickCounter extends owl.Component {
template = "click-counter";
state = { value: 0 }; state = { value: 0 };
increment() { increment() {
@@ -52,7 +57,7 @@ class ClickCounter extends owl.Component {
``` ```
```xml ```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"/>] Click Me! [<t t-esc="state.value"/>]
</button> </button>
``` ```
@@ -61,61 +66,12 @@ Note that this code is written in ESNext style, so it will only run on the
latest browsers without a transpilation step. latest browsers without a transpilation step.
This example show how a component should be defined: it simply subclasses the This example show how a component should be defined: it simply subclasses the
Component class. 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 a state object is defined. It is not mandatory to use the state object, but it
is certainly encouraged. The state object is [observed](observer.md), and any is certainly encouraged. The state object is [observed](observer.md), and any
change to it will cause a rerendering. change to it will cause a rerendering.
## Composition
The example above shows a QWeb template with a `t-on-click` directive. Widget
templates are standard [QWeb](qweb.md) templates, but with an extra directive:
`t-widget`. With the `t-widget` directive, widget templates can declare sub
widgets:
```xml
<div t-name="parent">
<span>some text</span>
<t t-widget="MyWidget" t-props="{info: 13}"/>
</div>
```
```js
class ParentWidget extends owl.Component {
template = 'parent';
widgets = { MyWidget: MyWidget};
...
}
```
In this example, the `ParentWidget`'s template creates a widget `MyWidget` just
after the span. See the [QWeb](qweb.md) documentation for more information on the
`t-widget` directive.
Note that the rendering context for the template is the widget itself. This means
that the template can access `state`, `props`, `env`, or any methods defined in the widget.
**CSS and style:** there is some specific support to allow the parent to declare
additional css classes or style for the sub widget: css declared in `class`, `style`, `t-att-class` or `t-att-style` will be added to the
root widget element.
```xml
<div t-name="parent">
<t t-widget="MyWidget" class="someClass" style="font-weight:bold;" t-props="{info: 13}"/>
</div>
```
Warning: there is a small caveat with dynamic class attributes: since Owl needs
to be able to add/remove proper classes whenever necessary, it needs to be aware
of the possible classes. Otherwise, it will not be able to make the difference
between a valid css class added by the component, or some custom code, and a
class that need to be removed. This is why we only support the explicit syntax
with a class object:
```js
<t t-widget="MyWidget" t-att-class="{a: state.flagA, b: state.flagB}" />
```
## Reference ## Reference
An Owl component is a small class which represent a widget or some UI element. An Owl component is a small class which represent a widget or some UI element.
@@ -123,6 +79,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 parent to its children. The environment needs to have a QWeb instance, which
will be used to render the component template. will be used to render the component template.
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 ### Properties
- **`el`** (HTMLElement | null): reference to the DOM root node of the element. It is `null` when the - **`el`** (HTMLElement | null): reference to the DOM root node of the element. It is `null` when the
@@ -130,12 +90,9 @@ will be used to render the component template.
- **`env`** (Object): the component environment, which contains a QWeb instance. - **`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. 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 - **`state`** (Object): this is the location of the component's state, if there is
any. After the willStart method, the `state` property is observed, and each any. After the willStart method, the `state` property is observed, and each
change will cause the widget to rerender itself. change will cause the widget to rerender itself.
@@ -143,11 +100,22 @@ will be used to render the component template.
- **`props`** (Object): this is an object given (in the constructor) by the parent - **`props`** (Object): this is an object given (in the constructor) by the parent
to configure the component. It can be dynamically changed later by the parent, to configure the component. It can be dynamically changed later by the parent,
in some case. Note that `props` are owned by the parent, not by the component. in some case. Note that `props` are owned by the parent, not by the component.
As such, it should not ever be modified by the component. As such, it should not ever be modified by the component!!
- **`refs`** (Object): the `refs` object contains all references to sub DOM nodes - **`refs`** (Object): the `refs` object contains all references to sub DOM nodes
or sub widgets defined by a `t-ref` directive in the component's template. or sub widgets defined by a `t-ref` directive in the component's template.
### Static Properties
- **`props`** (Object, optional): if given, this is an object that describes the
type and shape of the (actual) props given to the component. If Owl mode is
`dev`, this will be used to validate the props each time the component is
created/updated. See [Props Validation](#props-validation) for more information.
- **`defaultProps`** (Object, optional): if given, this object define default
values for (top-level) props. Whenever `props` are given to the object, they
will be altered to add default value (if missing). Note that it does not
change the initial object, a new object will be created instead.
### Methods ### Methods
- **`mount(target)`** (async): this is the main way a component's hierarchy is added to the - **`mount(target)`** (async): this is the main way a component's hierarchy is added to the
@@ -235,7 +203,6 @@ implemented in most cases:
```javascript ```javascript
class ClickCounter extends owl.Component { class ClickCounter extends owl.Component {
template = "click-counter";
state = { value: 0 }; state = { value: 0 };
... ...
@@ -345,6 +312,241 @@ the DOM. This is a good place to remove some listeners, for example.
This is the opposite method of `mounted`. This is the opposite method of `mounted`.
## Composition
The example above shows a QWeb template with a `t-on-click` directive. Widget
templates are standard [QWeb](qweb.md) templates, but with an extra directive:
`t-widget`. With the `t-widget` directive, widget templates can declare sub
widgets:
```xml
<div t-name="ParentWidget">
<span>some text</span>
<t t-widget="MyWidget" info="13"/>
</div>
```
```js
class ParentWidget extends owl.Component {
widgets = { MyWidget: MyWidget};
...
}
```
In this example, the `ParentWidget`'s template creates a widget `MyWidget` just
after the span. The `info` key will be added to the subwidget's props. Each
props is a string which represents a javascript (QWeb) expression, so it is
dynamic. If it is necessary to give a string, this can be done by quoting it:
`someString="'somevalue'"`. See the
[QWeb](qweb.md) documentation for more information on the `t-widget` directive.
Note that the rendering context for the template is the widget itself. This means
that the template can access `state`, `props`, `env`, or any methods defined in the widget.
The `t-widget` directive is the key to a declarative component
system. It allows a template to define where and how a sub widget is created
and/or updated. For example:
```xml
<div t-name="ParentWidget">
<t t-widget="ChildWidget" count="state.val"/>
</div>
```
```js
class ParentWidget {
widgets = { ChildWidget };
state = { val: 4 };
}
```
Whenever the template is rendered, it will automatically create the subwidget
`ChildWidget` at the correct place. It needs to find the reference to the
actual component class in the special `widgets` key, or the class registered in
QWeb's global registry (see `register` function of QWeb). It first looks inside
the local `widgets` key, then fallbacks on the global registry.
_Props_: In this example, the child widget will receive the object `{count: 4}` in its
constructor. This will be assigned to the `props` variable, which can be accessed
on the widget (and also, in the template). Whenever the state is updated, then
the subwidget will also be updated automatically.
Note that there are some restrictions on prop names: `class`, `style` and any
string which starts with `t-` are not allowed.
The `t-widget` directive also accepts dynamic values with string interpolation
(like the [`t-attf-`](#dynamic-attributes-t-att-and-t-attf-directives) directive):
```xml
<div t-name="ParentWidget">
<t t-widget="ChildWidget#{id}"/>
</div>
```
```js
class ParentWidget {
widgets = { ChildWidget1, ChildWidget2 };
state = { id: 1 };
}
```
Similarly to `t-attf-`, there is an alternate form of string interpolation:
```xml
<div t-name="ParentWidget">
<t t-widget="ChildWidget{{id}}"/>
</div>
```
**CSS and style:** there is some specific support to allow the parent to declare
additional css classes or style for the sub widget: css declared in `class`, `style`, `t-att-class` or `t-att-style` will be added to the
root widget element.
```xml
<div t-name="ParentWidget">
<t t-widget="MyWidget" class="someClass" style="font-weight:bold;" info="13"/>
</div>
```
Warning: there is a small caveat with dynamic class attributes: since Owl needs
to be able to add/remove proper classes whenever necessary, it needs to be aware
of the possible classes. Otherwise, it will not be able to make the difference
between a valid css class added by the component, or some custom code, and a
class that need to be removed. This is why we only support the explicit syntax
with a class object:
```xml
<t t-widget="MyWidget" t-att-class="{a: state.flagA, b: state.flagB}" />
```
### Event handling
In a component's template, it is useful to be able to register handlers on some
elements to some specific events. This is what makes a template _alive_. There
are four different use cases.
1. Register an event handler on a DOM node (_pure_ DOM event)
2. Register an event handler on a component (_pure_ DOM event)
3. Register an event handler on a DOM node (_business_ DOM event)
4. Register an event handler on a component (_business_ DOM event)
A _pure_ DOM event is directly triggered by a user interaction (e.g. a `click`).
```xml
<button t-on-click="someMethod">Do something</button>
```
This will be roughly translated in javascript like this:
```js
button.addEventListener("click", widget.someMethod.bind(widget));
```
The suffix (`click` in this example) is simply the name of the actual DOM
event.
A _business_ DOM event is triggered by a call to `trigger` on a component.
```xml
<t t-widget="MyWidget" t-on-menu-loaded="someMethod"/>
```
```js
class MyWidget {
someWhere() {
const payload = ...;
this.trigger('menu-loaded', payload);
}
}
```
The call to `trigger` generates a [_CustomEvent_](https://developer.mozilla.org/docs/Web/Guide/Events/Creating_and_triggering_events)
of type `menu-loaded` and dispatches it on the component's DOM element
(`this.el`). The event bubbles and is cancelable. The parent widget listening
to event `menu-loaded` will receive the payload in its `someMethod` handler
(in the `detail` property of the event), whenever the event is triggered.
```js
class ParentWidget {
someMethod(ev) {
const payload = ev.detail;
...
}
}
```
By convention, we use KebabCase for the name of _business_ events.
In order to remove the DOM event details from the event handlers (like calls to
`event.preventDefault`) and let them focus on data logic, _modifiers_ can be
specified as additional suffixes of the `t-on` directive.
| Modifier | Description |
| ---------- | ----------------------------------------------------------------- |
| `.stop` | calls `event.stopPropagation()` before calling the method |
| `.prevent` | calls `event.preventDefault()` before calling the method |
| `.self` | calls the method only if the `event.target` is the element itself |
```xml
<button t-on-click.stop="someMethod">Do something</button>
```
Note that modifiers can be combined (ex: `t-on-click.stop.prevent`), and that
the order may matter. For instance `t-on-click.prevent.self` will prevent all
clicks while `t-on-click.self.prevent` will only prevent clicks on the element
itself.
The `t-on` directive also allows to prebind some arguments. For example,
```xml
<button t-on-click="someMethod(expr)">Do something</button>
```
Here, `expr` is a valid Owl expression, so it could be `true` or some variable
from the rendering context.
### `t-key` directive
Even though Owl tries to be as declarative as possible, some DOM state is still
locked inside the DOM: for example, the scrolling state, the current user selection,
the focused element or the state of an input. This is why we use a virtual dom
algorithm to keep the actual DOM node as much as possible. However, this is
sometimes not enough, and we need to help Owl decide if an element is actually
the same, or is different. The `t-key` directive is used to give an identity to an element.
There are three main use cases:
- _elements in a list_:
```xml
<span t-foreach="todos" t-as="todo" t-key="todo.id">
<t t-esc="todo.text"/>
</span>
```
- _`t-if`/`t-else`_
- _animations_: give a different identity to a component. Ex: thread id with
animations on add/remove message.
### `t-mounted` directive
The `t-mounted` directive allows to register a callback to execute whenever the node
is inserted into the DOM.
```xml
<div><input t-ref="someInput" t-mounted="focusMe"/></div>
```
```js
class MyWidget extends owl.Component {
...
focusMe() {
this.refs.someInput.focus();
}
}
```
### Semantics ### Semantics
We give here an informal description of the way components are created/updated We give here an informal description of the way components are created/updated
@@ -431,3 +633,162 @@ Here is what Owl will do:
5. patching of `D` 5. patching of `D`
6. `patched` hooks are called on `D`, `C` 6. `patched` hooks are called on `D`, `C`
### Props Validation
As an application becomes complex, it may be quite unsafe to define props in an informal way. This leads to two issues:
- hard to tell how a component should be used, by looking at its code.
- unsafe, it is easy to send wrong props into a component, either by refactoring a component, or one of its parent.
A props type system would solve both issues, by describing the types and shapes
of the props. Here is how it works in Owl:
- `props` key is a static key (so, different from `this.props` in a component instance)
- it is optional: it is ok for a component to not define a `props` key.
- props are validated whenever a component is created/updated
- props are only validated in `dev` mode (see [tooling page](tooling.md#development-mode))
- if a key does not match the description, an error is thrown
- it only validates keys defined in (static) `props`. Additional keys in (component) `props` are not validated.
For example:
```js
class ComponentA extends owl.Component {
static props = ['id', 'url'];
...
}
class ComponentB extends owl.Component {
static props = {
count: {type: Number},
messages: {
type: Array,
element: {type: Object, shape: {id: Boolean, text: 'string' }
},
date: Date,
combinedVal: [Number, Boolean]
};
...
}
```
- it is an object or a list of strings
- a list of strings is a simplified props definition, which only lists the name
of the props.
- all props are by default required, unless they are defined with `optional: true`
(in that case, validation is only done if there is a value)
- valid types are: `Number, String, Boolean, Object, Array, Date, Function`, and all
constructor functions (so, if you have a `Person` class, it can be used as a type)
- arrays are homogeneous (all elements have the same type/shape)
For each key, a `prop` definition is either a constructor, a list of constructors, or an object:
- a constructor: this should describe the type, for example: `id: Number` describe
the props `id` as a number
- a list of constructors. In that case, this means that we allow more than one
type. For example, `id: [Number, String]` means that `id` can be either a string
or a number.
- an object. This makes it possible to have more expressive definition. The following sub keys are then allowed:
- `type`: the main type of the prop being validated
- `element`: if the type was `Array`, then the `element` key describes the type of each element in the array. It is optional (not set means that we only validate the array, not its elements),
- `shape`: if the type was `Object`, then the `shape` key describes the interface of the object. It is optional (not set means that we only validate the object, not its elements)
Examples:
```js
// only the existence of those 3 keys is documented
static props = ['message', 'id', 'date'];
```
```js
static props = {
messageIds: {type: Array, element: Number}, // list of number
otherArr: {type: Array}, // just array. no validation is made on sub elements
otherArr2: Array, // same as otherArr
someObj: {type: Object}, // just an object, no internal validation
someObj2: {
type: Object,
shape: {
id: Number,
name: {type: String, optional: true},
url: String
]}, // object, with keys id (number), name (string, optional) and url (string)
someFlag: Boolean, // a boolean, mandatory (even if `false`)
someVal: [Boolean, Date] // either a boolean or a date
};
```
### Keeping references
The `t-ref` directive helps a component keep reference to some inside part of it.
Like the `t-on` directive, it can work either on a DOM node, or on a component:
```xml
<div>
<div t-ref="someDiv"/>
<t t-widget="SubWidget" t-ref="someWidget"/>
</div>
```
In this example, the widget will be able to access the `div` and the component
inside the special `refs` variable:
```js
this.refs.someDiv;
this.refs.someWidget;
```
This is useful for various usecases: for example, integrating with an external
library that needs to render itself inside an actual DOM node. Or for calling
some method on a sub widget.
Note: if used on a component, the reference will be set in the `refs`
variable between `willPatch` and `patched`.
The `t-ref` directive also accepts dynamic values with string interpolation
(like the [`t-attf-`](#dynamic-attributes-t-att-and-t-attf-directives) and
[`t-widget-`](#component-t-widget) directives). For example, if we have
`id` set to 44 in the rendering context,
```xml
<div t-ref="widget_#{id}"/>
```
```js
this.refs.widget_44;
```
Similarly to `t-attf-` and `t-widget`, there is an alternate form of string
interpolation:
```xml
<div t-ref="widget_{{id}}"/>
```
### 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`)
+298 -200
View File
@@ -3,46 +3,222 @@
## Content ## Content
- [Overview](#overview) - [Overview](#overview)
- [QWeb Specification](#qweb-specification) - [Directives](#directives)
- [Static html nodes](#static-html-nodes) - [QWeb Engine](#qweb-engine)
- [`t-esc` directive](#t-esc-directive) - [Reference](#qweb-specification)
- [`t-raw` directive](#t-raw-directive) - [White spaces](#white-spaces)
- [`t-set` directive](#t-set-directive) - [Root nodes](#root-nodes)
- [`t-if` directive](#t-if-directive) - [Expression evaluation](#expression-evaluation)
- [Expression evaluation](#expression-evaluation) - [Static html nodes](#static-html-nodes)
- [`t-att` directive (dynamic attributes)](#t-att-directive-dynamic-attributes) - [Outputting data](#outputting-data)
- [`t-call` directive (sub templates)](#t-call-directive-sub-templates) - [Setting Variables](#setting-variables)
- [Conditionals](#conditionals)
- [JS/OWL Specific Extensions](#jsowl-specific-extensions) - [Dynamic attributes](#dynamic-attributes)
- [`t-on` directive](#t-on-directive) - [Loops](#loops)
- [Component: `t-widget`, `t-props`](#component-t-widget-t-props) - [Rendering Sub Templates](#rendering-sub-templates)
- [`t-ref` directive](#t-ref-directive) - [Debugging](#debugging)
- [`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)
## Overview ## Overview
[QWeb](https://www.odoo.com/documentation/12.0/reference/qweb.html) is the primary templating engine used by Odoo. It is based on the XML format, and used [QWeb](https://www.odoo.com/documentation/12.0/reference/qweb.html) is the primary templating engine used by Odoo. It is based on the XML format, and used
mostly to generate html. In OWL, QWeb templates are compiled into functions that mostly to generate HTML. In OWL, QWeb templates are compiled into functions that
generate a virtual dom representation of the html. generate a virtual dom representation of the HTML.
Template directives are specified as XML attributes prefixed with `t-`, for instance `t-if` for conditionals, with elements and other attributes being rendered directly. Template directives are specified as XML attributes prefixed with `t-`, for instance `t-if` for conditionals, with elements and other attributes being rendered directly.
To avoid element rendering, a placeholder element `<t>` is also available, which executes its directive but doesn’t generate any output in and of itself. To avoid element rendering, a placeholder element `<t>` is also available, which executes its directive but doesn’t generate any output in and of itself.
The QWeb implementation in the OWL project is slightly different. It compiles ```xml
templates into functions that output a virtual DOM instead of a string. This is <div>
necessary for the component system. In addition, it has a few extra directives <span t-if="somecondition">Some string</span>
(see [OWL Specific Extensions](#owlspecificextensions)) <ul t-else="1">
<li t-foreach="messages" t-as="message">
<t t-esc="message">
</li>
</ul>
</div>
```
The QWeb class in the OWL project is an implementation of that specification
with a few interesting points:
## QWeb Specification - it compiles templates into functions that output a virtual DOM instead of a
string. This is necessary for the component system.
- it has a few extra directives: `t-widget`, `t-on`, ...
## Directives
### Static html nodes We present here a list of all standard QWeb directives:
| Name | Description |
| ------------------------------ | ------------------------------------------------------------ |
| `t-esc` | [Outputting safely a value](#outputting-data) |
| `t-raw` | [Outputting value, without escaping](#outputting-data) |
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
| `t-if`, `t-elif`, `t-else`, | [conditionally rendering](#conditionals) |
| `t-foreach`, `t-as` | [Loops](#loops) |
| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
| `t-call` | [Rendering sub templates](#rendering-sub-templates) |
| `t-debug`, `t-log` | [Debugging](#debugging) |
| `t-name` | [Defining a template (not really a directive)](#qweb-engine) |
The component system in Owl requires additional directives, to express various
needs. Here is a list of all Owl specific directives:
| Name | Description |
| ------------------------- | --------------------------------------------------------------------------------------- |
| `t-widget`, `t-keepalive` | [Defining a sub component](component.md#composition) |
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#keeping-references) |
| `t-key` | [Defining a key (to help virtual dom reconciliation)](component.md#t-key-directive) |
| `t-on-*` | [Event handling](component.md#event-handling) |
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
| `t-mounted` | [Callback when a node or component is mounted](#component.md#t-mounted-directive) |
## 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);
```
- **`register(name, Component)`**: static function to register an OWL Component
to QWeb's global registry. Globally registered Components can be used in
templates (see the `t-widget` directive). This is useful for commonly used
components accross the application.
```js
class Dialog extends owl.Component { ... }
QWeb.register("Dialog", Dialog);
...
class ParentWidget extends owl.Component { ... }
qweb.addTemplate("ParentWidget", "<div><t t-widget='Dialog'/></div>");
```
## Reference
We define in this section the specification of how `QWeb` templates should be
rendered. Note that we only document here the standard QWeb specification. Owl
specific extensions are documented in various other parts of the documentation.
### White spaces
White spaces in a templates are handled in a special way:
- consecutive whitespaces are always condensed to a single whitespace
- if a whitespace-only text node contains a linebreak, it is ignored
- the previous rules do not apply if we are in a `<pre>` tag
### Root nodes
For many reasons, Owl QWeb templates should have a single root node. More
precisely, the result of a template rendering should have a single root node:
```xml
<!–– not ok: two root nodes ––>
<t>
<div>foo</div>
<div>bar</div>
</t>
<!–– ok: result has one single root node ––>
<t>
<div t-if="someCondition">foo</div>
<span t-else="1">bar</span>
</t>
```
Extra root nodes will actually be ignored (even though they will be rendered
in memory).
Note: this does not apply to subtemplates (see the `t-call` directive). In that
case, they will be inlined in the main template, and can actually have many
root nodes.
### Expression evaluation
It is useful to explain the various rules that applies on QWeb expressions. These
expressions are strings that will be converted to a javascript expression at
compile time.
1. it should be a simple expression which returns a value. It cannot be a statement.
```xml
<div><p t-if="1 + 2 === 3">ok</p></div>
```
is valid, but the following is not valid:
```xml
<div><p t-if="console.log(1)">NOT valid</p></div>
```
2. it can use anything in the rendering context:
```xml
<p t-if="user.birthday == today()">Happy bithday!</p>
```
is valid, and will read the `user` object from the context, and call the
`today` function.
3. it can use a few special operators to avoid using symbols such as `<`, `>`,
`&` or `|`. This is useful to make sure that we still write valid XML.
| Word | will be replaced by |
| ----- | ------------------- |
| `and` | `&&` |
| `or` | `\|\|` |
| `gt` | `>` |
| `gte` | `>=` |
| `lt` | `<` |
| `lte` | `<=` |
So, one can write this:
```xml
<div><p t-if="10 + 2 gt 5">ok</p></div>
```
### Static Html Nodes
Normal, regular html nodes are rendered into themselves: Normal, regular html nodes are rendered into themselves:
@@ -50,7 +226,7 @@ Normal, regular html nodes are rendered into themselves:
<div>hello</div> <!–– rendered as itself ––> <div>hello</div> <!–– rendered as itself ––>
``` ```
### `t-esc` directive ### Outputting Data
The `t-esc` directive is necessary whenever you want to add a dynamic text The `t-esc` directive is necessary whenever you want to add a dynamic text
expression in a template. The text is escaped to avoid security issues. expression in a template. The text is escaped to avoid security issues.
@@ -65,14 +241,12 @@ rendered with the value `value` set to `42` in the rendering context yields:
<p>42</p> <p>42</p>
``` ```
### `t-raw` directive
The `t-raw` directive is almost the same as `t-esc`, but without the escaping. The `t-raw` directive is almost the same as `t-esc`, but without the escaping.
This is mostly useful to inject a raw html string somewhere. Obviously, this This is mostly useful to inject a raw html string somewhere. Obviously, this
is unsafe to do in general, and should only be used for strings known to be safe. is unsafe to do in general, and should only be used for strings known to be safe.
```xml ```xml
<p><t t-esc="value"/></p> <p><t t-raw="value"/></p>
``` ```
rendered with the value `value` set to `<span>foo</span>` in the rendering context yields: rendered with the value `value` set to `<span>foo</span>` in the rendering context yields:
@@ -81,7 +255,7 @@ rendered with the value `value` set to `<span>foo</span>` in the rendering conte
<p><span>foo</span></p> <p><span>foo</span></p>
``` ```
### `t-set` directive ### Setting Variables
QWeb allows creating variables from within the template, to memoize a computation (to use it multiple times), give a piece of data a clearer name, ... QWeb allows creating variables from within the template, to memoize a computation (to use it multiple times), give a piece of data a clearer name, ...
@@ -113,7 +287,7 @@ This is done via the `t-set` directive, which takes the name of the variable to
The `t-set` directive acts like a regular variable in most programming language. The `t-set` directive acts like a regular variable in most programming language.
It is lexically scoped (inner nodes are sub scopes), can be shadowed, ... It is lexically scoped (inner nodes are sub scopes), can be shadowed, ...
### `t-if` directive ### Conditionals
The `t-if` directive is useful to conditionally render something. It evaluates The `t-if` directive is useful to conditionally render something. It evaluates
the expression given as attribute value, and then acts accordingly. the expression given as attribute value, and then acts accordingly.
@@ -163,52 +337,7 @@ Extra conditional branching directives `t-elif` and `t-else` are also available:
</div> </div>
``` ```
### Expression evaluation ### Dynamic attributes
It is useful to explain the various rules that applies on QWeb expressions. These
expressions are strings that will be converted to a javascript expression at
compile time.
1. it should be a simple expression which returns a value. It cannot be a statement.
```xml
<div><p t-if="1 + 2 === 3">ok</p></div>
```
is valid, but the following is not valid:
```xml
<div><p t-if="console.log(1)">NOT valid</p></div>
```
2. it can use anything in the rendering context:
```xml
<p t-if="user.birthday == today()">Happy bithday!</p>
```
is valid, and will read the `user` object from the context, and call the
`today` function.
3. it can use a few special operators to avoid using symbols such as `<`, `>`,
`&` or `|`. This is useful to make sure that we still write valid XML.
| Word | will be replaced by |
| ----- | ------------------- |
| `and` | `&&` |
| `or` | `\|\|` |
| `gt` | `>` |
| `gte` | `>=` |
| `lt` | `<` |
| `lte` | `<=` |
So, one can write this:
```xml
<div><p t-if="10 + 2 gt 5">ok</p></div>
```
### `t-att` directive (dynamic attributes)
One can use the `t-att-` directive to add dynamic attributes. Its main use is to 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: evaluate an expression (at rendering time) and bind an attribute to its result:
@@ -216,7 +345,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, For example, if we have `id` set to 32 in the rendering context,
```xml ```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: If an expression evaluates to a falsy value, it will not be set at all:
@@ -225,7 +354,90 @@ If an expression evaluates to a falsy value, it will not be set at all:
<div t-att-foo="false"/> <!-- result: <div></div> --> <div t-att-foo="false"/> <!-- result: <div></div> -->
``` ```
### `t-call` directive (sub templates) 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> -->
```
### Loops
QWeb has an iteration directive `t-foreach` which take an expression returning the
collection to iterate on, and a second parameter `t-as` providing the name to use
for the current item of the iteration:
```xml
<t t-foreach="[1, 2, 3]" t-as="i">
<p><t t-esc="i"/></p>
</t>
```
will be rendered as:
```xml
<p>1</p>
<p>2</p>
<p>3</p>
```
Like conditions, `t-foreach` applies to the element bearing the directive’s attribute, and
```xml
<p t-foreach="[1, 2, 3]" t-as="i">
<t t-esc="i"/>
</p>
```
is equivalent to the previous example.
`t-foreach` can iterate on an array (the current item will be the current value),
an object (the current item will be the current key) or an integer (equivalent
to iterating on an array between 0 inclusive and the provided integer exclusive).
In addition to the name passed via t-as, `t-foreach` provides a few other
variables for various data points (note: `$as` will be replaced by the name
passed to `t-as`):
- `$as_value`: the current iteration value, identical to `$as` for lists and
integers, but for objects, it provides the value (where `$as` provides the key)
- `$as_index`: the current iteration index (the first item of the iteration has index 0)
- `$as_first`: whether the current item is the first of the iteration
(equivalent to `$as_index == 0`)
- `$as_last`: whether the current item is the last of the iteration
(equivalent to `$as_index + 1 == $as_size`), requires the iteratee’s size be
available
- `$as_parity` (deprecated): either "even" or "odd", the parity of the current
iteration round
These extra variables provided and all new variables created into the `t-foreach`
are only available in the scope of the `t-foreach`. If the variable exists outside
the context of the `t-foreach`, the value is copied at the end of the foreach
into the global context.
```xml
<t t-set="existing_variable" t-value="False"/>
<!-- existing_variable now False -->
<p t-foreach="[1, 2, 3]" t-as="i">
<t t-set="existing_variable" t-value="True"/>
<t t-set="new_variable" t-value="True"/>
<!-- existing_variable and new_variable now True -->
</p>
<!-- existing_variable always True -->
<!-- new_variable undefined -->
```
### Rendering Sub Templates
QWeb templates can be used for top level rendering, but they can also be used QWeb templates can be used for top level rendering, but they can also be used
from within another template (to avoid duplication or give names to parts of from within another template (to avoid duplication or give names to parts of
@@ -242,12 +454,12 @@ templates), using the `t-call` directive:
</div> </div>
``` ```
will be rendered as `<div><p>owl</p></div>`. This example shows that the sub 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 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 is actually inlined in the main template, but in a sub scope: variables defined
in the sub template do not escape. 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 the content of the body of the `t-call` directive is available as a special
magic variable `0`: magic variable `0`:
@@ -273,86 +485,7 @@ will result in :
</div> </div>
``` ```
### Debugging
## JS/OWL Specific Extensions
### `t-on` directive
### Component: `t-widget`, `t-props`
### `t-ref` directive
### `t-key` directive
Even though Owl tries to be as declarative as possible, some DOM state is still
locked inside the DOM: for example, the scrolling state, the current user selection,
the focused element or the state of an input. This is why we use a virtual dom
algorithm to keep the actual DOM node as much as possible. However, this is
sometimes not enough, and we need to help Owl decide if an element is actually
the same, or is different. The `t-key` directive is used to give an identity to an element.
There are three main use cases:
- *elements in a list*:
```xml
<span t-foreach="todos" t-as="todo" t-key="todo.id">
<t t-esc="todo.text"/>
</span>
```
- *`t-if`/`t-else`*
- *animations*: give a different identity to a component. Ex: thread id with
animations on add/remove message.
### `t-transition` directive
To perform useful transition effects, whenever an element appears or disappears,
it is necessary to add/remove some css style or class at some precise moment in
the lifetime of a node. Since this is not easy to do by hand, Owl `t-transition`
directive is there to help.
Whenever a node has a `t-transition` directive, with a `name` value, the following
will happen:
At node creation:
- the css classes `name-enter` and `name-enter-active` will be added before the
node is added to the DOM,
- on the next animation frame: the css class `name-enter` will be removed and the
class `name-enter-to` will be added (so they can be used to trigger css
transition effects),
- the css class `name-enter-active` will be removed whenever a css transition
ends.
At node destruction:
- the css classes `name-leave` and `name-leave-active` will be added before the
node is removed to the DOM,
- the css class `name-leave` will be removed on the next animation frame (so it
can be used to trigger css transition effects),
- the css class `name-leave-active` will be removed whenever a css transition
ends. Only then will the element be removed from the DOM.
For example, a simple fade in/out effect can be done with this:
```xml
<div>
<div t-if="state.flag" class="square" t-transition="fade">Hello</div>
</div>
```
```css
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
```
Note: more information on animations are available [here](doc/animations.md).
### Debugging (`t-debug` and `t-log`)
The javascript QWeb implementation provides two useful debugging directives: The javascript QWeb implementation provides two useful debugging directives:
@@ -362,7 +495,7 @@ The javascript QWeb implementation provides two useful debugging directives:
<t t-if="a_test"> <t t-if="a_test">
<t t-debug=""> <t t-debug="">
</t> </t>
```` ```
will stop execution if the browser dev tools are open. will stop execution if the browser dev tools are open.
@@ -374,38 +507,3 @@ will stop execution if the browser dev tools are open.
``` ```
will print 42 to the console will print 42 to the console
### White spaces
White spaces in a templates are handled in a special way:
- consecutive whitespaces are always condensed to a single whitespace
- if a whitespace-only text node contains a linebreak, it is ignored
- the previous rules do not apply if we are in a `<pre>` tag
### Root nodes
For many reasons, Owl QWeb templates should have a single root node. More
precisely, the result of a template rendering should have a single root node:
```xml
<!–– not ok: two root nodes ––>
<t>
<div>foo</div>
<div>bar</div>
</t>
<!–– ok: result has one single root node ––>
<t>
<div t-if="someCondition">foo</div>
<span t-else="1">bar</span>
</t>
```
Extra root nodes will actually be ignored (even though they will be rendered
in memory).
Note: this does not apply to subtemplates (see the `t-call` directive). In that
case, they will be inlined in the main template, and can actually have many
root nodes.
+2
View File
@@ -8,8 +8,10 @@
- [Store](store.md) - [Store](store.md)
- [Observer](observer.md) - [Observer](observer.md)
- [Virtual DOM](vdom.md) - [Virtual DOM](vdom.md)
- [Utils](utils.md)
## Miscellaneous ## Miscellaneous
- [Quick Start](quick_start.md) - [Quick Start](quick_start.md)
- [Animations](animations.md) - [Animations](animations.md)
- [Tooling](tooling.md)
- [Comparison with React/Vue](comparison.md) - [Comparison with React/Vue](comparison.md)
+18
View File
@@ -0,0 +1,18 @@
# 🦉 Tooling 🦉
## Development mode
By default, Owl is in *production* mode, this means that it will try to do its
job fast, and skip some expensive operations. However, in some cases, it is
convenient to have better information on what is going on, this is the purpose
of the dev mode.
Owl has a mode flag, in `owl.__info__.mode`. Its default value is `prod`, but
it can be set to `dev`:
```js
owl.__info__.mode = 'dev';
```
Note that templates compiled with the `prod` settings will not be recompiled.
So, changing this setting is best done at startup.
+39
View File
@@ -0,0 +1,39 @@
# 🦉 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)
## `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`
+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>
+84
View File
@@ -0,0 +1,84 @@
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
template = "counter";
state = { counter: 0 };
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
template = "message";
widgets = { Counter };
shouldUpdate(nextProps) {
return nextProps !== this.props;
}
removeMessage() {
this.trigger("remove_message", {
id: this.props.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
template = "root";
widgets = { Message };
state = { messages: [] };
addMessages(n) {
startMeasure("add " + n);
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
stopMeasure();
}
clear() {
startMeasure("clear");
this.state.messages = [];
stopMeasure();
}
updateSomeMessages() {
startMeasure("update every 10th");
const messages = this.state.messages;
for (let i = 0; i < this.state.messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += '!!!';
this.set(messages, i, msg);
}
stopMeasure();
}
removeMessage(data) {
startMeasure("remove message");
const index = this.state.messages.findIndex(m => m.id === data.id);
this.state.messages.splice(index, 1);
stopMeasure();
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.11.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<div id='main'></div>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,32 @@
<templates>
<div t-name="root" class="main">
<div class="left-thing">
<div>Number of msg: <t t-esc="state.messages.length"/></div>
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1000 messages</button>
<button t-on-click="addMessages(10000)">Add 10000 messages</button>
<button t-on-click="addMessages(50000)">Add 50000 messages</button>
<button t-on-click="updateSomeMessages">Update every 10th message</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="right-thing">
<div class="content">
<t t-foreach="state.messages" t-as="message">
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
</t>
</div>
</div>
</div>
<div t-name="message" class="message">
<span class="author"><t t-esc="props.author"/></span>
<span class="msg"><t t-esc="props.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<t t-widget="Counter"/>
</div>
<div t-name="counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
+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.12.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<div id='main'></div>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,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 // Likes Counter Widget
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class Counter extends owl.Component { class Counter extends owl.Component {
template = "counter";
state = { counter: 0 }; state = { counter: 0 };
increment() { increment() {
@@ -16,7 +15,6 @@ class Counter extends owl.Component {
// Message Widget // Message Widget
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class Message extends owl.Component { class Message extends owl.Component {
template = "message";
widgets = { Counter }; widgets = { Counter };
shouldUpdate(nextProps) { shouldUpdate(nextProps) {
@@ -33,7 +31,6 @@ class Message extends owl.Component {
// Root Widget // Root Widget
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class App extends owl.Component { class App extends owl.Component {
template = "root";
widgets = { Message }; widgets = { Message };
state = { messages: [] }; state = { messages: [] };
+4 -4
View File
@@ -1,5 +1,5 @@
<templates> <templates>
<div t-name="root" class="main"> <div t-name="App" class="main">
<div class="left-thing"> <div class="left-thing">
<div>Number of msg: <t t-esc="state.messages.length"/></div> <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(100)">Add 100 messages</button>
@@ -12,20 +12,20 @@
<div class="right-thing"> <div class="right-thing">
<div class="content"> <div class="content">
<t t-foreach="state.messages" t-as="message"> <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 t-widget="Message" t-key="message.id" message="message" t-on-remove_message="removeMessage"/>
</t> </t>
</div> </div>
</div> </div>
</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="author"><t t-esc="props.author"/></span>
<span class="msg"><t t-esc="props.msg"/></span> <span class="msg"><t t-esc="props.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button> <button class="remove" t-on-click="removeMessage">Remove</button>
<t t-widget="Counter"/> <t t-widget="Counter"/>
</div> </div>
<div t-name="counter"> <div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button> <button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div> </div>
+35 -15
View File
@@ -3,26 +3,46 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>OWL Extras</title> <title>OWL Extras</title>
<link rel="stylesheet" href="main.css">
</head> </head>
<body> <body>
<h1>🦉 OWL Extra Stuff 🦉</h1>
<div class="title">
<h1>🦉 OWL Extra Stuff 🦉</h1>
<div>Mysterious OWL: A web framework for structured, dynamic and maintainable applications</div>
</div>
<h2>Benchmarks</h2> <div class="section misc">
<p>The goal is to compare some operations on large lists</p> <h2>Misc</h2>
<div class="links">
<a href="playground">Playground (local)</a>
<a href="https://github.com/odoo/owl">Github</a>
<a href="https://odoo.github.io/owl/">Owl main page</a>
<a href="https://github.com/odoo/owl/blob/master/doc/readme.md">Owl Documentation</a>
</div>
</div>
<ul> <div class="section">
<li><a href="benchmarks/odoo-widgets-12.0">Odoo Widgets (12.0)</a></li> <h2>Benchmarks</h2>
<li><a href="benchmarks/odoo-widgets-12.3">Odoo Widgets (12.3)</a></li>
<li><a href="benchmarks/owl-0.7.0">OWL 0.7.0</a></li> <div class="benchmarks">
<li><a href="benchmarks/owl-0.8.0">OWL 0.8.0</a></li> <ul>
<li><a href="benchmarks/owl-0.9.0">OWL 0.9.0</a></li> <li><a href="benchmarks/owl-0.7.0">OWL 0.7.0</a></li>
<li><a href="benchmarks/owl-master">OWL Master</a></li> <li><a href="benchmarks/owl-0.8.0">OWL 0.8.0</a></li>
<li><a href="benchmarks/vue">Vue</a></li> <li><a href="benchmarks/owl-0.9.0">OWL 0.9.0</a></li>
<li><a href="benchmarks/react">React</a></li> <li><a href="benchmarks/owl-0.10.0">OWL 0.10.0</a></li>
</ul> <li><a href="benchmarks/owl-0.11.0">OWL 0.11.0</a></li>
<li><a href="benchmarks/owl-0.12.0">OWL 0.12.0</a></li>
<li><a href="benchmarks/owl-master">OWL Master</a></li>
</ul>
<ul>
<li><a href="benchmarks/odoo-widgets-12.0">Odoo Widgets (12.0)</a></li>
<li><a href="benchmarks/odoo-widgets-12.3">Odoo Widgets (12.3)</a></li>
<li><a href="benchmarks/vue">Vue</a></li>
<li><a href="benchmarks/react">React</a></li>
</ul>
</div>
</div>
<h2>Playground</h2>
<a href="playground">Playground</a>
</body> </body>
</html> </html>
+30
View File
@@ -0,0 +1,30 @@
body {
font-family: sans-serif;
}
.title,
.section {
width: 768px;
margin: auto;
}
.title div {
color: #333333;
font-style: italic;
}
.section {
background-color: #d5d5d5;
padding: 16px;
margin-top: 10px;
margin-bottom: 10px;
}
.links a {
margin: 10px 5px;
}
.benchmarks {
display: grid;
grid-template-columns: auto auto;
}
+190 -155
View File
@@ -1,5 +1,8 @@
import { SAMPLES } from "./samples.js"; import { SAMPLES } from "./samples.js";
//------------------------------------------------------------------------------
// Constants, helpers, utils
//------------------------------------------------------------------------------
let owlJS; let owlJS;
async function owlSourceCode() { async function owlSourceCode() {
@@ -36,104 +39,118 @@ const DEFAULT_HTML = `<!DOCTYPE html>
</html> </html>
`; `;
const APP_PY = `import sys const APP_PY = `#!/usr/bin/env python3
import thread
import webbrowser import threading
import time import time
import BaseHTTPServer, SimpleHTTPServer from http.server import SimpleHTTPRequestHandler, HTTPServer
def start_server(): def start_server():
httpd = BaseHTTPServer.HTTPServer(('127.0.0.1', 3600), SimpleHTTPServer.SimpleHTTPRequestHandler) SimpleHTTPRequestHandler.extensions_map['.js'] = 'application/javascript'
httpd = HTTPServer(('0.0.0.0', 3600), SimpleHTTPRequestHandler)
httpd.serve_forever() httpd.serve_forever()
thread.start_new_thread(start_server,())
url = 'http://127.0.0.1:3600' url = 'http://127.0.0.1:3600'
webbrowser.open_new(url)
while True: if __name__ == "__main__":
try: print("Owl Application")
time.sleep(1) print("---------------")
except KeyboardInterrupt: print("Server running on: {}".format(url))
sys.exit(0) threading.Thread(target=start_server, daemon=True).start()
while True:
try:
time.sleep(1)
except KeyboardInterrupt:
httpd.server_close()
quit(0)
`; `;
//------------------------------------------------------------------------------ /**
// Tabbed editor * Make an iframe, with all the js, css and xml properly injected.
//------------------------------------------------------------------------------ */
class TabbedEditor extends owl.Component { function makeCodeIframe(js, css, xml, errorHandler) {
constructor() { // check templates
super(...arguments); var qweb = new owl.QWeb();
this.template = "tabbed-editor"; const sanitizedXML = xml.replace(/<!--[\s\S]*?-->/g, "");
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")
};
}
mounted() { // will throw error if there is something wrong with xml
this.editor = ace.edit(this.refs.editor); qweb.addTemplates(sanitizedXML);
// remove this for xml/css (?) // create iframe
this.editor.session.setOption("useWorker", false); const iframe = document.createElement("iframe");
this.editor.setValue(this.props[this.state.currentTab], -1);
this.editor.setFontSize("12px"); iframe.onload = () => {
this.editor.setTheme("ace/theme/monokai"); const doc = iframe.contentDocument;
this.editor.session.setMode(MODES[this.state.currentTab]); // inject js
const tabSize = this.state.currentTab === "xml" ? 2 : 4; const owlScript = doc.createElement("script");
this.editor.session.setOption("tabSize", tabSize); owlScript.type = "text/javascript";
this.editor.on("blur", () => { owlScript.src = "../owl.js";
const editorValue = this.editor.getValue(); owlScript.addEventListener("load", () => {
const propsValue = this.props[this.state.currentTab]; const script = doc.createElement("script");
if (editorValue !== propsValue) { script.type = "text/javascript";
this.trigger("updateCode", { const content = `owl.__info__.mode = 'dev';\nwindow.TEMPLATES = \`${sanitizedXML}\`\n${js}`;
type: this.state.currentTab, script.innerHTML = content;
value: editorValue 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() { // Application code
if (this.editor) { ${processedJS}
window.dispatchEvent(new Event("resize")); }
this.editor.setValue(this.props[this.state.currentTab], -1);
}
}
willUnmount() { // wait for DOM ready before starting
this.editor.destroy(); owl.utils.whenReady(startApp);`;
delete this.editor;
}
setTab(tab) { zip.file("app.js", JS);
this.editor.setValue(this.props[tab], -1); zip.file("app.css", css);
zip.file("app.py", APP_PY);
const mode = MODES[tab]; zip.file("app.xml", xml);
this.editor.session.setMode(mode); zip.file("index.html", DEFAULT_HTML);
const tabSize = tab === "xml" ? 2 : 4; zip.file("owl.js", owlSourceCode());
this.editor.session.setOption("tabSize", tabSize); return zip.generateAsync({ type: "blob" });
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);
});
}
}
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -142,8 +159,7 @@ class TabbedEditor extends owl.Component {
class App extends owl.Component { class App extends owl.Component {
constructor(...args) { constructor(...args) {
super(...args); super(...args);
this.template = "playground"; this.version = owl.__info__.version;
this.version = owl._version;
this.SAMPLES = SAMPLES; this.SAMPLES = SAMPLES;
this.widgets = { TabbedEditor }; this.widgets = { TabbedEditor };
@@ -157,6 +173,10 @@ class App extends owl.Component {
leftPaneWidth: Math.ceil(window.innerWidth / 2), leftPaneWidth: Math.ceil(window.innerWidth / 2),
topPanelHeight: null topPanelHeight: null
}; };
this.toggleLayout = owl.utils.debounce(this.toggleLayout, 250, true);
this.runCode = owl.utils.debounce(this.runCode, 250, true);
this.downloadCode = owl.utils.debounce(this.downloadCode, 250, true);
} }
displayError(error) { displayError(error) {
@@ -169,61 +189,26 @@ class App extends owl.Component {
} }
} }
async runCode() { runCode() {
this.state.displayWelcome = false; this.state.displayWelcome = false;
// check templates let subiframe;
var qweb = new owl.QWeb(); let error = false;
var error = false; const errorHandler = e => this.displayError(e.message || e.reason.message);
const sanitizedXML = this.state.xml.replace(/<!--[\s\S]*?-->/g, "");
try { try {
qweb.loadTemplates(sanitizedXML); const { js, css, xml } = this.state;
subiframe = makeCodeIframe(js, css, xml, errorHandler);
} catch (e) { } catch (e) {
//probably problem with the templates
error = e; error = e;
} }
if (error) { if (error) {
this.displayError(error.message); this.displayError(error.message);
return; return;
} else { } else {
this.state.error = false; 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.innerHTML = "";
this.refs.content.appendChild(iframe); this.refs.content.appendChild(subiframe);
} }
setSample(ev) { setSample(ev) {
@@ -263,61 +248,111 @@ class App extends owl.Component {
}); });
} }
updateCode(ev) { updateCode(ev) {
this.state[ev.type] = ev.value; this.state[ev.detail.type] = ev.detail.value;
} }
toggleLayout() { toggleLayout() {
this.state.splitLayout = !this.state.splitLayout; this.state.splitLayout = !this.state.splitLayout;
} }
updatePanelHeight(ev) { updatePanelHeight(ev) {
if (!ev.delta) { if (!ev.detail.delta) {
return; return;
} }
let height = this.state.topPanelHeight; let height = this.state.topPanelHeight;
if (!height) { if (!height) {
height = document.getElementsByClassName("tabbed-editor")[0].clientHeight; height = document.getElementsByClassName("tabbed-editor")[0].clientHeight;
} }
this.state.topPanelHeight = height + ev.delta; this.state.topPanelHeight = height + ev.detail.delta;
} }
async downloadCode() { 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/FileSaver.min.js");
await owl.utils.loadJS("libs/jszip.min.js"); saveAs(content, "app.zip");
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;
} }
// 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); this.sessions = {};
zip.file("app.css", this.state.css); for (let tab of ["js", "xml", "css"]) {
zip.file("app.py", APP_PY); if (props[tab]) {
zip.file("app.xml", this.state.xml); this.sessions[tab] = new ace.EditSession(props[tab], MODES[tab]);
zip.file("index.html", DEFAULT_HTML); this.sessions[tab].setOption("useWorker", false);
zip.file("owl.js", owlSourceCode()); const tabSize = tab === "xml" ? 2 : 4;
zip.generateAsync({ type: "blob" }).then(function(content) { this.sessions[tab].setOption("tabSize", tabSize);
saveAs(content, "app.zip"); this.sessions[tab].setUndoManager(new ace.UndoManager());
}
}
this.editor = null;
}
mounted() {
this.editor = 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);
this.editor.resize();
}
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);
});
}
}
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Application initialization // Application initialization
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
document.title = `${document.title} (v${owl._version})`; document.title = `${document.title} (v${owl.__info__.version})`;
document.addEventListener("DOMContentLoaded", async function() { document.addEventListener("DOMContentLoaded", async function() {
const templates = await owl.utils.loadTemplates("templates.xml"); const templates = await owl.utils.loadTemplates("templates.xml");
const qweb = new owl.QWeb(templates); const qweb = new owl.QWeb(templates);
+4 -1
View File
@@ -22,7 +22,7 @@ body {
display: grid; display: grid;
height: 100%; height: 100%;
width: 100%; width: 100%;
grid-template-columns: auto 6px 100%; grid-template-columns: auto 8px 100%;
} }
/* LEFT BAR ****************************************/ /* LEFT BAR ****************************************/
@@ -115,6 +115,7 @@ body {
width: 34px; width: 34px;
text-align: center; text-align: center;
border-bottom: 2px solid transparent; border-bottom: 2px solid transparent;
text-transform: uppercase;
} }
.tab.active { .tab.active {
@@ -184,4 +185,6 @@ body {
font-size: 30px; font-size: 30px;
color: darkred; color: darkred;
text-align: center; text-align: center;
padding-left: 30px;
padding-right: 30px;
} }
+168 -285
View File
@@ -1,7 +1,6 @@
const CLICK_COUNTER = `class ClickCounter extends owl.Component { const CLICK_COUNTER = `class ClickCounter extends owl.Component {
constructor() { constructor() {
super(...arguments); super(...arguments);
this.template = "clickcounter";
this.state = { value: 0 }; this.state = { value: 0 };
} }
@@ -16,7 +15,7 @@ counter.mount(document.body);
`; `;
const CLICK_COUNTER_XML = `<templates> 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"/>] Click Me! [<t t-esc="state.value"/>]
</button> </button>
</templates>`; </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 const CLICK_COUNTER_ESNEXT = `// This example will not work if your browser does not support ESNext class fields
class ClickCounter extends owl.Component { class ClickCounter extends owl.Component {
template = "clickcounter";
state = { value: 0 }; state = { value: 0 };
increment() { increment() {
@@ -42,67 +40,87 @@ const counter = new ClickCounter({ qweb });
counter.mount(document.body); counter.mount(document.body);
`; `;
const WIDGET_COMPOSITION = `class ClickCounter extends owl.Component { const WIDGET_COMPOSITION = `// This example will not work if your browser does not support ESNext class fields
constructor(parent, props) {
super(parent, props); class ClickCounter extends owl.Component {
this.template = "clickcounter"; state = { value: 0 };
this.state = { value: props.initialState || 0 };
}
increment() { increment() {
this.state.value++; this.state.value++;
} }
} }
let nextId = 1; class InputWidget extends owl.Component {
state = {text: ""};
class App extends owl.Component {
constructor() { updateVal(event) {
super(...arguments); let value = event.target.value;
this.template = "app"; if (this.props.reverse) {
this.state = { counters: [] } value = value.split("").reverse().join("");
this.widgets = { ClickCounter }; }
} this.state.text = value
addCounter() {
this.state.counters.push(nextId++);
} }
} }
// Main root widget
class App extends owl.Component {
widgets = {ClickCounter, InputWidget};
}
// Application setup
const qweb = new owl.QWeb(TEMPLATES); const qweb = new owl.QWeb(TEMPLATES);
const app = new App({ qweb }); const app = new App({ qweb });
app.mount(document.body); app.mount(document.body);
`; `;
const WIDGET_COMPOSITION_XML = `<templates> 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"/>] Click Me! [<t t-esc="state.value"/>]
</button> </button>
<div t-name="app"> <div t-name="InputWidget">
<div><button t-on-click="addCounter">Add a counter</button></div> <input t-on-input="updateVal" placeholder="type here some text..."/>
<div> <span><t t-esc="state.text"/></span>
<t t-foreach="state.counters" t-as="counter"> </div>
<t t-widget="ClickCounter" t-key="counter" t-props="{initialState: counter}"/>
</t> <div t-name="App">
</div> <t t-widget="ClickCounter"/>
<t t-widget="InputWidget" reverse="true"/>
</div> </div>
</templates>`; </templates>`;
const WIDGET_COMPOSITION_CSS = `button { const WIDGET_COMPOSITION_CSS = `button, input {
color: darkred; font-size: 20px;
font-size: 30px;
width: 220px; width: 220px;
margin: 5px;
}`; }`;
const ANIMATION = `// This example will not work if your browser does not support ESNext class fields const ANIMATION = `// This example will not work if your browser does not support ESNext class fields
class ClickCounter extends owl.Component {
state = { value: 0 };
increment() {
this.state.value++;
}
}
class App extends owl.Component { class App extends owl.Component {
template = "app"; state = {flag: false, widgetFlag: false, numbers: []};
state = {flag: 0}; widgets = {ClickCounter};
toggle() { toggle() {
this.state.flag = !this.state.flag; this.state.flag = !this.state.flag;
} }
toggleWidget() {
this.state.widgetFlag = !this.state.widgetFlag;
}
addNumber() {
const n = this.state.numbers.length + 1;
this.state.numbers.push(n);
}
} }
const qweb = new owl.QWeb(TEMPLATES); const qweb = new owl.QWeb(TEMPLATES);
@@ -111,33 +129,82 @@ app.mount(document.body);
`; `;
const ANIMATION_XML = `<templates> const ANIMATION_XML = `<templates>
<div t-name="app"> <button t-name="ClickCounter" t-on-click="increment" class="clickcounter">
<button t-on-click="toggle"> Click Me! [<t t-esc="state.value"/>]
Click Me!
</button> </button>
<div>
<div t-if="state.flag" class="square" t-transition="fade">Hello</div> <div t-name="App">
<h2>Transition on DOM element</h2>
<div class="demo">
<button t-on-click="toggle">Toggle square</button>
<div>
<div t-if="state.flag" class="square" t-transition="fade">Hello</div>
</div>
</div> </div>
<h2>Transition on sub widgets</h2>
<div class="demo">
<button t-on-click="toggleWidget">
Toggle widget
</button>
<div>
<t t-widget="ClickCounter" t-if="state.widgetFlag" t-transition="fade"/>
</div>
</div>
<h2>Transition on lists</h2>
<p>Transitions can also be applied on lists</p>
<div class="demo">
<button t-on-click="addNumber">Add a number</button>
<div>
<t t-foreach="state.numbers" t-as="n">
<span t-transition="fade" class="numberspan"><t t-esc="n"/></span>
</t>
</div>
</div>
<h2>Simple CSS animation</h2>
<p>Remember, normal CSS still apply: for example, a simple flash animation with pure css </p>
<div><a class="btn flash">Click</a></div>
</div> </div>
</templates> </templates>
`; `;
const ANIMATION_CSS = `button { const ANIMATION_CSS = `button {
width: 100px; font-size: 18px;
height: 30px; height: 35px;
font-size: 20px;
} }
.btn {
cursor: pointer;
padding: 5px;
margin: 5px;
background-color: #dddddd;
}
.flash {
background-position: center;
transition: background 0.5s;
}
.flash:active {
background-color: gray;
transition: background 0s;
}
.square { .square {
background-color: red; background-color: red;
width: 100px; width: 100px;
height: 100px; height: 70px;
color: white; color: white;
margin: 20px; margin: 0 20px;
font-size: 24px; font-size: 24px;
line-height: 100px; line-height: 70px;
text-align: center; text-align: center;
line-height: 100px;
} }
.fade-enter-active, .fade-leave-active { .fade-enter-active, .fade-leave-active {
@@ -146,13 +213,30 @@ const ANIMATION_CSS = `button {
.fade-enter, .fade-leave-to { .fade-enter, .fade-leave-to {
opacity: 0; opacity: 0;
} }
.demo {
display: flex;
height: 80px;
}
.clickcounter {
margin-left: 20px;
height: 50px;
background-color: blue;
color: white;
}
.numberspan {
border: 1px solid green;
margin: 5px;
padding: 5px;
}
`; `;
const LIFECYCLE_DEMO = `class HookWidget extends owl.Component { const LIFECYCLE_DEMO = `class HookWidget extends owl.Component {
constructor() { constructor() {
super(...arguments); super(...arguments);
this.template = "demo.hookwidget";
this.state = { n: 0 }; this.state = { n: 0 };
console.log("constructor"); console.log("constructor");
} }
@@ -183,7 +267,6 @@ class ParentWidget extends owl.Component {
constructor() { constructor() {
super(...arguments); super(...arguments);
this.widgets = { HookWidget }; this.widgets = { HookWidget };
this.template = "demo.parentwidget";
this.state = { n: 0, flag: true }; this.state = { n: 0, flag: true };
} }
increment() { increment() {
@@ -200,193 +283,14 @@ widget.mount(document.body);
`; `;
const LIFECYCLE_DEMO_XML = `<templates> 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="increment">Increment</button>
<button t-on-click="toggleSubWidget">ToggleSubWidget</button> <button t-on-click="toggleSubWidget">ToggleSubWidget</button>
<div t-if="state.flag"> <div t-if="state.flag">
<t t-widget="HookWidget" t-props="{n:state.n}"/> <t t-widget="HookWidget" n="state.n"/>
</div> </div>
</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 t-name="HookWidget" t-on-click="increment">Demo Sub Widget. Props: <t t-esc="props.n"/>. State: <t t-esc="state.n"/>. (click on me to update me)</div>
</templates>`;
const BENCHMARK_APP = `//------------------------------------------------------------------------------
// Generating demo data
//------------------------------------------------------------------------------
const messages = [];
const authors = ["Aaron", "David", "Vincent"];
const content = [
"Lorem ipsum dolor sit amet",
"Sed ut perspiciatis unde omnis iste natus error sit voluptatem",
"Excepteur sint occaecat cupidatat non proident"
];
function chooseRandomly(array) {
const index = Math.floor(Math.random() * array.length);
return array[index];
}
for (let i = 1; i < 16000; i++) {
messages.push({
id: i,
author: chooseRandomly(authors),
msg: \`\${i}: \${chooseRandomly(content)}\`,
likes: 0
});
}
//------------------------------------------------------------------------------
// Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
constructor(parent, props) {
super(parent, props);
this.template = "counter";
this.state = { counter: props.initialState || 0 };
}
increment(delta) {
this.state.counter += delta;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
constructor() {
super(...arguments);
this.template = "message";
this.widgets = { Counter };
}
removeMessage() {
this.trigger("remove_message", {
id: this.props.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
constructor() {
super(...arguments);
this.template = "root";
this.widgets = { Message };
this.state = { messages: messages.slice(0, 10) };
}
setMessageCount(n) {
this.state.messages = messages.slice(0, n);
}
removeMessage(data) {
const index = messages.findIndex(m => m.id === data.id);
this.state.messages.splice(index, 1);
}
increment(delta) {
const n = this.state.messages.length + delta;
this.setMessageCount(n);
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
const env = {
qweb: new owl.QWeb(TEMPLATES)
};
const app = new App(env);
app.mount(document.body);
`;
const BENCHMARK_APP_CSS = `.main {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
display: grid;
grid-template-columns: 220px 1fr;
}
.left-thing {
background-color: gray;
padding: 20px;
}
.left-thing button {
width: 100%;
}
.left-thing .counter span {
color: white;
}
.left-thing .counter button {
width: 40px;
}
.right-thing {
padding: 20px;
overflow: auto;
}
/* Message widget */
.message .author {
font-weight: bold;
}
.message {
width: 400px;
background-color: lightblue;
margin: 10px 5px;
border-radius: 5px;
padding: 5px;
}`;
const BENCHMARK_APP_XML = `<templates>
<div t-name="root" class="main">
<div class="left-thing">
<div class="counter">
<button t-on-click="increment(-1)">-</button>
<span style="font-weight:bold">Value: <t t-esc="state.messages.length"/></span>
<button t-on-click="increment(1)">+</button>
</div>
<button t-on-click="setMessageCount(10)">10 messages</button>
<button t-on-click="setMessageCount(20)">20 messages</button>
<button t-on-click="setMessageCount(500)">500 messages</button>
<button t-on-click="setMessageCount(1000)">1000 messages</button>
<button t-on-click="setMessageCount(5000)">5000 messages</button>
<button t-on-click="setMessageCount(15000)">15000 messages</button>
</div>
<div class="right-thing">
<div class="content">
<t t-foreach="state.messages" t-as="message">
<t t-widget="Message" t-att-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
</t>
</div>
</div>
</div>
<div t-name="message" class="message">
<span class="author"><t t-esc="props.author"/></span>
<span class="msg"><t t-esc="props.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<t t-widget="Counter" t-props="{initialState: props.id}"/>
</div>
<div t-name="counter">
<button t-on-click="increment(-1)">-</button>
<span style="font-weight:bold">Value: <t t-esc="state.counter"/></span>
<button t-on-click="increment(1)">+</button>
</div>
</templates>`; </templates>`;
const TODO_APP_STORE = `const ENTER_KEY = 13; const TODO_APP_STORE = `const ENTER_KEY = 13;
@@ -477,7 +381,6 @@ function makeStore() {
// TodoItem // TodoItem
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class TodoItem extends owl.Component { class TodoItem extends owl.Component {
template = "todoitem";
state = { isEditing: false }; state = { isEditing: false };
removeTodo() { removeTodo() {
@@ -490,11 +393,12 @@ class TodoItem extends owl.Component {
async editTodo() { async editTodo() {
this.state.isEditing = true; this.state.isEditing = true;
setTimeout(() => { }
this.refs.input.value = "";
this.refs.input.focus(); focusInput() {
this.refs.input.value = this.props.title; this.refs.input.value = "";
}); this.refs.input.focus();
this.refs.input.value = this.props.title;
} }
handleKeyup(ev) { handleKeyup(ev) {
@@ -535,7 +439,6 @@ function mapStateToProps(state) {
} }
class TodoApp extends owl.Component { class TodoApp extends owl.Component {
template = "todoapp";
widgets = { TodoItem }; widgets = { TodoItem };
state = { filter: "all" }; state = { filter: "all" };
@@ -602,7 +505,7 @@ app.mount(document.body);
`; `;
const TODO_APP_STORE_XML = `<templates> const TODO_APP_STORE_XML = `<templates>
<section t-name="todoapp" class="todoapp"> <section t-name="TodoApp" class="todoapp">
<header class="header"> <header class="header">
<h1>todos</h1> <h1>todos</h1>
<input class="new-todo" autofocus="true" autocomplete="off" placeholder="What needs to be done?" t-on-keyup="addTodo"/> <input class="new-todo" autofocus="true" autocomplete="off" placeholder="What needs to be done?" t-on-keyup="addTodo"/>
@@ -612,7 +515,7 @@ const TODO_APP_STORE_XML = `<templates>
<label for="toggle-all"></label> <label for="toggle-all"></label>
<ul class="todo-list"> <ul class="todo-list">
<t t-foreach="visibleTodos" t-as="todo"> <t t-foreach="visibleTodos" t-as="todo">
<t t-widget="TodoItem" t-key="todo.id" t-props="todo"/> <t t-widget="TodoItem" t-key="todo.id" id="todo.id" completed="todo.completed" title="todo.title"/>
</t> </t>
</ul> </ul>
</section> </section>
@@ -640,7 +543,7 @@ const TODO_APP_STORE_XML = `<templates>
</footer> </footer>
</section> </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"> <div class="view">
<input class="toggle" type="checkbox" t-on-change="toggleTodo" t-att-checked="props.completed"/> <input class="toggle" type="checkbox" t-on-change="toggleTodo" t-att-checked="props.completed"/>
<label t-on-dblclick="editTodo"> <label t-on-dblclick="editTodo">
@@ -648,7 +551,7 @@ const TODO_APP_STORE_XML = `<templates>
</label> </label>
<button class="destroy" t-on-click="removeTodo"></button> <button class="destroy" t-on-click="removeTodo"></button>
</div> </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> </li>
</templates>`; </templates>`;
@@ -1034,39 +937,25 @@ html .clear-completed:active {
} }
`; `;
const RESPONSIVE = `class Navbar extends owl.Component { const RESPONSIVE = `class Navbar extends owl.Component {}
template="navbar";
}
class ControlPanel extends owl.Component { class ControlPanel extends owl.Component {
template="controlpanel";
widgets = { MobileSearchView }; widgets = { MobileSearchView };
} }
class FormView extends owl.Component { class FormView extends owl.Component {
template="formview";
widgets = { AdvancedWidget }; widgets = { AdvancedWidget };
} }
class AdvancedWidget extends owl.Component { class AdvancedWidget extends owl.Component {}
template="advancedwidget";
}
class Chatter extends owl.Component { class Chatter extends owl.Component {}
template="chatter";
}
class MobileSearchView extends owl.Component { class MobileSearchView extends owl.Component {}
template="mobilesearchview";
}
class App extends owl.Component { class App extends owl.Component {
constructor() { widgets = { Navbar, ControlPanel, FormView, Chatter };
super(...arguments);
this.template = "app";
this.widgets = { Navbar, ControlPanel, FormView, Chatter };
}
} }
function isMobile() { function isMobile() {
@@ -1081,39 +970,39 @@ const env = {
const app = new App(env); const app = new App(env);
app.mount(document.body); app.mount(document.body);
window.addEventListener( function updateEnv() {
"resize", const _isMobile = isMobile();
owl.utils.debounce(function() { if (_isMobile !== env.isMobile) {
const _isMobile = isMobile(); app.updateEnv({
if (_isMobile !== env.isMobile) { isMobile: _isMobile
app.updateEnv({ });
isMobile: _isMobile }
}); }
}
}, 20) window.addEventListener("resize", owl.utils.debounce(updateEnv, 20));
);`; `;
const RESPONSIVE_XML = `<templates> 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> <h2>controlpanel</h2>
<t t-if="env.isMobile" t-widget="MobileSearchView"/> <t t-if="env.isMobile" t-widget="MobileSearchView"/>
</div> </div>
<div t-name="formview" class="formview"> <div t-name="FormView" class="formview">
<h2>formview</h2> <h2>formview</h2>
<t t-if="!env.isMobile" t-widget="AdvancedWidget"/> <t t-if="!env.isMobile" t-widget="AdvancedWidget"/>
</div> </div>
<div t-name="chatter" class="chatter"> <div t-name="Chatter" class="chatter">
<h2>Chatter</h2> <h2>Chatter</h2>
<t t-foreach="100" t-as="item"><div>Message <t t-esc="item"/></div></t> <t t-foreach="100" t-as="item"><div>Message <t t-esc="item"/></div></t>
</div> </div>
<div t-name="mobilesearchview">MOBILE searchview</div> <div t-name="MobileSearchView">MOBILE searchview</div>
<div t-name="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="Navbar"/>
<t t-widget="ControlPanel"/> <t t-widget="ControlPanel"/>
<div class="content-wrapper" t-if="!env.isMobile"> <div class="content-wrapper" t-if="!env.isMobile">
@@ -1128,7 +1017,7 @@ const RESPONSIVE_XML = `<templates>
</t> </t>
</div> </div>
<div t-name="advancedwidget"> <div t-name="AdvancedWidget">
This widget is only created in desktop mode. This widget is only created in desktop mode.
<button>Button!</button> <button>Button!</button>
</div> </div>
@@ -1230,12 +1119,6 @@ export const SAMPLES = [
code: LIFECYCLE_DEMO, code: LIFECYCLE_DEMO,
xml: LIFECYCLE_DEMO_XML xml: LIFECYCLE_DEMO_XML
}, },
{
description: "Benchmark application",
code: BENCHMARK_APP,
css: BENCHMARK_APP_CSS,
xml: BENCHMARK_APP_XML
},
{ {
description: "Todo List App (with store)", description: "Todo List App (with store)",
code: TODO_APP_STORE, code: TODO_APP_STORE,
+24 -14
View File
@@ -1,15 +1,19 @@
<templates> <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"> <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> <t t-foreach="['js', 'xml', 'css']" t-as="tab">
<a t-if="tabs.xml" class="tab flash" t-att-class="{active: state.currentTab==='xml'}" t-on-click="setTab('xml')">XML</a> <a t-ref="{{tab}}" t-if="props[tab]" t-key="tab" class="tab flash" t-att-class="{active: state.currentTab===tab}" t-on-click="setTab(tab)">
<a t-if="tabs.css" class="tab flash" t-att-class="{active: state.currentTab==='css'}" t-on-click="setTab('css')">CSS</a> <t t-esc="tab"/>
</a>
</t>
</div> </div>
<div class="code-editor" t-ref="'editor'"></div> <div class="code-editor" t-ref="editor"></div>
</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="left-bar" t-att-class="{split: state.splitLayout}"
t-att-style="leftPaneStyle"
t-on-updateCode="updateCode">
<div class="menubar"> <div class="menubar">
<a class="btn run-code flash" t-on-click="runCode" title="Execute this Code">▶ Run</a> <a class="btn run-code flash" t-on-click="runCode" title="Execute this Code">▶ Run</a>
<select t-on-change="setSample"> <select t-on-change="setSample">
@@ -20,13 +24,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="btn flash" t-on-click="downloadCode" title="Download a Zip with this Code"><i class="fas fa-download"></i></a>
<a class="layout-selector flash" t-on-click="toggleLayout" title="Toggle Layout"><i class="fas" t-att-class="state.splitLayout ? 'fa-toggle-on' : 'fa-toggle-off'"></i></a> <a class="layout-selector flash" t-on-click="toggleLayout" title="Toggle Layout"><i class="fas" t-att-class="state.splitLayout ? 'fa-toggle-on' : 'fa-toggle-off'"></i></a>
</div> </div>
<t t-if="!state.splitLayout"> <t t-widget="TabbedEditor"
<t t-widget="TabbedEditor" t-props="{js:state.js, css:state.css, xml: state.xml, display: 'js|xml|css'}" t-on-updateCode="updateCode"/> js="state.js"
</t> css="!state.splitLayout and state.css"
<t t-else="1"> xml="!state.splitLayout and state.js"
<t t-widget="TabbedEditor" t-props="{js:state.js, css:state.css, xml: state.xml, display: 'js', style:topEditorStyle}" t-on-updateCode="updateCode"/> t-att-style="topEditorStyle"/>
<t t-if="state.splitLayout">
<div class="separator horizontal"/> <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-keepalive="1"
js="false"
css="state.css"
xml="state.xml"
resizeable="true"
t-on-updatePanelHeight="updatePanelHeight"/>
</t> </t>
</div> </div>
<div class="separator vertical" t-on-mousedown="onMouseDown"/> <div class="separator vertical" t-on-mousedown="onMouseDown"/>
@@ -40,7 +50,7 @@
<div t-if="state.error" class="error"> <div t-if="state.error" class="error">
<t t-esc="state.error"/> <t t-esc="state.error"/>
</div> </div>
<div class="content" t-ref="'content'"/> <div class="content" t-ref="content"/>
</div> </div>
</div> </div>
</templates> </templates>
+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 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: 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 ## Benchmarks
+23 -15
View File
@@ -1,10 +1,8 @@
import sys #!/usr/bin/env python3
import thread
import webbrowser
import time
import BaseHTTPServer import threading
import SimpleHTTPServer import time
from http.server import SimpleHTTPRequestHandler, HTTPServer
HOST = '127.0.0.1' HOST = '127.0.0.1'
PORT = 8000 PORT = 8000
@@ -15,24 +13,34 @@ URL = 'http://{0}:{1}/extras'.format(HOST, PORT)
# in dist/. This is useful for the benchmarks and playground applications. # in dist/. This is useful for the benchmarks and playground applications.
# With this, we can simply copy the playground folder as is in the gh-page when # With this, we can simply copy the playground folder as is in the gh-page when
# we want to update the playground. # we want to update the playground.
class OWLHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): class OWLHandler(SimpleHTTPRequestHandler):
def do_GET(self): def do_GET(self):
if self.path == '/extras/owl.js': if self.path == '/extras/owl.js':
self.path = '/dist/owl.js' self.path = '/dist/owl.js'
return SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self) return SimpleHTTPRequestHandler.do_GET(self)
def end_headers(self):
self.disable_cache_headers()
SimpleHTTPRequestHandler.end_headers(self)
def disable_cache_headers(self):
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
self.send_header("Pragma", "no-cache")
self.send_header("Expires", "0")
def start_server(): OWLHandler.extensions_map['.js'] = 'application/javascript'
httpd = BaseHTTPServer.HTTPServer((HOST, PORT), OWLHandler)
httpd.serve_forever()
if __name__ == "__main__": if __name__ == "__main__":
thread.start_new_thread(start_server, ()) print("Owl Extras")
webbrowser.open_new(URL) print("----------")
print("Server running on: {}".format(URL))
httpd = HTTPServer((HOST, PORT), OWLHandler)
threading.Thread(target=httpd.serve_forever, daemon=True).start()
while True: while True:
try: try:
time.sleep(1) time.sleep(1)
except KeyboardInterrupt: except KeyboardInterrupt:
sys.exit(0) httpd.server_close()
quit(0)
+9 -13
View File
@@ -1,20 +1,19 @@
{ {
"name": "owl", "name": "owl",
"version": "0.10.0", "version": "0.13.0",
"description": "Odoo Web Library (OWL)", "description": "Odoo Web Library (OWL)",
"main": "src/index.ts", "main": "src/index.ts",
"scripts": { "scripts": {
"build:js": "tsc --target esnext --module es6 --outDir dist/owl src/*", "build:js": "tsc --target esnext --module es6 --outDir dist/owl",
"build:js:es5": "tsc --target es5 --module es6 --outDir dist/owl src/*",
"build:bundle": "rollup -c", "build:bundle": "rollup -c",
"build": "npm run build:js && npm run build:bundle", "build": "npm run build:js && npm run build:bundle",
"build:es5": "npm run build:js:es5 && npm run build:bundle",
"minify": "uglifyjs dist/owl.js -o dist/owl.min.js --compress --mangle", "minify": "uglifyjs dist/owl.js -o dist/owl.min.js --compress --mangle",
"dev": "npm-run-all --parallel \"build:* -- --watch\"",
"test": "jest", "test": "jest",
"test:watch": "jest --watch", "test:watch": "jest --watch",
"preextras": "npm run build", "extras:serve": "python3 extras/server.py || python extras/server.py",
"extras": "python extras/server.py" "extras": "npm run build && npm run extras:serve",
"preextras:watch": "npm run build",
"extras:watch": "npm-run-all --parallel extras:serve \"build:* -- --watch\""
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -36,16 +35,13 @@
"rollup": "^1.6.0", "rollup": "^1.6.0",
"rollup-plugin-typescript2": "^0.20.1", "rollup-plugin-typescript2": "^0.20.1",
"sass": "^1.16.1", "sass": "^1.16.1",
"snabbdom-to-html": "^5.1.1",
"source-map-support": "^0.5.10", "source-map-support": "^0.5.10",
"ts-jest": "^23.10.5", "ts-jest": "^23.10.5",
"typescript": "^3.2.2", "typescript": "^3.2.2",
"uglify-es": "^3.3.9" "uglify-es": "^3.3.9",
}, "jest-environment-jsdom": "^24.7.1"
"dependencies": {
"jest-environment-jsdom": "^24.7.1",
"snabbdom": "^0.7.3"
}, },
"dependencies": {},
"jest": { "jest": {
"roots": [ "roots": [
"<rootDir>/src", "<rootDir>/src",
+1 -1
View File
@@ -9,6 +9,6 @@ export default {
format: "iife", format: "iife",
name: "owl", name: "owl",
extend: true, extend: true,
outro: `exports._version = '${version}';\nexports._date = '${new Date().toISOString()}';\nexports._hash = '${git.short()}';\nexports._url = 'https://github.com/odoo/owl';` outro: `exports.__info__.version = '${version}';\nexports.__info__.date = '${new Date().toISOString()}';\nexports.__info__.hash = '${git.short()}';\nexports.__info__.url = 'https://github.com/odoo/owl';`
} }
}; };
+322 -111
View File
@@ -1,4 +1,3 @@
import { EventBus } from "./event_bus";
import { Observer } from "./observer"; import { Observer } from "./observer";
import { QWeb, CompiledTemplate } from "./qweb_core"; import { QWeb, CompiledTemplate } from "./qweb_core";
import { h, patch, VNode } from "./vdom"; import { h, patch, VNode } from "./vdom";
@@ -18,14 +17,27 @@ import { h, patch, VNode } from "./vdom";
// Types/helpers // Types/helpers
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
/**
* An Env (environment) is an object that will be (mostly) shared between all
* components of an Owl application. It is the location which should contain
* the qweb instance necessary to render all components.
*
* Note that it is totally fine to extend the environment with application
* specific keys/objects/whatever. For example, a key `isMobile` (to declare
* if we are in "mobile" mode), or a shared bus could be useful.
*/
export interface Env { export interface Env {
qweb: QWeb; qweb: QWeb;
} }
/**
* This is mostly an internal detail of implementation. The Meta interface is
* useful to typecheck and describe the internal keys used by Owl to manage the
* component tree.
*/
export interface Meta<T extends Env, Props> { export interface Meta<T extends Env, Props> {
readonly id: number; readonly id: number;
vnode: VNode | null; vnode: VNode | null;
isStarted: boolean;
isMounted: boolean; isMounted: boolean;
isDestroyed: boolean; isDestroyed: boolean;
parent: Component<T, any, any> | null; parent: Component<T, any, any> | null;
@@ -40,22 +52,28 @@ export interface Meta<T extends Env, Props> {
boundHandlers: { [key: number]: any }; boundHandlers: { [key: number]: any };
observer?: Observer; observer?: Observer;
render?: CompiledTemplate; 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 // Widget
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
let nextId = 1; let nextId = 1;
export class Component< export class Component<T extends Env, Props extends {}, State extends {}> {
T extends Env,
Props extends {},
State extends {}
> extends EventBus {
readonly __owl__: Meta<Env, Props>; readonly __owl__: Meta<Env, Props>;
template: string = "default"; template?: string;
inlineTemplate: string | null = null;
/**
* The `el` is the root element of the widget. Note that it could be null:
* this is the case if the widget is not mounted yet, or is destroyed.
*/
get el(): HTMLElement | null { get el(): HTMLElement | null {
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null; return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
} }
@@ -63,6 +81,11 @@ export class Component<
env: T; env: T;
state?: State; state?: State;
props: Props; props: Props;
// type of props is not easily representable in typescript...
static props?: any;
static defaultProps?: any;
refs: { refs: {
[key: string]: Component<T, any, any> | HTMLElement | undefined; [key: string]: Component<T, any, any> | HTMLElement | undefined;
} = {}; } = {};
@@ -91,8 +114,13 @@ export class Component<
* the t-widget directive in a template) * the t-widget directive in a template)
*/ */
constructor(parent: Component<T, any, any> | T, props?: Props) { constructor(parent: Component<T, any, any> | T, props?: Props) {
super(); const defaultProps = (<any>this.constructor).defaultProps;
if (defaultProps) {
props = this._applyDefaultProps(props, defaultProps);
}
if (QWeb.dev) {
this._validateProps(props || {});
}
// is this a good idea? // is this a good idea?
// Pro: if props is empty, we can create easily a widget // Pro: if props is empty, we can create easily a widget
// Con: this is not really safe // Con: this is not really safe
@@ -110,7 +138,6 @@ export class Component<
this.__owl__ = { this.__owl__ = {
id: id, id: id,
vnode: null, vnode: null,
isStarted: false,
isMounted: false, isMounted: false,
isDestroyed: false, isDestroyed: false,
parent: p, parent: p,
@@ -119,7 +146,8 @@ export class Component<
renderId: 1, renderId: 1,
renderPromise: null, renderPromise: null,
renderProps: props || null, renderProps: props || null,
boundHandlers: {} boundHandlers: {},
mountedHandlers: {}
}; };
} }
@@ -203,10 +231,16 @@ export class Component<
// Public // Public
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
/**
* Mount the component to a target element.
*
* This should only be done if the component was created manually. Components
* created declaratively in templates are managed by the Owl system.
*/
async mount(target: HTMLElement): Promise<void> { async mount(target: HTMLElement): Promise<void> {
const vnode = await this._prepare(); const vnode = await this._prepare();
if (this.__owl__.isDestroyed) { if (this.__owl__.isDestroyed) {
// widget was destroyed before we get here... // component was destroyed before we get here...
return; return;
} }
this._patch(vnode); this._patch(vnode);
@@ -217,30 +251,6 @@ export class Component<
} }
} }
_callMounted() {
const children = this.__owl__.children;
for (let id in children) {
const comp = children[id];
if (!comp.__owl__.isMounted && this.el!.contains(comp.el)) {
comp._callMounted();
}
}
this.__owl__.isMounted = true;
this.mounted();
}
_callWillUnmount() {
this.willUnmount();
this.__owl__.isMounted = false;
const children = this.__owl__.children;
for (let id in children) {
const comp = children[id];
if (comp.__owl__.isMounted) {
comp._callWillUnmount();
}
}
}
unmount() { unmount() {
if (this.__owl__.isMounted) { if (this.__owl__.isMounted) {
this._callWillUnmount(); this._callWillUnmount();
@@ -249,7 +259,8 @@ export class Component<
} }
async render(force: boolean = false, patchQueue?: any[]): Promise<void> { async render(force: boolean = false, patchQueue?: any[]): Promise<void> {
if (!this.__owl__.isMounted) { const __owl__ = this.__owl__;
if (!__owl__.isMounted) {
return; return;
} }
const shouldPatch: boolean = !patchQueue; const shouldPatch: boolean = !patchQueue;
@@ -257,62 +268,54 @@ export class Component<
patchQueue = []; patchQueue = [];
} }
const renderVDom = this._render(force, patchQueue); const renderVDom = this._render(force, patchQueue);
const renderId = this.__owl__.renderId; const renderId = __owl__.renderId;
await renderVDom; await renderVDom;
if ( if (shouldPatch && __owl__.isMounted && renderId === __owl__.renderId) {
shouldPatch &&
this.__owl__.isMounted &&
renderId === this.__owl__.renderId
) {
// we only update the vnode and the actual DOM if no other rendering // we only update the vnode and the actual DOM if no other rendering
// occurred between now and when the render method was initially called. // occurred between now and when the render method was initially called.
for (let i = 0; i < patchQueue!.length; i++) { const patchLen = patchQueue!.length;
for (let i = 0; i < patchLen; i++) {
const patch = patchQueue![i]; const patch = patchQueue![i];
patch.push(patch[0].willPatch()); patch.push(patch[0].willPatch());
} }
for (let i = 0; i < patchQueue!.length; i++) { for (let i = 0; i < patchLen; i++) {
const patch = patchQueue![i]; const patch = patchQueue![i];
patch[0]._patch(patch[1]); patch[0]._patch(patch[1]);
} }
for (let i = patchQueue!.length - 1; i >= 0; i--) { for (let i = patchLen - 1; i >= 0; i--) {
const patch = patchQueue![i]; const patch = patchQueue![i];
patch[0].patched(patch[2]); patch[0].patched(patch[2]);
} }
} }
} }
/**
* Destroy the component. This operation is quite complex:
* - it recursively destroy all children
* - call the willUnmount hooks if necessary
* - remove the dom node from the dom
*
* This should only be called manually if you created the widget. Most widgets
* will be automatically destroyed.
*/
destroy() { destroy() {
if (!this.__owl__.isDestroyed) { const __owl__ = this.__owl__;
if (!__owl__.isDestroyed) {
const el = this.el; const el = this.el;
this._destroy(this.__owl__.parent); this._destroy(__owl__.parent);
if (el) { if (el) {
el.remove(); el.remove();
} }
} }
} }
_destroy(parent) { /**
const isMounted = this.__owl__.isMounted; * This method is called by the component system whenever its props are
if (isMounted) { * updated. If it returns true, then the component will be rendered.
this.willUnmount(); * Otherwise, it will skip the rendering (also, its props will not be updated)
this.__owl__.isMounted = false; */
}
const children = Object.values(this.__owl__.children);
for (let child of children) {
child._destroy(this);
}
if (parent) {
let id = this.__owl__.id;
delete parent.__owl__.children[id];
this.__owl__.parent = null;
}
this.clear();
this.__owl__.isDestroyed = true;
delete this.__owl__.vnode;
}
shouldUpdate(nextProps: Props): boolean { shouldUpdate(nextProps: Props): boolean {
return true; return true;
} }
@@ -327,23 +330,95 @@ export class Component<
* mode or not. * mode or not.
*/ */
async updateEnv(nextEnv: Partial<T>): Promise<void> { async updateEnv(nextEnv: Partial<T>): Promise<void> {
if (this.__owl__.parent && this.__owl__.parent.env === this.env) { const __owl__ = this.__owl__;
if (__owl__.parent && __owl__.parent.env === this.env) {
this.env = Object.create(this.env); this.env = Object.create(this.env);
} }
Object.assign(this.env, nextEnv); Object.assign(this.env, nextEnv);
if (this.__owl__.isMounted) { if (__owl__.isMounted) {
await this.render(true); await this.render(true);
} }
} }
/**
* Sets a key (from the state) to a specific value. This is mostly useful to
* work around the limitation in observed value with new keys.
*/
set(target: any, key: string | number, value: any) { set(target: any, key: string | number, value: any) {
this.__owl__.observer!.set(target, key, value); this.__owl__.observer!.set(target, key, value);
} }
/**
* Emit a custom event of type 'eventType' with the given 'payload' on the
* component's el, if it exists. However, note that the event will only bubble
* up to the parent DOM nodes. Thus, it must be called between mounted() and
* willUnmount().
*/
trigger(eventType: string, payload?: any) {
if (this.el) {
const ev = new CustomEvent(eventType, {
bubbles: true,
cancelable: true,
detail: payload
});
this.el.dispatchEvent(ev);
}
}
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
// Private // Private
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
_destroy(parent: Component<any, any, any> | null) {
const __owl__ = this.__owl__;
const isMounted = __owl__.isMounted;
if (isMounted) {
this.willUnmount();
__owl__.isMounted = false;
}
const children = __owl__.children;
for (let key in children) {
children[key]._destroy(this);
}
if (parent) {
let id = __owl__.id;
delete parent.__owl__.children[id];
__owl__.parent = null;
}
__owl__.isDestroyed = true;
delete __owl__.vnode;
}
_callMounted() {
const __owl__ = this.__owl__;
const children = __owl__.children;
for (let id in children) {
const comp = children[id];
if (!comp.__owl__.isMounted && this.el!.contains(comp.el)) {
comp._callMounted();
}
}
__owl__.isMounted = true;
const handlers = __owl__.mountedHandlers;
for (let key in handlers) {
handlers[key]();
}
this.mounted();
}
_callWillUnmount() {
this.willUnmount();
const __owl__ = this.__owl__;
__owl__.isMounted = false;
const children = __owl__.children;
for (let id in children) {
const comp = children[id];
if (comp.__owl__.isMounted) {
comp._callWillUnmount();
}
}
}
async _updateProps( async _updateProps(
nextProps: Props, nextProps: Props,
forceUpdate: boolean = false, forceUpdate: boolean = false,
@@ -351,6 +426,13 @@ export class Component<
): Promise<void> { ): Promise<void> {
const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps); const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
if (shouldUpdate) { if (shouldUpdate) {
const defaultProps = (<any>this.constructor).defaultProps;
if (defaultProps) {
nextProps = this._applyDefaultProps(nextProps, defaultProps);
}
if (QWeb.dev) {
this._validateProps(nextProps);
}
await this.willUpdateProps(nextProps); await this.willUpdateProps(nextProps);
this.props = nextProps; this.props = nextProps;
await this.render(forceUpdate, patchQueue); await this.render(forceUpdate, patchQueue);
@@ -358,54 +440,81 @@ export class Component<
} }
_patch(vnode) { _patch(vnode) {
this.__owl__.renderPromise = null; const __owl__ = this.__owl__;
const target = this.__owl__.vnode || document.createElement(vnode.sel!); __owl__.renderPromise = null;
this.__owl__.vnode = patch(target, vnode); const target = __owl__.vnode || document.createElement(vnode.sel!);
__owl__.vnode = patch(target, vnode);
} }
_prepare(): Promise<VNode> { _prepare(): Promise<VNode> {
this.__owl__.renderProps = this.props; const __owl__ = this.__owl__;
this.__owl__.renderPromise = this._prepareAndRender(); __owl__.renderProps = this.props;
return this.__owl__.renderPromise; __owl__.renderPromise = this._prepareAndRender();
return __owl__.renderPromise;
} }
async _prepareAndRender(): Promise<VNode> { async _prepareAndRender(): Promise<VNode> {
await this.willStart(); await this.willStart();
if (this.__owl__.isDestroyed) { const __owl__ = this.__owl__;
if (__owl__.isDestroyed) {
return Promise.resolve(h("div")); return Promise.resolve(h("div"));
} }
this.__owl__.isStarted = true; const qweb = this.env.qweb;
if (this.inlineTemplate) { if (!this.template) {
this.env.qweb.addTemplate(this.inlineTemplate, this.inlineTemplate, true); 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( __owl__.render = qweb.render.bind(qweb, this.template);
this.env.qweb,
this.inlineTemplate || this.template
);
this._observeState(); this._observeState();
return this._render(); return this._render();
} }
async _render( async _render(
force: boolean = false, force: boolean = false,
patchQueue: any[] = [] patchQueue: any[] = []
): Promise<VNode> { ): Promise<VNode> {
this.__owl__.renderId++; const __owl__ = this.__owl__;
__owl__.renderId++;
const promises: Promise<void>[] = []; const promises: Promise<void>[] = [];
const patch: any[] = [this]; const patch: any[] = [this];
if (this.__owl__.isMounted) { if (__owl__.isMounted) {
patchQueue.push(patch); patchQueue.push(patch);
} }
if (this.__owl__.observer) { if (__owl__.observer) {
this.__owl__.observer.allowMutations = false; __owl__.observer.allowMutations = false;
} }
let vnode = this.__owl__.render!(this, { let vnode = __owl__.render!(this, {
promises, promises,
handlers: this.__owl__.boundHandlers, handlers: __owl__.boundHandlers,
mountedHandlers: __owl__.mountedHandlers,
forceUpdate: force, forceUpdate: force,
patchQueue patchQueue
}); });
patch.push(vnode); patch.push(vnode);
if (this.__owl__.observer) { if (__owl__.observer) {
this.__owl__.observer.allowMutations = true; __owl__.observer.allowMutations = true;
} }
// this part is critical for the patching process to be done correctly. The // this part is critical for the patching process to be done correctly. The
@@ -413,39 +522,141 @@ export class Component<
// will update its own vnode representation without the knowledge of the // will update its own vnode representation without the knowledge of the
// parent widget. With this, we make sure that the parent widget will be // parent widget. With this, we make sure that the parent widget will be
// able to patch itself properly after // able to patch itself properly after
vnode.key = this.__owl__.id; vnode.key = __owl__.id;
this.__owl__.renderProps = this.props; __owl__.renderProps = this.props;
this.__owl__.renderPromise = Promise.all(promises).then(() => vnode); __owl__.renderPromise = Promise.all(promises).then(() => vnode);
return this.__owl__.renderPromise; return __owl__.renderPromise;
} }
/** /**
* Only called by qweb t-widget directive * Only called by qweb t-widget directive
*/ */
_mount(vnode: VNode, elm: HTMLElement): VNode { _mount(vnode: VNode, elm: HTMLElement): VNode {
this.__owl__.vnode = patch(elm, vnode); const __owl__ = this.__owl__;
if ( __owl__.vnode = patch(elm, vnode);
this.__owl__.parent && if (__owl__.parent!.__owl__.isMounted && !__owl__.isMounted) {
this.__owl__.parent.__owl__.isMounted &&
!this.__owl__.isMounted
) {
this._callMounted(); this._callMounted();
} }
return this.__owl__.vnode; return __owl__.vnode;
} }
__mount() { /**
if (!this.__owl__.isMounted) { * Only called by qweb t-widget directive (when t-keepalive is set)
this.__owl__.isMounted = true; */
_remount() {
const __owl__ = this.__owl__;
if (!__owl__.isMounted) {
__owl__.isMounted = true;
this.mounted(); this.mounted();
} }
} }
_observeState() { _observeState() {
if (this.state) { if (this.state) {
this.__owl__.observer = new Observer(); const __owl__ = this.__owl__;
this.__owl__.observer.observe(this.state); __owl__.observer = new Observer();
this.__owl__.observer.notifyCB = this.render.bind(this); __owl__.observer.observe(this.state);
__owl__.observer.notifyCB = this.render.bind(this);
}
}
/**
* Apply default props (only top level).
*
* Note that this method does not modify in place the props, it returns a new
* prop object
*/
_applyDefaultProps(props: Object | undefined, defaultProps: Object): Props {
props = props ? Object.create(props) : {};
for (let propName in defaultProps) {
if (props![propName] === undefined) {
props![propName] = defaultProps[propName];
}
}
return <Props>props;
}
/**
* Validate the component props (or next props) against the (static) props
* description. This is potentially an expensive operation: it may needs to
* visit recursively the props and all the children to check if they are valid.
* This is why it is only done in 'dev' mode.
*/
_validateProps(props: Object) {
const propsDef = (<any>this.constructor).props;
if (propsDef instanceof Array) {
// list of strings (prop names)
for (let i = 0, l = propsDef.length; i < l; i++) {
if (!(propsDef[i] in props)) {
throw new Error(
`Missing props '${propsDef[i]}' (widget '${this.constructor.name}')`
);
}
}
} else if (propsDef) {
// propsDef is an object now
for (let propName in propsDef) {
if (!(propName in props)) {
if (propsDef[propName] && !propsDef[propName].optional) {
throw new Error(
`Missing props '${propName}' (widget '${this.constructor.name}')`
);
} else {
break;
}
}
let isValid = isValidProp(props[propName], propsDef[propName]);
if (!isValid) {
throw new Error(
`Props '${propName}' of invalid type in widget '${
this.constructor.name
}'`
);
}
}
} }
} }
} }
//------------------------------------------------------------------------------
// Prop validation helper
//------------------------------------------------------------------------------
/**
* Check if an invidual prop value matches its (static) prop definition
*/
function isValidProp(prop, propDef): boolean {
if (typeof propDef === "function") {
// Check if a value is constructed by some Constructor. Note that there is a
// slight abuse of language: we want to consider primitive values as well.
//
// So, even though 1 is not an instance of Number, we want to consider that
// it is valid.
if (typeof prop === "object") {
return prop instanceof propDef;
}
return typeof prop === propDef.name.toLowerCase();
} else if (propDef instanceof Array) {
// If this code is executed, this means that we want to check if a prop
// matches at least one of its descriptor.
let result = false;
for (let i = 0, iLen = propDef.length; i < iLen; i++) {
result = result || isValidProp(prop, propDef[i]);
}
return result;
}
// propsDef is an object
let result = isValidProp(prop, propDef.type);
if (propDef.type === Array) {
for (let i = 0, iLen = prop.length; i < iLen; i++) {
result = result && isValidProp(prop[i], propDef.element);
}
}
if (propDef.type === Object) {
const shape = propDef.shape;
for (let key in shape) {
result = result && isValidProp(prop[key], shape[key]);
}
}
return result;
}
+2 -1
View File
@@ -64,7 +64,8 @@ export class EventBus {
*/ */
trigger(eventType: string, ...args: any[]) { trigger(eventType: string, ...args: any[]) {
const subs = this.subscriptions[eventType] || []; const subs = this.subscriptions[eventType] || [];
for (let sub of subs) { for (let i = 0, iLen = subs.length; i < iLen; i++) {
const sub = subs[i];
sub.callback.call(sub.owner, ...args); sub.callback.call(sub.owner, ...args);
} }
} }
+19 -1
View File
@@ -12,10 +12,28 @@ export { Observer } from "./observer";
// themselves in QWeb, otherwise these files will not even be loaded. // themselves in QWeb, otherwise these files will not even be loaded.
import "./qweb_directives"; import "./qweb_directives";
import "./qweb_extensions"; import "./qweb_extensions";
export { QWeb } from "./qweb_core"; import { QWeb } from "./qweb_core";
export { QWeb };
export { connect, Store } from "./store"; export { connect, Store } from "./store";
import * as _utils from "./utils"; import * as _utils from "./utils";
export const __info__ = {};
Object.defineProperty(__info__, "mode", {
get() {
return QWeb.dev ? "dev" : "prod";
},
set(mode: string) {
QWeb.dev = mode === "dev";
if (QWeb.dev) {
const url = `https://github.com/odoo/owl/blob/master/doc/tooling.md#development-mode`;
console.warn(
`Owl is running in 'dev' mode. This is not suitable for production use. See ${url} for more information.`
);
} else {
console.log(`Owl is now running in 'prod' mode.`)
}
}
});
export const utils = _utils; export const utils = _utils;
+29 -16
View File
@@ -30,15 +30,19 @@ const methodsToPatch = [
"sort", "sort",
"reverse" "reverse"
]; ];
const methodLen = methodsToPatch.length;
const ArrayProto = Array.prototype; const ArrayProto = Array.prototype;
const ModifiedArrayProto = Object.create(ArrayProto); const ModifiedArrayProto = Object.create(ArrayProto);
for (let method of methodsToPatch) { for (let i = 0; i < methodLen; i++) {
const method = methodsToPatch[i];
const initialMethod = ArrayProto[method]; const initialMethod = ArrayProto[method];
ModifiedArrayProto[method] = function(...args) { ModifiedArrayProto[method] = function(...args) {
if (!this.__observer__.allowMutations) { if (!this.__observer__.allowMutations) {
throw new Error(`Array cannot be changed here")`); throw new Error(`Array cannot be changed here")`);
} }
this.__observer__.rev++;
this.__observer__.notifyChange(); this.__observer__.notifyChange();
this.__owl__.rev++; this.__owl__.rev++;
let parent = this; let parent = this;
@@ -56,8 +60,8 @@ for (let method of methodsToPatch) {
break; break;
} }
if (inserted) { if (inserted) {
for (let elem of inserted) { for (let i = 0, iLen = inserted.length; i < iLen; i++) {
this.__observer__.observe(elem, this); this.__observer__.observe(inserted[i], this);
} }
} }
return initialMethod.call(this, ...args); return initialMethod.call(this, ...args);
@@ -74,7 +78,6 @@ export class Observer {
notifyCB() {} notifyCB() {}
notifyChange() { notifyChange() {
this.rev++;
this.dirty = true; this.dirty = true;
Promise.resolve().then(() => { Promise.resolve().then(() => {
if (this.dirty) { if (this.dirty) {
@@ -105,26 +108,32 @@ export class Observer {
} }
set(target: any, key: number | string, value: any) { set(target: any, key: number | string, value: any) {
this._addProp(target, key, value); let alreadyDefined =
target.__owl__.rev++; key in target &&
Object.getOwnPropertyDescriptor(target, key)!.configurable === false;
if (alreadyDefined) {
target[key] = value;
} else {
this._addProp(target, key, value);
this._updateRevNumber(target);
}
this.notifyChange(); this.notifyChange();
} }
_observeObj<T extends { __owl__?: any }>(obj: T, parent?: any) { _observeObj<T extends { __owl__?: any }>(obj: T, parent?: any) {
const keys = Object.keys(obj); obj.__owl__ = { rev: this.rev, deepRev: this.rev, parent };
obj.__owl__ = { rev: 1, deepRev: 1, parent };
Object.defineProperty(obj, "__owl__", { enumerable: false }); Object.defineProperty(obj, "__owl__", { enumerable: false });
for (let key of keys) { for (let key in obj) {
this._addProp(obj, key, obj[key]); this._addProp(obj, key, obj[key]);
} }
} }
_observeArr(arr: Array<any>, parent?: any) { _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 }); Object.defineProperty(arr, "__owl__", { enumerable: false });
(<any>arr).__proto__ = Object.create(ModifiedArrayProto); (<any>arr).__proto__ = Object.create(ModifiedArrayProto);
(<any>arr).__proto__.__observer__ = this; (<any>arr).__proto__.__observer__ = this;
for (let i = 0; i < arr.length; i++) { for (let i = 0, iLen = arr.length; i < iLen; i++) {
this.observe(arr[i], arr); this.observe(arr[i], arr);
} }
} }
@@ -147,17 +156,21 @@ export class Observer {
`Observed state cannot be changed here! (key: "${key}", val: "${newVal}")` `Observed state cannot be changed here! (key: "${key}", val: "${newVal}")`
); );
} }
self._updateRevNumber(obj);
value = newVal; value = newVal;
self.observe(newVal, obj); self.observe(newVal, obj);
obj.__owl__.rev!++;
let parent = obj;
do {
parent.__owl__.deepRev++;
} while ((parent = parent.__owl__.parent) && parent !== obj);
self.notifyChange(); self.notifyChange();
} }
} }
}); });
this.observe(value, obj); this.observe(value, obj);
} }
_updateRevNumber(target: any) {
this.rev++;
target.__owl__.rev!++;
let parent = target;
do {
parent.__owl__.deepRev++;
} while ((parent = parent.__owl__.parent) && parent !== target);
}
} }
+285 -187
View File
@@ -1,5 +1,4 @@
import { VNode, h } from "./vdom"; import { VNode, h } from "./vdom";
import { parseXML } from "./utils";
/** /**
* Owl QWeb Engine * Owl QWeb Engine
@@ -31,7 +30,6 @@ interface Template {
} }
interface CompilationInfo { interface CompilationInfo {
nodeID?: number;
node: Element; node: Element;
qweb: QWeb; qweb: QWeb;
ctx: Context; ctx: Context;
@@ -39,6 +37,11 @@ interface CompilationInfo {
value: string; value: string;
} }
interface NodeCreationCompilationInfo extends CompilationInfo {
nodeID: number;
addNodeHook: Function;
}
export interface Directive { export interface Directive {
name: string; name: string;
extraNames?: string[]; extraNames?: string[];
@@ -46,7 +49,7 @@ export interface Directive {
// if return true, then directive is fully applied and there is no need to // if return true, then directive is fully applied and there is no need to
// keep processing node. Otherwise, we keep going. // keep processing node. Otherwise, we keep going.
atNodeEncounter?(info: CompilationInfo): boolean | void; atNodeEncounter?(info: CompilationInfo): boolean | void;
atNodeCreation?(info: CompilationInfo): void; atNodeCreation?(info: NodeCreationCompilationInfo): void;
finalize?(info: CompilationInfo): void; finalize?(info: CompilationInfo): void;
} }
@@ -87,7 +90,20 @@ const DIRECTIVE_NAMES = {
const DIRECTIVES: Directive[] = []; const DIRECTIVES: Directive[] = [];
export const UTILS = { const NODE_HOOKS_PARAMS = {
create: "(_, n)",
insert: "vn",
remove: "(vn, rm)"
};
interface Utils {
h: typeof h;
objectToAttrString(obj: Object): string;
shallowEqual(p1: Object, p2: Object): boolean;
[key: string]: any;
}
export const UTILS: Utils = {
h: h, h: h,
objectToAttrString(obj: Object): string { objectToAttrString(obj: Object): string {
let classes: string[] = []; let classes: string[] = [];
@@ -97,163 +113,49 @@ export const UTILS = {
} }
} }
return classes.join(" "); return classes.join(" ");
},
shallowEqual(p1, p2) {
for (let k in p1) {
if (p1[k] !== p2[k]) {
return false;
}
}
return true;
} }
}; };
//------------------------------------------------------------------------------ function parseXML(xml: string): Document {
// Compilation Context const parser = new DOMParser();
//------------------------------------------------------------------------------ const doc = parser.parseFromString(xml, "text/xml");
export class Context { if (doc.getElementsByTagName("parsererror").length) {
nextID: number = 1; throw new Error("Invalid XML in template");
code: string[] = [];
variables: { [key: string]: any } = {};
definedVariables: { [key: string]: string } = {};
escaping: boolean = false;
parentNode: number | null = null;
rootNode: number | null = null;
indentLevel: number = 0;
rootContext: Context;
caller: Element | undefined;
shouldDefineOwner: boolean = false;
shouldProtectContext: boolean = false;
inLoop: boolean = false;
inPreTag: boolean = false;
templateName: string;
constructor(name?: string) {
this.rootContext = this;
this.templateName = name || "noname";
this.addLine("var h = this.utils.h;");
}
generateID(): number {
const id = this.rootContext.nextID++;
return id;
}
withParent(node: number): Context {
if (this === this.rootContext && this.parentNode) {
throw new Error("A template should not have more than one root node");
}
if (!this.rootContext.rootNode) {
this.rootContext.rootNode = node;
}
return this.subContext("parentNode", node);
}
subContext(key: keyof Context, value: any): Context {
const newContext = Object.create(this);
newContext[key] = value;
return newContext;
}
indent() {
this.indentLevel++;
}
dedent() {
this.indentLevel--;
}
addLine(line: string) {
const prefix = new Array(this.indentLevel + 2).join(" ");
this.code.push(prefix + line);
}
addIf(condition: string) {
this.addLine(`if (${condition}) {`);
this.indent();
}
addElse() {
this.dedent();
this.addLine("} else {");
this.indent();
}
closeIf() {
this.dedent();
this.addLine("}");
}
getValue(val: any): any {
return val in this.variables ? this.getValue(this.variables[val]) : val;
}
formatExpression(e: string): string {
e = e.trim();
if (e[0] === "{" && e[e.length - 1] === "}") {
const innerExpr = e
.slice(1, -1)
.split(",")
.map(p => {
let [key, val] = p.trim().split(":");
if (key === "") {
return "";
}
if (!val) {
val = key;
}
return `${key}: ${this.formatExpression(val)}`;
})
.join(",");
return "{" + innerExpr + "}";
}
// Thanks CHM for this code...
const chars = e.split("");
let instring = "";
let invar = "";
let invarPos = 0;
let r = "";
chars.push(" ");
for (var i = 0, ilen = chars.length; i < ilen; i++) {
var c = chars[i];
if (instring.length) {
if (c === instring && chars[i - 1] !== "\\") {
instring = "";
}
} else if (c === '"' || c === "'") {
instring = c;
} else if (c.match(/[a-zA-Z_\$]/) && !invar.length) {
invar = c;
invarPos = i;
continue;
} else if (c.match(/\W/) && invar.length) {
// TODO: Should check for possible spaces before dot
if (chars[invarPos - 1] !== "." && RESERVED_WORDS.indexOf(invar) < 0) {
if (!(invar in this.definedVariables)) {
invar =
WORD_REPLACEMENT[invar] ||
(invar in this.variables &&
this.formatExpression(this.variables[invar])) ||
"context['" + invar + "']";
}
}
r += invar;
invar = "";
} else if (invar.length) {
invar += c;
continue;
}
r += c;
}
const result = r.slice(0, -1);
return result;
} }
return doc;
} }
let nextID = 1;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// QWeb rendering engine // QWeb rendering engine
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
export class QWeb { export class QWeb {
templates: { [name: string]: Template } = {}; templates: { [name: string]: Template } = {};
utils = UTILS; utils = UTILS;
static widgets = Object.create(null);
// dev mode enables better error messages or more costly validations
static dev: boolean = false;
// the id field is useful to be able to hash qweb instances. The current
// use case is that component's templates are qweb dependant, and need to be
// able to map a qweb instance to a template name.
id = nextID++;
constructor(data?: string) { constructor(data?: string) {
if (data) { if (data) {
this.loadTemplates(data); this.addTemplates(data);
} }
this.addTemplate("default", "<div></div>");
} }
static addDirective(directive: Directive) { static addDirective(directive: Directive) {
@@ -265,18 +167,18 @@ export class QWeb {
} }
} }
static register(name: string, Component: any) {
if (QWeb.widgets[name]) {
throw new Error(`Component '${name}' has already been registered`);
}
QWeb.widgets[name] = Component;
}
/** /**
* Add a template to the internal template map. Note that it is not * Add a template to the internal template map. Note that it is not
* immediately compiled. * immediately compiled.
*/ */
addTemplate( addTemplate(name: string, xmlString: string) {
name: string,
xmlString: string,
allowDuplicates: boolean = false
) {
if (name in this.templates && allowDuplicates) {
return;
}
const doc = parseXML(xmlString); const doc = parseXML(xmlString);
if (!doc.firstChild) { if (!doc.firstChild) {
throw new Error("Invalid template (should not be empty)"); throw new Error("Invalid template (should not be empty)");
@@ -284,6 +186,23 @@ export class QWeb {
this._addTemplate(name, <Element>doc.firstChild); 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) { _addTemplate(name: string, elem: Element) {
if (name in this.templates) { if (name in this.templates) {
throw new Error(`Template ${name} already defined`); throw new Error(`Template ${name} already defined`);
@@ -341,22 +260,6 @@ export class QWeb {
} }
} }
} }
/**
* 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 * Render a template
* *
@@ -383,6 +286,12 @@ export class QWeb {
// pollute the rendering context by adding some keys in it. // pollute the rendering context by adding some keys in it.
ctx.code.unshift(" let owner = context;"); ctx.code.unshift(" let owner = context;");
} }
if (ctx.shouldDefineQWeb) {
ctx.code.unshift(" let QWeb = this.constructor;");
}
if (ctx.shouldDefineUtils) {
ctx.code.unshift(" let utils = this.utils;");
}
if (!ctx.rootNode) { if (!ctx.rootNode) {
throw new Error("A template should have one root node"); throw new Error("A template should have one root node");
@@ -396,11 +305,14 @@ export class QWeb {
ctx.code.join("\n") ctx.code.join("\n")
) as CompiledTemplate; ) as CompiledTemplate;
} catch (e) { } catch (e) {
const templateName = ctx.templateName.replace(/`/g, "'");
console.groupCollapsed(`Invalid Code generated by ${templateName}`);
console.warn(ctx.code.join("\n"));
console.groupEnd();
throw new Error( throw new Error(
`Invalid generated code while compiling template '${ctx.templateName.replace( `Invalid generated code while compiling template '${templateName}': ${
/`/g, e.message
"'" }`
)}': ${e.message}`
); );
} }
if (isDebug) { if (isDebug) {
@@ -461,17 +373,20 @@ export class QWeb {
} }
} }
for (let directive of DIRECTIVES) { const DIR_N = DIRECTIVES.length;
const ATTR_N = attributes.length;
for (let i = 0; i < DIR_N; i++) {
let directive = DIRECTIVES[i];
let fullName; let fullName;
let value; let value;
for (let i = 0; i < attributes.length; i++) { for (let j = 0; j < ATTR_N; j++) {
const name = attributes[i].name; const name = attributes[j].name;
if ( if (
name === "t-" + directive.name || name === "t-" + directive.name ||
name.startsWith("t-" + directive.name + "-") name.startsWith("t-" + directive.name + "-")
) { ) {
fullName = name; fullName = name;
value = attributes[i].textContent; value = attributes[j].textContent;
validDirectives.push({ directive, value, fullName }); validDirectives.push({ directive, value, fullName });
if (directive.name === "on") { if (directive.name === "on") {
withHandlers = true; withHandlers = true;
@@ -497,6 +412,11 @@ export class QWeb {
if (node.nodeName !== "t") { if (node.nodeName !== "t") {
let nodeID = this._compileGenericNode(node, ctx, withHandlers); let nodeID = this._compileGenericNode(node, ctx, withHandlers);
ctx = ctx.withParent(nodeID); 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) { for (let { directive, value, fullName } of validDirectives) {
if (directive.atNodeCreation) { if (directive.atNodeCreation) {
@@ -506,10 +426,23 @@ export class QWeb {
ctx, ctx,
fullName, fullName,
value, value,
nodeID 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") { if (node.nodeName === "pre") {
ctx = ctx.subContext("inPreTag", true); ctx = ctx.subContext("inPreTag", true);
@@ -620,18 +553,13 @@ export class QWeb {
// attribute contains 'non letters' => we want to quote it // attribute contains 'non letters' => we want to quote it
attName = '"' + attName + '"'; attName = '"' + attName + '"';
} }
const formattedExpr = value!.replace( const formattedExpr = ctx.interpolate(value);
/\{\{.*?\}\}/g,
s => "${" + ctx.formatExpression(s.slice(2, -2)) + "}"
);
const attID = ctx.generateID(); const attID = ctx.generateID();
let staticVal = (<Element>node).getAttribute(attName); let staticVal = (<Element>node).getAttribute(attName);
if (staticVal) { if (staticVal) {
ctx.addLine( ctx.addLine(`var _${attID} = '${staticVal} ' + ${formattedExpr};`);
`var _${attID} = '${staticVal} ' + \`${formattedExpr}\`;`
);
} else { } else {
ctx.addLine(`var _${attID} = \`${formattedExpr}\`;`); ctx.addLine(`var _${attID} = ${formattedExpr};`);
} }
attrs.push(`${attName}: _${attID}`); attrs.push(`${attName}: _${attID}`);
} }
@@ -661,7 +589,7 @@ export class QWeb {
parts.push(`on:{}`); parts.push(`on:{}`);
} }
ctx.addLine(`var c${nodeID} = [], p${nodeID} = {${parts.join(",")}};`); ctx.addLine(`let c${nodeID} = [], p${nodeID} = {${parts.join(",")}};`);
for (let id of tattrs) { for (let id of tattrs) {
ctx.addIf(`_${id} instanceof Array`); ctx.addIf(`_${id} instanceof Array`);
ctx.addLine(`p${nodeID}.attrs[_${id}[0]] = _${id}[1];`); ctx.addLine(`p${nodeID}.attrs[_${id}[0]] = _${id}[1];`);
@@ -691,3 +619,173 @@ export class QWeb {
} }
} }
} }
//------------------------------------------------------------------------------
// Compilation Context
//------------------------------------------------------------------------------
export class Context {
nextID: number = 1;
code: string[] = [];
variables: { [key: string]: any } = {};
definedVariables: { [key: string]: string } = {};
escaping: boolean = false;
parentNode: number | null = null;
rootNode: number | null = null;
indentLevel: number = 0;
rootContext: Context;
caller: Element | undefined;
shouldDefineOwner: boolean = false;
shouldDefineQWeb: boolean = false;
shouldDefineUtils: boolean = false;
shouldProtectContext: boolean = false;
inLoop: boolean = false;
inPreTag: boolean = false;
templateName: string;
constructor(name?: string) {
this.rootContext = this;
this.templateName = name || "noname";
this.addLine("var h = this.utils.h;");
}
generateID(): number {
const id = this.rootContext.nextID++;
return id;
}
withParent(node: number): Context {
if (this === this.rootContext && this.parentNode) {
throw new Error("A template should not have more than one root node");
}
if (!this.rootContext.rootNode) {
this.rootContext.rootNode = node;
}
return this.subContext("parentNode", node);
}
subContext(key: keyof Context, value: any): Context {
const newContext = Object.create(this);
newContext[key] = value;
return newContext;
}
indent() {
this.indentLevel++;
}
dedent() {
this.indentLevel--;
}
addLine(line: string) {
const prefix = new Array(this.indentLevel + 2).join(" ");
this.code.push(prefix + line);
}
addIf(condition: string) {
this.addLine(`if (${condition}) {`);
this.indent();
}
addElse() {
this.dedent();
this.addLine("} else {");
this.indent();
}
closeIf() {
this.dedent();
this.addLine("}");
}
getValue(val: any): any {
return val in this.variables ? this.getValue(this.variables[val]) : val;
}
formatExpression(e: string): string {
e = e.trim();
if (e[0] === "{" && e[e.length - 1] === "}") {
const innerExpr = e
.slice(1, -1)
.split(",")
.map(p => {
let [key, val] = p.trim().split(":");
if (key === "") {
return "";
}
if (!val) {
val = key;
}
return `${key}: ${this.formatExpression(val)}`;
})
.join(",");
return "{" + innerExpr + "}";
}
// Thanks CHM for this code...
const chars = e.split("");
let instring = "";
let invar = "";
let invarPos = 0;
let r = "";
chars.push(" ");
for (let i = 0, ilen = chars.length; i < ilen; i++) {
let c = chars[i];
if (instring.length) {
if (c === instring && chars[i - 1] !== "\\") {
instring = "";
}
} else if (c === '"' || c === "'") {
instring = c;
} else if (c.match(/[a-zA-Z_\$]/) && !invar.length) {
invar = c;
invarPos = i;
continue;
} else if (c.match(/\W/) && invar.length) {
// TODO: Should check for possible spaces before dot
if (chars[invarPos - 1] !== "." && RESERVED_WORDS.indexOf(invar) < 0) {
if (!(invar in this.definedVariables)) {
invar =
WORD_REPLACEMENT[invar] ||
(invar in this.variables &&
this.formatExpression(this.variables[invar])) ||
"context['" + invar + "']";
}
}
r += invar;
invar = "";
} else if (invar.length) {
invar += c;
continue;
}
r += c;
}
const result = r.slice(0, -1);
return result;
}
/**
* Perform string interpolation on the given string. Note that if the whole
* string is an expression, it simply returns it (formatted and enclosed in
* parentheses).
* For instance:
* 'Hello {{x}}!' -> `Hello ${x}`
* '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b')
*/
interpolate(s: string): string {
let matches = s.match(/\{\{.*?\}\}/g);
if (matches && matches[0].length === s.length) {
return `(${this.formatExpression(s.slice(2, -2))})`;
}
matches = s.match(/\#\{.*?\}/g);
if (matches && matches[0].length === s.length) {
return `(${this.formatExpression(s.slice(2, -1))})`;
}
let formatter = expr => "${" + this.formatExpression(expr) + "}";
let r = s
.replace(/\{\{.*?\}\}/g, s => formatter(s.slice(2, -2)))
.replace(/\#\{.*?\}/g, s => formatter(s.slice(2, -1)));
return "`" + r + "`";
}
}
+3 -2
View File
@@ -250,14 +250,15 @@ QWeb.addDirective({
ctx.addLine( ctx.addLine(
`var _${keysID} = _${arrayID} instanceof Array ? _${arrayID} : Object.keys(_${arrayID});` `var _${keysID} = _${arrayID} instanceof Array ? _${arrayID} : Object.keys(_${arrayID});`
); );
ctx.addLine(`var _length${keysID} = _${keysID}.length;`);
let valuesID = ctx.generateID(); let valuesID = ctx.generateID();
ctx.addLine( ctx.addLine(
`var _${valuesID} = _${arrayID} instanceof Array ? _${arrayID} : Object.values(_${arrayID});` `var _${valuesID} = _${arrayID} instanceof Array ? _${arrayID} : Object.values(_${arrayID});`
); );
ctx.addLine(`for (let i = 0; i < _${keysID}.length; i++) {`); ctx.addLine(`for (let i = 0; i < _length${keysID}; i++) {`);
ctx.indent(); ctx.indent();
ctx.addLine(`context.${name}_first = i === 0;`); ctx.addLine(`context.${name}_first = i === 0;`);
ctx.addLine(`context.${name}_last = i === _${keysID}.length - 1;`); ctx.addLine(`context.${name}_last = i === _length${keysID} - 1;`);
ctx.addLine(`context.${name}_parity = i % 2 === 0 ? 'even' : 'odd';`); ctx.addLine(`context.${name}_parity = i % 2 === 0 ? 'even' : 'odd';`);
ctx.addLine(`context.${name}_index = i;`); ctx.addLine(`context.${name}_index = i;`);
ctx.addLine(`context.${name} = _${keysID}[i];`); ctx.addLine(`context.${name} = _${keysID}[i];`);
+375 -98
View File
@@ -9,39 +9,63 @@ import { QWeb, UTILS } from "./qweb_core";
* - t-on * - t-on
* - t-ref * - t-ref
* - t-transition * - t-transition
* - t-widget/t-props/t-keepalive * - t-widget/t-keepalive
* - t-mounted
*/ */
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// t-on // t-on
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// these are pieces of code that will be injected into the event handler if
// modifiers are specified
const MODS_CODE = {
prevent: "e.preventDefault();",
self: "if (e.target !== this.elm) {return}",
stop: "e.stopPropagation();"
};
QWeb.addDirective({ QWeb.addDirective({
name: "on", name: "on",
priority: 90, priority: 90,
atNodeCreation({ ctx, fullName, value, nodeID }) { atNodeCreation({ ctx, fullName, value, nodeID }) {
ctx.rootContext.shouldDefineOwner = true; ctx.rootContext.shouldDefineOwner = true;
const eventName = fullName.slice(5); const [eventName, ...mods] = fullName.slice(5).split(".");
if (!eventName) { if (!eventName) {
throw new Error("Missing event name with t-on directive"); throw new Error("Missing event name with t-on directive");
} }
let extraArgs; let extraArgs;
let handler = value.replace(/\(.*\)/, function(args) { let handlerName = value.replace(/\(.*\)/, function(args) {
extraArgs = args.slice(1, -1); extraArgs = args.slice(1, -1);
return ""; return "";
}); });
let error = `(function () {throw new Error('Missing handler \\'' + '${handler}' + \`\\' when evaluating template '${ctx.templateName.replace( ctx.addIf(`!context['${handlerName}']`);
/`/g, ctx.addLine(
"'" `throw new Error('Missing handler \\'' + '${handlerName}' + \`\\' when evaluating template '${ctx.templateName.replace(
)}'\`)})()`; /`/g,
"'"
)}'\`)`
);
ctx.closeIf();
let params = extraArgs
? `owner, ${ctx.formatExpression(extraArgs)}`
: "owner";
let handler;
if (mods.length > 0) {
handler = `function (e) {`;
handler += mods
.map(function(mod) {
return MODS_CODE[mod];
})
.join("");
handler += `context['${handlerName}'].call(${params}, e);}`;
} else {
handler = `context['${handlerName}'].bind(${params})`;
}
if (extraArgs) { if (extraArgs) {
ctx.addLine( ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
`p${nodeID}.on['${eventName}'] = (context['${handler}'] || ${error}).bind(owner, ${ctx.formatExpression(
extraArgs
)});`
);
} else { } else {
ctx.addLine( ctx.addLine(
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || (context['${handler}'] || ${error}).bind(owner);` `extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`
); );
ctx.addLine( ctx.addLine(
`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];` `p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`
@@ -56,28 +80,23 @@ QWeb.addDirective({
QWeb.addDirective({ QWeb.addDirective({
name: "ref", name: "ref",
priority: 95, priority: 95,
atNodeCreation({ ctx, nodeID, value }) { atNodeCreation({ ctx, value, addNodeHook }) {
const refKey = `ref${ctx.generateID()}`; const refKey = `ref${ctx.generateID()}`;
ctx.addLine(`const ${refKey} = ${ctx.formatExpression(value)}`); ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
ctx.addLine(`p${nodeID}.hook = { addNodeHook("create", `context.refs[${refKey}] = n.elm;`);
create: (_, n) => context.refs[${refKey}] = n.elm,
};`);
} }
}); });
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// t-transition // t-transition
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
(<any>UTILS).nextFrame = function(cb: () => void) { UTILS.nextFrame = function(cb: () => void) {
requestAnimationFrame(() => requestAnimationFrame(cb)); requestAnimationFrame(() => requestAnimationFrame(cb));
}; };
(<any>UTILS).transitionCreate = function(elm: HTMLElement, name: string) { UTILS.transitionInsert = function(elm: HTMLElement, name: string) {
elm.classList.add(name + "-enter"); elm.classList.add(name + "-enter");
elm.classList.add(name + "-enter-active"); elm.classList.add(name + "-enter-active");
};
(<any>UTILS).transitionInsert = function(elm: HTMLElement, name: string) {
const finalize = () => { const finalize = () => {
elm.classList.remove(name + "-enter-active"); elm.classList.remove(name + "-enter-active");
elm.classList.remove(name + "-enter-to"); elm.classList.remove(name + "-enter-to");
@@ -89,7 +108,7 @@ QWeb.addDirective({
}); });
}; };
(<any>UTILS).transitionRemove = function( UTILS.transitionRemove = function(
elm: HTMLElement, elm: HTMLElement,
name: string, name: string,
rm: () => void rm: () => void
@@ -98,7 +117,7 @@ QWeb.addDirective({
elm.classList.add(name + "-leave-active"); elm.classList.add(name + "-leave-active");
const finalize = () => { const finalize = () => {
elm.classList.remove(name + "-leave-active"); elm.classList.remove(name + "-leave-active");
elm.classList.remove(name + "-enter-to"); elm.classList.remove(name + "-leave-to");
rm(); rm();
}; };
this.nextFrame(() => { this.nextFrame(() => {
@@ -147,25 +166,187 @@ function whenTransitionEnd(elm: HTMLElement, cb) {
QWeb.addDirective({ QWeb.addDirective({
name: "transition", name: "transition",
priority: 96, priority: 96,
atNodeCreation({ ctx, value, nodeID }) { atNodeCreation({ value, addNodeHook }) {
let name = value; let name = value;
ctx.addLine(`p${nodeID}.hook = { const hooks = {
create: (_, n) => { insert: `this.utils.transitionInsert(vn.elm, '${name}');`,
this.utils.transitionCreate(n.elm, '${name}'); remove: `this.utils.transitionRemove(vn.elm, '${name}', rm);`
}, };
insert: vn => { for (let hookName in hooks) {
this.utils.transitionInsert(vn.elm, '${name}'); addNodeHook(hookName, hooks[hookName]);
}, }
remove: (vn, rm) => {
this.utils.transitionRemove(vn.elm, '${name}', rm);
}
};`);
} }
}); });
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// t-widget // t-widget
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const T_WIDGET_MODS_CODE = Object.assign({}, MODS_CODE, {
self: "if (e.target !== vn.elm) {return}"
});
/**
* The t-widget directive is certainly a complicated and hard to maintain piece
* of code. To help you, fellow developer, if you have to maintain it, I offer
* you this advice: Good luck...
*
* Since it is not 'direct' code, but rather code that generates other code, it
* is not easy to understand. To help you, here is a detailed and commented
* explanation of the code generated by the t-widget directive for the following
* situation:
* ```xml
* <t t-widget="child"
* t-key="'somestring'"
* flag="state.flag"
* t-transition="fade"/>
* ```
*
* ```js
* // we assign utils on top of the function because it will be useful for
* // each widgets
* let utils = this.utils;
*
* // this is the virtual node representing the parent div
* let c1 = [], p1 = { key: 1 };
* var vn1 = h("div", p1, c1);
*
* // t-widget directive: we start by evaluating the expression given by t-key:
* let key5 = "somestring";
*
* // 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 promise that will contain later either the new widget
* // creation, or the props update...
* let def3;
*
* // this is kind of tricky: we need here to find if the widget was already
* // created by a previous rendering. This is done by checking the internal
* // `cmap` (children map) of the parent widget: it maps keys to widget ids,
* // and, then, if there is an id, we look into the children list to get the
* // instance
* let w4 =
* key5 in context.__owl__.cmap
* ? context.__owl__.children[context.__owl__.cmap[key5]]
* : false;
*
* // we evaluate here the props given to the component. It is done here to be
* // able to easily reference it later, and also, it might be an expensive
* // computation, so it is certainly better to do it only once
* let props4 = { flag: context["state"].flag };
*
* // If we have a widget, currently rendering, but not ready yet, we do not want
* // to wait for it to be ready if we can avoid it
* if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
* // we check if the props are the same. In that case, we can simply reuse
* // the previous rendering and skip all useless work
* if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
* def3 = w4.__owl__.renderPromise;
* } else {
* // if the props are not the same, we destroy the widget and starts anew.
* // this will be faster than waiting for its rendering, then updating it
* w4.destroy();
* w4 = false;
* }
* }
*
* if (!w4) {
* // in this situation, we need to create a new widget. First step is
* // to get a reference to the class, then create an instance with
* // current context as parent, and the props.
* let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
* if (!W4) {
* throw new Error("Cannot find the definition of widget 'child'");
* }
* w4 = new W4(owner, props4);
*
* // Whenever we rerender the parent widget, we need to be sure that we
* // are able to find the widget instance. To do that, we register it to
* // the parent cmap (children map). Note that the 'template' key is
* // used here, since this is what identify the widget from the template
* // perspective.
* context.__owl__.cmap[key5] = w4.__owl__.id;
*
* // _prepare is called, to basically call willStart, then render the
* // widget
* def3 = w4._prepare();
*
* def3 = def3.then(vnode => {
* // we create here a virtual node for the parent (NOT the widget). This
* // means that the vdom of the parent will be stopped here, and from
* // the parent's perspective, it simply is a vnode with no children.
* // However, it shares the same dom element with the component root
* // vnode.
* let pvnode = h(vnode.sel, { key: key5 });
*
* // we add hooks to the parent vnode so we can interact with the new
* // widget at the proper time
* pvnode.data.hook = {
* insert(vn) {
* // the _mount method will patch the widget vdom into the elm vn.elm,
* // then call the mounted hooks. However, suprisingly, the snabbdom
* // patch method actually replace the elm by a new elm, so we need
* // to synchronise the pvnode elm with the resulting elm
* let nvn = w4._mount(vnode, vn.elm);
* pvnode.elm = nvn.elm;
* // what follows is only present if there are animations on the widget
* utils.transitionInsert(vn.elm, "fade");
* },
* remove() {
* // override with empty function to prevent from removing the node
* // directly. It will be removed when destroy is called anyway, which
* // delays the removal if there are animations.
* },
* destroy() {
* // if there are animations, we delay the call to destroy on the
* // widget, if not, we call it directly.
* let finalize = () => {
* w4.destroy();
* };
* utils.transitionRemove(vn.elm, "fade", finalize);
* }
* };
* // the pvnode is inserted at the correct position in the div's children
* c1[_2_index] = pvnode;
*
* // we keep here a reference to the parent vnode (representing the
* // widget, so we can reuse it later whenever we update the widget
* w4.__owl__.pvnode = pvnode;
* });
* } else {
* // this is the 'update' path of the directive.
* // the call to _updateProps is the actual widget update
* // Note that we only update the props if we cannot reuse the previous
* // rendering work (in the case it was rendered with the same props)
* def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
* def3 = def3.then(() => {
* // if widget was destroyed in the meantime, we do nothing (so, this
* // means that the parent's element children list will have a null in
* // the widget's position, which will cause the pvnode to be removed
* // when it is patched.
* if (w4.__owl__.isDestroyed) {
* return;
* }
* // like above, we register the pvnode to the children list, so it
* // will not be patched out of the dom.
* let pvnode = w4.__owl__.pvnode;
* c1[_2_index] = pvnode;
* });
* }
*
* // we register the deferred here so the parent can coordinate its patch operation
* // with all the children.
* extra.promises.push(def3);
* return vn1;
* ```
*/
QWeb.addDirective({ QWeb.addDirective({
name: "widget", name: "widget",
extraNames: ["props", "keepalive"], extraNames: ["props", "keepalive"],
@@ -173,16 +354,33 @@ QWeb.addDirective({
atNodeEncounter({ ctx, value, node }): boolean { atNodeEncounter({ ctx, value, node }): boolean {
ctx.addLine("//WIDGET"); ctx.addLine("//WIDGET");
ctx.rootContext.shouldDefineOwner = true; ctx.rootContext.shouldDefineOwner = true;
let props = node.getAttribute("t-props"); ctx.rootContext.shouldDefineQWeb = true;
ctx.rootContext.shouldDefineUtils = true;
let keepAlive = node.getAttribute("t-keepalive") ? true : false; let keepAlive = node.getAttribute("t-keepalive") ? true : false;
// t-on- events... // t-on- events and t-transition
const events: [string, string][] = []; const events: [string, string[], string, string][] = [];
let transition: string = "";
const attributes = (<Element>node).attributes; const attributes = (<Element>node).attributes;
const props: { [key: string]: string } = {};
for (let i = 0; i < attributes.length; i++) { for (let i = 0; i < attributes.length; i++) {
const name = attributes[i].name; const name = attributes[i].name;
const value = attributes[i].textContent!;
if (name.startsWith("t-on-")) { if (name.startsWith("t-on-")) {
events.push([name.slice(5), attributes[i].textContent!]); const [eventName, ...mods] = name.slice(5).split(".");
let extraArgs;
let handlerName = value.replace(/\(.*\)/, function(args) {
extraArgs = args.slice(1, -1);
return "";
});
events.push([eventName, mods, handlerName, extraArgs]);
} else if (name === "t-transition") {
transition = value;
} else if (!name.startsWith("t-")) {
if (name !== "class" && name !== "style") {
// this is a prop!
props[name] = ctx.formatExpression(value);
}
} }
} }
@@ -190,9 +388,11 @@ QWeb.addDirective({
if (key) { if (key) {
key = ctx.formatExpression(key); key = ctx.formatExpression(key);
} }
if (props) {
props = ctx.formatExpression(props); // computing the props string representing the props object
} let propStr = Object.keys(props)
.map(k => k + ":" + props[k])
.join(",");
let dummyID = ctx.generateID(); let dummyID = ctx.generateID();
let defID = ctx.generateID(); let defID = ctx.generateID();
let widgetID = ctx.generateID(); let widgetID = ctx.generateID();
@@ -210,62 +410,32 @@ QWeb.addDirective({
: ctx.inLoop : ctx.inLoop
? `String(-${widgetID} - i)` ? `String(-${widgetID} - i)`
: String(widgetID); : 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 ref = node.getAttribute("t-ref");
let refExpr = ""; let refExpr = "";
let refKey: string = ""; let refKey: string = "";
if (ref) { if (ref) {
refKey = `ref${ctx.generateID()}`; refKey = `ref${ctx.generateID()}`;
ctx.addLine(`const ${refKey} = ${ctx.formatExpression(ref)}`); ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`);
refExpr = `context.refs[${refKey}] = w${widgetID};`; refExpr = `context.refs[${refKey}] = w${widgetID};`;
} }
let transitionsInsertCode = "";
if (transition) {
transitionsInsertCode = `utils.transitionInsert(vn.elm, '${transition}');`;
}
let finalizeWidgetCode = `w${widgetID}.${ let finalizeWidgetCode = `w${widgetID}.${
keepAlive ? "unmount" : "destroy" keepAlive ? "unmount" : "destroy"
}()`; }();`;
if (ref) { if (ref) {
finalizeWidgetCode += `;delete context.refs[${refKey}]`; finalizeWidgetCode += `delete context.refs[${refKey}];`; // FIXME: shouldn't we keep ref if keepAlive is true?
} }
if (transition) {
finalizeWidgetCode = `let finalize = () => {
${finalizeWidgetCode}
};
utils.transitionRemove(vn.elm, '${transition}', finalize);`;
}
let createHook = ""; let createHook = "";
let classAttr = node.getAttribute("class"); let classAttr = node.getAttribute("class");
let tattClass = node.getAttribute("t-att-class"); let tattClass = node.getAttribute("t-att-class");
@@ -277,7 +447,7 @@ QWeb.addDirective({
tattStyle = attVar; tattStyle = attVar;
} }
let updateClassCode = ""; let updateClassCode = "";
if (classAttr || tattClass || styleAttr || tattStyle) { if (classAttr || tattClass || styleAttr || tattStyle || events.length) {
let classCode = ""; let classCode = "";
if (classAttr) { if (classAttr) {
classCode = classCode =
@@ -296,32 +466,139 @@ QWeb.addDirective({
}`; }`;
updateClassCode = `let cl=w${widgetID}.el.classList;for (let k in ${attVar}) {if (${attVar}[k]) {cl.add(k)} else {cl.remove(k)}}`; updateClassCode = `let cl=w${widgetID}.el.classList;for (let k in ${attVar}) {if (${attVar}[k]) {cl.add(k)} else {cl.remove(k)}}`;
} }
let eventsCode = events
.map(function([eventName, mods, handlerName, extraArgs]) {
let params = extraArgs
? `owner, ${ctx.formatExpression(extraArgs)}`
: "owner";
let handler;
if (mods.length > 0) {
handler = `function (e) {`;
handler += mods
.map(function(mod) {
return T_WIDGET_MODS_CODE[mod];
})
.join("");
handler += `owner['${handlerName}'].call(${params}, e);}`;
} else {
handler = `owner['${handlerName}'].bind(${params})`;
}
return `vn.elm.addEventListener('${eventName}', ${handler});`;
})
.join("");
const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false); const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false);
const styleCode = styleExpr ? `vn.elm.style = ${styleExpr}` : ""; const styleCode = styleExpr ? `vn.elm.style = ${styleExpr};` : "";
createHook = `vnode.data.hook = {create(_, vn){${classCode}${styleCode}}};`; createHook = `vnode.data.hook = {create(_, vn){${classCode}${styleCode}${eventsCode}}};`;
} }
ctx.addIf(`isNew${widgetID}`);
ctx.addLine( ctx.addLine(
`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}});c${ `let w${widgetID} = ${templateID} in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[${templateID}]] : false;`
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;});`
); );
ctx.addLine(`let props${widgetID} = {${propStr}};`);
ctx.addIf(
`w${widgetID} && w${widgetID}.__owl__.renderPromise && !w${widgetID}.__owl__.vnode`
);
ctx.addIf(
`utils.shallowEqual(props${widgetID}, w${widgetID}.__owl__.renderProps)`
);
ctx.addLine(`def${defID} = w${widgetID}.__owl__.renderPromise;`);
ctx.addElse(); ctx.addElse();
ctx.addLine(`w${widgetID}.destroy();`);
ctx.addLine(`w${widgetID} = false;`);
ctx.closeIf();
ctx.closeIf();
ctx.addIf(`!w${widgetID}`);
// new widget
ctx.addLine(`let widgetKey${widgetID} = ${ctx.interpolate(value)};`);
ctx.addLine(
`let W${widgetID} = context.widgets && context.widgets[widgetKey${widgetID}] || QWeb.widgets[widgetKey${widgetID}];`
);
// maybe only do this in dev mode...
ctx.addLine(
`if (!W${widgetID}) {throw new Error('Cannot find the definition of widget "' + widgetKey${widgetID} + '"')}`
);
ctx.addLine(`w${widgetID} = new W${widgetID}(owner, props${widgetID});`);
ctx.addLine(
`context.__owl__.cmap[${templateID}] = w${widgetID}.__owl__.id;`
);
ctx.addLine(`def${defID} = w${widgetID}._prepare();`);
// hack: specify empty remove hook to prevent the node from being removed from the DOM
// FIXME: click to re-add widget during remove transition -> leak
ctx.addLine(
`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${widgetID}._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeWidgetCode}}}});c${
ctx.parentNode
}[_${dummyID}_index]=pvnode;w${widgetID}.__owl__.pvnode = pvnode;});`
);
ctx.addElse();
// need to update widget
ctx.addLine(
`def${defID} = def${defID} || w${widgetID}._updateProps(props${widgetID}, extra.forceUpdate, extra.patchQueue);`
);
let keepAliveCode = "";
if (keepAlive) {
keepAliveCode = `pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w${widgetID}.el,vn.elm);vn.elm=w${widgetID}.el;w${widgetID}._remount();};`;
}
ctx.addLine( ctx.addLine(
`def${defID} = def${defID}.then(()=>{if (w${widgetID}.__owl__.isDestroyed) {return};${ `def${defID} = def${defID}.then(()=>{if (w${widgetID}.__owl__.isDestroyed) {return};${
tattStyle ? `w${widgetID}.el.style=${tattStyle};` : "" 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 ctx.parentNode
}[_${dummyID}_index]=vnode;});` }[_${dummyID}_index]=pvnode;});`
); );
ctx.closeIf(); ctx.closeIf();
ctx.addLine(`extra.promises.push(def${defID});`); 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(); ctx.closeIf();
} }
return true; 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]; const func: (...any) => any = entry[1];
this.getters[name] = payload => { this.getters[name] = payload => {
return func({ state: this.state, getters: this.getters }, payload); return func({ state: this.state, getters: this.getters }, payload);
} };
} }
} }
@@ -154,6 +154,8 @@ interface EnvWithStore extends Env {
store: Store; store: Store;
} }
let nextID = 1;
export function connect(mapStateToProps, options: any = {}) { export function connect(mapStateToProps, options: any = {}) {
let hashFunction = options.hashFunction || null; let hashFunction = options.hashFunction || null;
@@ -184,7 +186,7 @@ export function connect(mapStateToProps, options: any = {}) {
return function<E extends EnvWithStore, P, S>( return function<E extends EnvWithStore, P, S>(
Comp: Constructor<Component<E, P, S>> Comp: Constructor<Component<E, P, S>>
) { ) {
return class extends Comp { const Result = class extends Comp {
constructor(parent, props?: any) { constructor(parent, props?: any) {
const env = parent instanceof Component ? parent.env : parent; const env = parent instanceof Component ? parent.env : parent;
const ownProps = Object.assign({}, props || {}); const ownProps = Object.assign({}, props || {});
@@ -269,5 +271,13 @@ export function connect(mapStateToProps, options: any = {}) {
return super._updateProps(mergedProps, forceUpdate, patchQueue); 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: * We have here a small collection of utility functions:
* *
* - escape
* - memoize
* - debounce
* - patch
* - unpatch
* - loadTemplates
* - loadJS
* - whenReady * - 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 { export function escape(str: string | number | undefined): string {
if (str === undefined) { if (str === undefined) {
return ""; return "";
@@ -29,26 +66,6 @@ export function escape(str: string | number | undefined): string {
.replace(/`/g, "&#x60;"); .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 * 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 * 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;
}
+29 -17
View File
@@ -15,12 +15,15 @@
* - patch function (to apply a vnode to an actual DOM node) * - patch function (to apply a vnode to an actual DOM node)
*/ */
// because those in TypeScript are too restrictive: https://github.com/Microsoft/TSJS-lib-generator/pull/237 // because those in TypeScript are too restrictive: https://github.com/Microsoft/TSJS-lib-generator/pull/237
declare global { declare global {
interface Element { interface Element {
setAttribute(name: string, value: string | number | boolean): void; setAttribute(name: string, value: string | number | boolean): void;
setAttributeNS(namespaceURI: string, qualifiedName: string, value: string | number | boolean): void; setAttributeNS(
namespaceURI: string,
qualifiedName: string,
value: string | number | boolean
): void;
} }
} }
@@ -162,6 +165,7 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node { function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
let i: any, let i: any,
iLen: number,
data = vnode.data; data = vnode.data;
if (data !== undefined) { if (data !== undefined) {
if (isDef((i = data.hook)) && isDef((i = i.init))) { if (isDef((i = data.hook)) && isDef((i = i.init))) {
@@ -193,9 +197,10 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
if (hash < dot) elm.setAttribute("id", sel.slice(hash + 1, dot)); if (hash < dot) elm.setAttribute("id", sel.slice(hash + 1, dot));
if (dotIdx > 0) if (dotIdx > 0)
elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " ")); elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " "));
for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode); for (i = 0, iLen = cbs.create.length; i < iLen; ++i)
cbs.create[i](emptyNode, vnode);
if (array(children)) { if (array(children)) {
for (i = 0; i < children.length; ++i) { for (i = 0, iLen = children.length; i < iLen; ++i) {
const ch = children[i]; const ch = children[i];
if (ch != null) { if (ch != null) {
api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue)); api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue));
@@ -233,13 +238,16 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
function invokeDestroyHook(vnode: VNode) { function invokeDestroyHook(vnode: VNode) {
let i: any, let i: any,
iLen: number,
j: number, j: number,
jLen: number,
data = vnode.data; data = vnode.data;
if (data !== undefined) { if (data !== undefined) {
if (isDef((i = data.hook)) && isDef((i = i.destroy))) i(vnode); if (isDef((i = data.hook)) && isDef((i = i.destroy))) i(vnode);
for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode); for (i = 0, iLen = cbs.destroy.length; i < iLen; ++i)
cbs.destroy[i](vnode);
if (vnode.children !== undefined) { if (vnode.children !== undefined) {
for (j = 0; j < vnode.children.length; ++j) { for (j = 0, jLen = vnode.children.length; j < jLen; ++j) {
i = vnode.children[j]; i = vnode.children[j];
if (i != null && typeof i !== "string") { if (i != null && typeof i !== "string") {
invokeDestroyHook(i); invokeDestroyHook(i);
@@ -257,6 +265,7 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
): void { ): void {
for (; startIdx <= endIdx; ++startIdx) { for (; startIdx <= endIdx; ++startIdx) {
let i: any, let i: any,
iLen: number,
listeners: number, listeners: number,
rm: () => void, rm: () => void,
ch = vnodes[startIdx]; ch = vnodes[startIdx];
@@ -265,7 +274,8 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
invokeDestroyHook(ch); invokeDestroyHook(ch);
listeners = cbs.remove.length + 1; listeners = cbs.remove.length + 1;
rm = createRmCb(ch.elm as Node, listeners); rm = createRmCb(ch.elm as Node, listeners);
for (i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm); for (i = 0, iLen = cbs.remove.length; i < iLen; ++i)
cbs.remove[i](ch, rm);
if ( if (
isDef((i = ch.data)) && isDef((i = ch.data)) &&
isDef((i = i.hook)) && isDef((i = i.hook)) &&
@@ -395,7 +405,7 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
vnode: VNode, vnode: VNode,
insertedVnodeQueue: VNodeQueue insertedVnodeQueue: VNodeQueue
) { ) {
let i: any, hook: any; let i: any, iLen: number, hook: any;
if ( if (
isDef((i = vnode.data)) && isDef((i = vnode.data)) &&
isDef((hook = i.hook)) && isDef((hook = i.hook)) &&
@@ -408,7 +418,8 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
let ch = vnode.children; let ch = vnode.children;
if (oldVnode === vnode) return; if (oldVnode === vnode) return;
if (vnode.data !== undefined) { if (vnode.data !== undefined) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode); for (i = 0, iLen = cbs.update.length; i < iLen; ++i)
cbs.update[i](oldVnode, vnode);
i = vnode.data.hook; i = vnode.data.hook;
if (isDef(i) && isDef((i = i.update))) i(oldVnode, vnode); if (isDef(i) && isDef((i = i.update))) i(oldVnode, vnode);
} }
@@ -458,9 +469,9 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
} }
return function patch(oldVnode: VNode | Element, vnode: VNode): VNode { return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
let i: number, elm: Node, parent: Node; let i: number, iLen: number, elm: Node, parent: Node;
const insertedVnodeQueue: VNodeQueue = []; const insertedVnodeQueue: VNodeQueue = [];
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i](); for (i = 0, iLen = cbs.pre.length; i < iLen; ++i) cbs.pre[i]();
if (!isVnode(oldVnode)) { if (!isVnode(oldVnode)) {
oldVnode = emptyNodeAt(oldVnode); oldVnode = emptyNodeAt(oldVnode);
@@ -480,12 +491,12 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
} }
} }
for (i = 0; i < insertedVnodeQueue.length; ++i) { for (i = 0, iLen = insertedVnodeQueue.length; i < iLen; ++i) {
(((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)( (((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)(
insertedVnodeQueue[i] insertedVnodeQueue[i]
); );
} }
for (i = 0; i < cbs.post.length; ++i) cbs.post[i](); for (i = 0, iLen = cbs.post.length; i < iLen; ++i) cbs.post[i]();
return vnode; return vnode;
}; };
} }
@@ -647,7 +658,7 @@ function addNS(
): void { ): void {
data.ns = "http://www.w3.org/2000/svg"; data.ns = "http://www.w3.org/2000/svg";
if (sel !== "foreignObject" && children !== undefined) { if (sel !== "foreignObject" && children !== undefined) {
for (let i = 0; i < children.length; ++i) { for (let i = 0, iLen = children.length; i < iLen; ++i) {
let childData = children[i].data; let childData = children[i].data;
if (childData !== undefined) { if (childData !== undefined) {
addNS( addNS(
@@ -668,7 +679,8 @@ export function h(sel: any, b?: any, c?: any): VNode {
var data: VNodeData = {}, var data: VNodeData = {},
children: any, children: any,
text: any, text: any,
i: number; i: number,
iLen: number;
if (c !== undefined) { if (c !== undefined) {
data = b; data = b;
if (array(c)) { if (array(c)) {
@@ -690,7 +702,7 @@ export function h(sel: any, b?: any, c?: any): VNode {
} }
} }
if (children !== undefined) { if (children !== undefined) {
for (i = 0; i < children.length; ++i) { for (i = 0, iLen = children.length; i < iLen; ++i) {
if (primitive(children[i])) if (primitive(children[i]))
children[i] = vnode( children[i] = vnode(
undefined, undefined,
@@ -788,7 +800,7 @@ function invokeHandler(handler: any, vnode?: VNode, event?: Event): void {
} }
} else { } else {
// call multiple handlers // call multiple handlers
for (var i = 0; i < handler.length; i++) { for (let i = 0, iLen = handler.length; i < iLen; i++) {
invokeHandler(handler[i], vnode, event); invokeHandler(handler[i], vnode, event);
} }
} }
+127
View File
@@ -0,0 +1,127 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`animations t-transition combined with t-widget 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let QWeb = this.constructor;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false;
}
}
if (!w4) {
let widgetKey4 = \`Child\`;
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4._prepare();
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn.elm, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
w4.destroy();
};
utils.transitionRemove(vn.elm, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
return vn1;
}"
`;
exports[`animations t-transition combined with t-widget and t-if 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let QWeb = this.constructor;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['state'].display) {
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false;
}
}
if (!w4) {
let widgetKey4 = \`Child\`;
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4._prepare();
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn.elm, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
w4.destroy();
};
utils.transitionRemove(vn.elm, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
}
return vn1;
}"
`;
exports[`animations t-transition with no delay/duration 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1);
p1.hook = {
insert: vn => {
this.utils.transitionInsert(vn.elm, 'jupiler');
},
remove: (vn, rm) => {
this.utils.transitionRemove(vn.elm, 'jupiler', rm);
},
};
c1.push({text: \`blue\`});
return vn1;
}"
`;
exports[`animations t-transition, on a simple node (insert) 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1);
p1.hook = {
insert: vn => {
this.utils.transitionInsert(vn.elm, 'chimay');
},
remove: (vn, rm) => {
this.utils.transitionRemove(vn.elm, 'chimay', rm);
},
};
c1.push({text: \`blue\`});
return vn1;
}"
`;
+479 -148
View File
@@ -3,44 +3,38 @@
exports[`class and style attributes with t-widget dynamic t-att-style is properly added and updated on widget root el 1`] = ` exports[`class and style attributes with t-widget dynamic t-att-style is properly added and updated on widget root el 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
let utils = this.utils;
let QWeb = this.constructor;
let owner = context; let owner = context;
var h = this.utils.h; var h = this.utils.h;
var c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
//WIDGET //WIDGET
let _2_index = c1.length; let _2_index = c1.length;
c1.push(null); c1.push(null);
let def3; let def3;
const _5 = context['state'].style;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false; let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {}; let props4 = {};
let isNew4 = !w4; if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (w4 && w4.__owl__.renderPromise) { if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
if (w4.__owl__.isStarted) { def3 = w4.__owl__.renderPromise;
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
} else { } else {
isNew4 = true w4.destroy();
if (props4 === w4.__owl__.renderProps) { w4 = false;
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false
}
} }
} }
if (!def3) { if (!w4) {
if (w4) { let widgetKey4 = \`child\`;
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue); let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
} else { if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
w4 = new context.widgets['child'](owner, props4); w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id; context.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4._prepare(); 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() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
}
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;});
} else { } 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 = 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); extra.promises.push(def3);
return vn1; return vn1;
@@ -50,48 +44,42 @@ exports[`class and style attributes with t-widget dynamic t-att-style is properl
exports[`class and style attributes with t-widget t-att-class is properly added/removed on widget root el 1`] = ` exports[`class and style attributes with t-widget t-att-class is properly added/removed on widget root el 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
let utils = this.utils;
let QWeb = this.constructor;
let owner = context; let owner = context;
var h = this.utils.h; var h = this.utils.h;
var c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
//WIDGET //WIDGET
let _2_index = c1.length; let _2_index = c1.length;
c1.push(null); c1.push(null);
let def3; 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 w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {}; let props4 = {};
let isNew4 = !w4; if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (w4 && w4.__owl__.renderPromise) { if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
if (w4.__owl__.isStarted) { def3 = w4.__owl__.renderPromise;
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
} else { } else {
isNew4 = true w4.destroy();
if (props4 === w4.__owl__.renderProps) { w4 = false;
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false
}
} }
} }
if (!def3) { if (!w4) {
if (w4) { let widgetKey4 = \`child\`;
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue); let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
} else { if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
w4 = new context.widgets['child'](owner, props4); w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id; context.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4._prepare(); def3 = w4._prepare();
}
}
const _5 = {a: context['state'].a,b: context['state'].b};
if (isNew4) {
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){for (let k in _5) { def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){for (let k in _5) {
if (_5[k]) { if (_5[k]) {
vn.elm.classList.add(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() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else { } 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 = 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); extra.promises.push(def3);
return vn1; return vn1;
@@ -101,19 +89,22 @@ exports[`class and style attributes with t-widget t-att-class is properly added/
exports[`composition sub widgets with some state rendered in a loop 1`] = ` exports[`composition sub widgets with some state rendered in a loop 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
let utils = this.utils;
let QWeb = this.constructor;
let owner = context; let owner = context;
context = Object.create(context); context = Object.create(context);
var h = this.utils.h; var h = this.utils.h;
var c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = context['state'].numbers; var _2 = context['state'].numbers;
if (!_2) { throw new Error('QWeb error: Invalid loop expression')} if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
if (typeof _2 === 'number') { _2 = Array.from(Array(_2).keys())} if (typeof _2 === 'number') { _2 = Array.from(Array(_2).keys())}
var _3 = _2 instanceof Array ? _2 : Object.keys(_2); var _3 = _2 instanceof Array ? _2 : Object.keys(_2);
var _length3 = _3.length;
var _4 = _2 instanceof Array ? _2 : Object.values(_2); var _4 = _2 instanceof Array ? _2 : Object.values(_2);
for (let i = 0; i < _3.length; i++) { for (let i = 0; i < _length3; i++) {
context.number_first = i === 0; context.number_first = i === 0;
context.number_last = i === _3.length - 1; context.number_last = i === _length3 - 1;
context.number_parity = i % 2 === 0 ? 'even' : 'odd'; context.number_parity = i % 2 === 0 ? 'even' : 'odd';
context.number_index = i; context.number_index = i;
context.number = _3[i]; context.number = _3[i];
@@ -125,33 +116,25 @@ exports[`composition sub widgets with some state rendered in a loop 1`] = `
let def6; let def6;
let w7 = key8 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[key8]] : false; let w7 = key8 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[key8]] : false;
let props7 = {}; let props7 = {};
let isNew7 = !w7; if (w7 && w7.__owl__.renderPromise && !w7.__owl__.vnode) {
if (w7 && w7.__owl__.renderPromise) { if (utils.shallowEqual(props7, w7.__owl__.renderProps)) {
if (w7.__owl__.isStarted) { def6 = w7.__owl__.renderPromise;
def6 = w7._updateProps(props7, extra.forceUpdate, extra.patchQueue);
} else { } else {
isNew7 = true w7.destroy();
if (props7 === w7.__owl__.renderProps) { w7 = false;
def6 = w7.__owl__.renderPromise;
} else {
w7.destroy();
w7 = false
}
} }
} }
if (!def6) { if (!w7) {
if (w7) { let widgetKey7 = \`ChildWidget\`;
def6 = w7._updateProps(props7, extra.forceUpdate, extra.patchQueue); let W7 = context.widgets && context.widgets[widgetKey7] || QWeb.widgets[widgetKey7];
} else { if (!W7) {throw new Error('Cannot find the definition of widget \\"' + widgetKey7 + '\\"')}
w7 = new context.widgets['ChildWidget'](owner, props7); w7 = new W7(owner, props7);
context.__owl__.cmap[key8] = w7.__owl__.id; context.__owl__.cmap[key8] = w7.__owl__.id;
def6 = w7._prepare(); 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() {},destroy(vn) {w7.destroy();}}});c1[_5_index]=pvnode;w7.__owl__.pvnode = pvnode;});
}
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;});
} else { } 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 = 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); extra.promises.push(def6);
} }
@@ -159,59 +142,14 @@ exports[`composition sub widgets with some state rendered in a loop 1`] = `
}" }"
`; `;
exports[`random stuff/miscellaneous snapshotting compiled code 1`] = ` exports[`composition t-widget with dynamic value 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
let utils = this.utils;
let QWeb = this.constructor;
let owner = context; let owner = context;
var h = this.utils.h; 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';
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = key5 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[key5]] : false;
let props4 = {flag: context['state'].flag};
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 (!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;});
} 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;});
}
extra.promises.push(def3);
return vn1;
}"
`;
exports[`random stuff/miscellaneous t-props should not be undefined (snapshotting) 1`] = `
"function anonymous(context,extra
) {
let owner = context;
var h = this.utils.h;
var c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
//WIDGET //WIDGET
let _2_index = c1.length; let _2_index = c1.length;
@@ -219,33 +157,426 @@ exports[`random stuff/miscellaneous t-props should not be undefined (snapshottin
let def3; let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false; let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {}; let props4 = {};
let isNew4 = !w4; if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (w4 && w4.__owl__.renderPromise) { if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
if (w4.__owl__.isStarted) { def3 = w4.__owl__.renderPromise;
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
} else { } else {
isNew4 = true w4.destroy();
if (props4 === w4.__owl__.renderProps) { w4 = false;
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false
}
} }
} }
if (!def3) { if (!w4) {
if (w4) { let widgetKey4 = (context['state'].widget);
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue); let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
} else { if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
w4 = new context.widgets['child'](owner, props4); w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id; context.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4._prepare(); def3 = w4._prepare();
} def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
}
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;});
} else { } 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 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
return vn1;
}"
`;
exports[`composition t-widget with dynamic value 2 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let QWeb = this.constructor;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false;
}
}
if (!w4) {
let widgetKey4 = \`Widget\${context['state'].widget}\`;
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4._prepare();
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
return vn1;
}"
`;
exports[`other directives with t-widget t-on with handler bound to argument 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let QWeb = this.constructor;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false;
}
}
if (!w4) {
let widgetKey4 = \`child\`;
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4._prepare();
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', owner['onEv'].bind(owner, 3));}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
return vn1;
}"
`;
exports[`other directives with t-widget t-on with handler bound to empty object (with non empty inner string) 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let QWeb = this.constructor;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false;
}
}
if (!w4) {
let widgetKey4 = \`child\`;
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4._prepare();
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', owner['onEv'].bind(owner, {}));}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
return vn1;
}"
`;
exports[`other directives with t-widget t-on with handler bound to empty object 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let QWeb = this.constructor;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false;
}
}
if (!w4) {
let widgetKey4 = \`child\`;
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4._prepare();
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', owner['onEv'].bind(owner, {}));}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
return vn1;
}"
`;
exports[`other directives with t-widget t-on with handler bound to object 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let QWeb = this.constructor;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false;
}
}
if (!w4) {
let widgetKey4 = \`child\`;
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4._prepare();
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', owner['onEv'].bind(owner, {val: 3}));}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
return vn1;
}"
`;
exports[`other directives with t-widget t-on with prevent and self modifiers (order matters) 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let QWeb = this.constructor;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false;
}
}
if (!w4) {
let widgetKey4 = \`child\`;
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4._prepare();
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {e.preventDefault();if (e.target !== vn.elm) {return}owner['onEv'].call(owner, e);});}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
return vn1;
}"
`;
exports[`other directives with t-widget t-on with self and prevent modifiers (order matters) 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let QWeb = this.constructor;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false;
}
}
if (!w4) {
let widgetKey4 = \`child\`;
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4._prepare();
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (e.target !== vn.elm) {return}e.preventDefault();owner['onEv'].call(owner, e);});}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
return vn1;
}"
`;
exports[`other directives with t-widget t-on with self modifier 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let QWeb = this.constructor;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false;
}
}
if (!w4) {
let widgetKey4 = \`child\`;
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4._prepare();
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev-1', owner['onEv1'].bind(owner));vn.elm.addEventListener('ev-2', function (e) {if (e.target !== vn.elm) {return}owner['onEv2'].call(owner, e);});}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
return vn1;
}"
`;
exports[`other directives with t-widget t-on with stop and/or prevent modifiers 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let QWeb = this.constructor;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false;
}
}
if (!w4) {
let widgetKey4 = \`child\`;
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4._prepare();
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev-1', function (e) {e.stopPropagation();owner['onEv1'].call(owner, e);});vn.elm.addEventListener('ev-2', function (e) {e.preventDefault();owner['onEv2'].call(owner, e);});vn.elm.addEventListener('ev-3', function (e) {e.stopPropagation();e.preventDefault();owner['onEv3'].call(owner, e);});}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
return vn1;
}"
`;
exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let QWeb = this.constructor;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let key5 = 'somestring';
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = key5 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[key5]] : false;
let props4 = {flag:context['state'].flag};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false;
}
}
if (!w4) {
let widgetKey4 = \`child\`;
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[key5] = w4.__owl__.id;
def3 = w4._prepare();
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: key5, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
} }
extra.promises.push(def3); extra.promises.push(def3);
return vn1; return vn1;
File diff suppressed because it is too large Load Diff
+33
View File
@@ -0,0 +1,33 @@
function anonymous(context, extra) {
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['state'].display) {
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
w4.destroy();
w4 = false
}
if (!w4) {
let W4 = context.widgets['Child'];
if (!W4) {throw new Error(`Cannot find the definition of widget "Child"`)}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4._prepare();
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert: (vn) => {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;this.utils.transitionInsert(vn.elm, 'chimay');},remove: () => {},destroy: (vn) => {let finalize = () => {
w4.destroy();
};
this.utils.transitionRemove(vn.elm, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
}
}
+290
View File
@@ -0,0 +1,290 @@
import { Component, Env } from "../src/component";
import { QWeb } from "../src/qweb_core";
import {
makeDeferred,
makeTestFixture,
makeTestEnv,
nextTick,
patchNextFrame,
renderToDOM,
unpatchNextFrame
} from "./helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
// We create before each test:
// - fixture: a div, appended to the DOM, intended to be the target of dom
// manipulations. Note that it is removed after each test.
// - qweb: a new QWeb instance
// - env: a WEnv, necessary to create new widgets
// - cssEl: a stylesheet injected into the dom
let fixture: HTMLElement;
let qweb: QWeb;
let env: Env;
let cssEl: HTMLElement;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
qweb = new QWeb();
});
afterEach(() => {
fixture.remove();
});
class Widget extends Component<any, any, any> {}
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("animations", () => {
beforeEach(() => {
cssEl = document.createElement("style");
let css = `
.chimay-enter-active, .chimay-leave-active {
transition-property: opacity;
transition-duration: 0.1s;
}
.chimay-enter, .chimay-leave-to {
opacity: 0;
}
`;
cssEl.textContent = css.trim();
document.head.appendChild(cssEl);
});
afterEach(() => {
document.head.removeChild(cssEl);
unpatchNextFrame();
});
test("t-transition, on a simple node (insert)", async () => {
expect.assertions(5);
qweb.addTemplate("test", `<span t-transition="chimay">blue</span>`);
let def = makeDeferred();
patchNextFrame(cb => {
expect(node.className).toBe("chimay-enter chimay-enter-active");
cb();
expect(node.className).toBe("chimay-enter-active chimay-enter-to");
def.resolve();
});
let node: HTMLElement = <HTMLElement>renderToDOM(qweb, "test");
expect(node.className).toBe("chimay-enter chimay-enter-active");
await def; // wait for the mocked repaint to be done
node.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(node.className).toBe("");
});
test("t-transition with no delay/duration", async () => {
expect.assertions(4);
qweb.addTemplate("test", `<span t-transition="jupiler">blue</span>`);
let def = makeDeferred();
patchNextFrame(cb => {
expect(node.className).toBe("jupiler-enter jupiler-enter-active");
cb();
expect(node.className).toBe("");
def.resolve();
});
let node: HTMLElement = <HTMLElement>renderToDOM(qweb, "test");
expect(node.className).toBe("jupiler-enter jupiler-enter-active");
await def;
});
test("t-transition on a conditional node", async () => {
expect.assertions(7);
env.qweb.addTemplate(
"TestWidget",
`<div><span t-if="!state.hide" t-transition="chimay">blue</span></div>`
);
class TestWidget extends Widget {
state = { hide: false };
}
const widget = new TestWidget(env);
// insert widget into the DOM
let def = makeDeferred();
var spanNode;
patchNextFrame(cb => {
expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
cb();
expect(spanNode.className).toBe("chimay-enter-active chimay-enter-to");
def.resolve();
});
await widget.mount(fixture);
spanNode = widget.el!.children[0];
expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
await def; // wait for the mocked repaint to be done
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(spanNode.className).toBe("");
// remove span from the DOM
def = makeDeferred();
widget.state.hide = true;
patchNextFrame(cb => {
expect(spanNode.className).toBe("chimay-leave chimay-leave-active");
cb();
expect(spanNode.className).toBe("chimay-leave-active chimay-leave-to");
def.resolve();
});
await def; // wait for the mocked repaint to be done
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(spanNode.className).toBe("");
});
test("t-transition combined with t-ref", async () => {
expect.assertions(5);
env.qweb.addTemplate(
"TestWidget",
`<div><span t-ref="span" t-transition="chimay">blue</span></div>`
);
class TestWidget extends Widget {
state = { hide: false };
}
const widget = new TestWidget(env);
// insert widget into the DOM
let def = makeDeferred();
var spanNode;
patchNextFrame(cb => {
expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
cb();
expect(spanNode.className).toBe("chimay-enter-active chimay-enter-to");
def.resolve();
});
await widget.mount(fixture);
spanNode = widget.el!.children[0];
expect(widget.refs.span).toBe(spanNode);
expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
await def; // wait for the mocked repaint to be done
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(spanNode.className).toBe("");
});
test("t-transition combined with t-widget", async () => {
expect.assertions(5);
env.qweb.addTemplate(
"Parent",
`<div><t t-widget="Child" t-transition="chimay"/></div>`
);
env.qweb.addTemplate("Child", `<span>blue</span>`);
class Parent extends Widget {
widgets = { Child: Child };
}
class Child extends Widget {}
const widget = new Parent(env);
let def = makeDeferred();
var spanNode;
patchNextFrame(cb => {
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-enter chimay-enter-active">blue</span></div>'
);
cb();
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-enter-active chimay-enter-to">blue</span></div>'
);
def.resolve();
});
await widget.mount(fixture);
spanNode = widget.el!.children[0];
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-enter chimay-enter-active">blue</span></div>'
);
await def; // wait for the mocked repaint to be done
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(fixture.innerHTML).toBe('<div><span class="">blue</span></div>');
});
test("t-transition combined with t-widget and t-if", async () => {
expect.assertions(8);
env.qweb.addTemplate(
"Parent",
`<div><t t-if="state.display" t-widget="Child" t-transition="chimay"/></div>`
);
env.qweb.addTemplate("Child", `<span>blue</span>`);
class Parent extends Widget {
widgets = { Child: Child };
state = { display: true };
}
class Child extends Widget {}
const widget = new Parent(env);
let def = makeDeferred();
var spanNode;
patchNextFrame(cb => {
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-enter chimay-enter-active">blue</span></div>'
);
cb();
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-enter-active chimay-enter-to">blue</span></div>'
);
def.resolve();
});
await widget.mount(fixture);
spanNode = widget.el!.children[0];
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-enter chimay-enter-active">blue</span></div>'
);
await def; // wait for the mocked repaint to be done
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(fixture.innerHTML).toBe('<div><span class="">blue</span></div>');
// remove span from the DOM
def = makeDeferred();
widget.state.display = false;
patchNextFrame(cb => {
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-leave chimay-leave-active">blue</span></div>'
);
cb();
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-leave-active chimay-leave-to">blue</span></div>'
);
def.resolve();
});
await def; // wait for the mocked repaint to be done
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(fixture.innerHTML).toBe("<div></div>");
});
test("t-transition combined with t-mounted", async () => {
env.qweb.addTemplate(
"TestWidget",
`<div><span t-if="state.flag" t-mounted="f" t-transition="chimay">blue</span></div>`
);
class TestWidget extends Widget {
state = { flag: false };
f() {}
}
const widget = new TestWidget(env);
widget.f = jest.fn();
await widget.mount(fixture);
patchNextFrame(cb => cb());
expect(widget.f).toHaveBeenCalledTimes(0);
widget.state.flag = true;
await nextTick();
expect(widget.f).toHaveBeenCalledTimes(1);
unpatchNextFrame();
});
});
+829 -244
View File
File diff suppressed because it is too large Load Diff
+51 -2
View File
@@ -1,5 +1,6 @@
import { Env } from "../src/component"; import { Env } from "../src/component";
import { QWeb } from "../src/qweb_core"; import { EvalContext, QWeb, UTILS } from "../src/qweb_core";
import { patch } from "../src/vdom";
import "../src/qweb_directives"; import "../src/qweb_directives";
import "../src/qweb_extensions"; import "../src/qweb_extensions";
@@ -37,8 +38,56 @@ export function makeDeferred(): Deferred {
return <Deferred>def; return <Deferred>def;
} }
export function makeTestWEnv(): Env { export function makeTestEnv(): Env {
return { return {
qweb: new QWeb() qweb: new QWeb()
}; };
} }
export function trim(str: string): string {
return str.replace(/\s/g, "");
}
export function renderToDOM(
qweb: QWeb,
template: string,
context: EvalContext = {},
extra?: any
): HTMLElement | Text {
const vnode = qweb.render(template, context, extra);
// we snapshot here the compiled code. This is useful to prevent unwanted code
// change.
expect(qweb.templates[template].fn.toString()).toMatchSnapshot();
if (vnode.sel === undefined) {
return document.createTextNode(vnode.text!);
}
const node = document.createElement(vnode.sel!);
const result = patch(node, vnode);
return result.elm as HTMLElement;
}
export function renderToString(
qweb: QWeb,
t: string,
context: EvalContext = {}
): string {
const node = renderToDOM(qweb, t, context);
return node instanceof Text ? node.textContent! : node.outerHTML;
}
// hereafter, we define two helpers to patch/unpatch the nextFrame utils. This
// is useful for animations tests, as we hook before repaints to trigger
// animations (thanks to requestAnimationFrame). Patching nextFrame allows to
// simulate calls to this hook. One must not forget to unpatch afterwards.
let nextFrame = UTILS.nextFrame;
export function patchNextFrame(f: Function) {
UTILS.nextFrame = (cb: () => void) => {
setTimeout(() => f(cb));
};
}
export function unpatchNextFrame() {
UTILS.nextFrame = nextFrame;
}
+26 -4
View File
@@ -126,12 +126,12 @@ describe("observer", () => {
arr.push({ kriek: 5 }); arr.push({ kriek: 5 });
expect(observer.rev).toBe(2); expect(observer.rev).toBe(2);
expect(arr.__owl__.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; arr[0].kriek = 6;
expect(observer.rev).toBe(3); expect(observer.rev).toBe(3);
expect(arr.__owl__.rev).toBe(2); 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", () => { test("properly observe arrays in object", () => {
@@ -180,12 +180,14 @@ describe("observer", () => {
obj.a = { b: 2 }; obj.a = { b: 2 };
expect(observer.rev).toBe(2); expect(observer.rev).toBe(2);
expect(obj.__owl__.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; obj.a.b = 3;
expect(observer.rev).toBe(3); expect(observer.rev).toBe(3);
expect(obj.__owl__.rev).toBe(2); 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", () => { test("deep observe misc changes", () => {
@@ -228,6 +230,26 @@ describe("observer", () => {
expect(obj2.__owl__.rev).toBe(3); expect(obj2.__owl__.rev).toBe(3);
}); });
test("can set a property more than once", () => {
const observer = new Observer();
const obj: any = {};
observer.observe(obj);
expect(obj.__owl__.rev).toBe(1);
expect(observer.rev).toBe(1);
expect(obj.__owl__.deepRev).toBe(1);
observer.set(obj, "aku", "always finds annoying problems");
expect(observer.rev).toBe(2);
expect(obj.__owl__.rev).toBe(2);
expect(obj.__owl__.deepRev).toBe(2);
observer.set(obj, "aku", "always finds good problems");
expect(observer.rev).toBe(3);
expect(obj.__owl__.rev).toBe(3);
expect(obj.__owl__.deepRev).toBe(3);
});
test("properly handle swapping elements", () => { test("properly handle swapping elements", () => {
const observer = new Observer(); const observer = new Observer();
const obj: any = { a: { arr: [] }, b: 1 }; const obj: any = { a: { arr: [] }, b: 1 };
+263
View File
@@ -0,0 +1,263 @@
import { Component, Env } from "../src/component";
import { makeTestFixture, makeTestEnv } from "./helpers";
import { QWeb } from "../src";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
let fixture: HTMLElement;
let env: Env;
let dev: boolean = false;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
dev = QWeb.dev;
QWeb.dev = true;
});
afterEach(() => {
fixture.remove();
QWeb.dev = dev;
});
class Widget extends Component<any, any, any> {}
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("props validation", () => {
test("validation is only done in dev mode", async () => {
class TestWidget extends Widget {
static props = ["message"];
}
QWeb.dev = true;
expect(() => {
new TestWidget(env);
}).toThrow();
QWeb.dev = false;
expect(() => {
new TestWidget(env);
}).not.toThrow();
});
test("props validation is also done on update props", async () => {
expect.assertions(1);
class TestWidget extends Widget {
static props = ["message"];
}
const w = new TestWidget(env, { message: "bottle" });
try {
await w._updateProps({});
} catch (e) {
expect(e.message).toBe("Missing props 'message' (widget 'TestWidget')");
}
});
test("props: list of strings", async () => {
class TestWidget extends Widget {
static props = ["message"];
}
expect(() => {
new TestWidget(env);
}).toThrow("Missing props 'message' (widget 'TestWidget')");
});
test("validate simple types", async () => {
const Tests = [
{ type: Number, ok: 1, ko: "1" },
{ type: Boolean, ok: true, ko: "1" },
{ type: String, ok: "1", ko: 1 },
{ type: Object, ok: {}, ko: "1" },
{ type: Date, ok: new Date(), ko: "1" },
{ type: Function, ok: () => {}, ko: "1" }
];
for (let test of Tests) {
let TestWidget = class extends Widget {
static props = { p: test.type };
};
expect(() => {
new TestWidget(env);
}).toThrow("Missing props 'p'");
expect(() => {
new TestWidget(env, { p: test.ok });
}).not.toThrow();
expect(() => {
new TestWidget(env, { p: test.ko });
}).toThrow("Props 'p' of invalid type in widget");
}
});
test("validate simple types, alternate form", async () => {
const Tests = [
{ type: Number, ok: 1, ko: "1" },
{ type: Boolean, ok: true, ko: "1" },
{ type: String, ok: "1", ko: 1 },
{ type: Object, ok: {}, ko: "1" },
{ type: Date, ok: new Date(), ko: "1" },
{ type: Function, ok: () => {}, ko: "1" }
];
for (let test of Tests) {
let TestWidget = class extends Widget {
static props = { p: { type: test.type } };
};
expect(() => {
new TestWidget(env);
}).toThrow("Missing props 'p'");
expect(() => {
new TestWidget(env, { p: test.ok });
}).not.toThrow();
expect(() => {
new TestWidget(env, { p: test.ko });
}).toThrow("Props 'p' of invalid type in widget");
}
});
test("can validate a prop with multiple types", async () => {
let TestWidget = class extends Widget {
static props = { p: [String, Boolean] };
};
expect(() => {
new TestWidget(env, { p: "string" });
new TestWidget(env, { p: true });
}).not.toThrow();
expect(() => {
new TestWidget(env, { p: 1 });
}).toThrow("Props 'p' of invalid type in widget");
});
test("can validate an optional props", async () => {
let TestWidget = class extends Widget {
static props = { p: { type: String, optional: true } };
};
expect(() => {
new TestWidget(env, { p: "hey" });
new TestWidget(env, { });
}).not.toThrow();
expect(() => {
new TestWidget(env, { p: 1 });
}).toThrow();
});
test("can validate an array with given primitive type", async () => {
let TestWidget = class extends Widget {
static props = { p: { type: Array, element: String } };
};
expect(() => {
new TestWidget(env, { p: [] });
new TestWidget(env, { p: ["string"] });
}).not.toThrow();
expect(() => {
new TestWidget(env, { p: [1] });
}).toThrow();
expect(() => {
new TestWidget(env, { p: ["string", 1] });
}).toThrow();
});
test("can validate an array with multiple sub element types", async () => {
let TestWidget = class extends Widget {
static props = { p: { type: Array, element: [String, Boolean] } };
};
expect(() => {
new TestWidget(env, { p: [] });
new TestWidget(env, { p: ["string"] });
new TestWidget(env, { p: [false, true, "string"] });
}).not.toThrow();
expect(() => {
new TestWidget(env, { p: [true, 1] });
}).toThrow();
});
test("can validate an object with simple shape", async () => {
let TestWidget = class extends Widget {
static props = {
p: { type: Object, shape: { id: Number, url: String } }
};
};
expect(() => {
new TestWidget(env, { p: { id: 1, url: "url" } });
new TestWidget(env, { p: { id: 1, url: "url", extra: true } });
}).not.toThrow();
expect(() => {
new TestWidget(env, { p: { id: "1", url: "url" } });
}).toThrow();
expect(() => {
new TestWidget(env, { p: { id: 1 } });
}).toThrow();
});
test("can validate recursively complicated prop def", async () => {
let TestWidget = class extends Widget {
static props = {
p: {
type: Object,
shape: {
id: Number,
url: [Boolean, {type: Array, element: Number}],
}
}
};
};
expect(() => {
new TestWidget(env, { p: { id: 1, url: true } });
new TestWidget(env, { p: { id: 1, url: [12]} });
}).not.toThrow();
expect(() => {
new TestWidget(env, { p: { id: 1, url: [12, true]} });
}).toThrow();
});
});
describe("default props", () => {
test("can set default values", async () => {
class TestWidget extends Widget {
static defaultProps = {p: 4}
}
const w = new TestWidget(env, {});
expect(w.props.p).toBe(4);
});
test("default values are also set whenever component is updated", async () => {
class TestWidget extends Widget {
static defaultProps = {p: 4}
}
const w = new TestWidget(env, {p: 1});
await w._updateProps({});
expect(w.props.p).toBe(4);
});
});
+148 -62
View File
@@ -1,6 +1,5 @@
import { EvalContext, QWeb } from "../src/qweb_core"; import { QWeb } from "../src/qweb_core";
import { patch } from "../src/vdom"; import { normalize, renderToDOM, renderToString, trim } from "./helpers";
import { normalize } from "./helpers";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Setup and helpers // Setup and helpers
@@ -15,39 +14,6 @@ beforeEach(() => {
qweb = new QWeb(); qweb = new QWeb();
}); });
function trim(str: string): string {
return str.replace(/\s/g, "");
}
function renderToDOM(
qweb: QWeb,
template: string,
context: EvalContext = {},
extra?: any
): HTMLElement | Text {
const vnode = qweb.render(template, context, extra);
// we snapshot here the compiled code. This is useful to prevent unwanted code
// change.
expect(qweb.templates[template].fn.toString()).toMatchSnapshot();
if (vnode.sel === undefined) {
return document.createTextNode(vnode.text!);
}
const node = document.createElement(vnode.sel!);
const result = patch(node, vnode);
return result.elm as HTMLElement;
}
function renderToString(
qweb: QWeb,
t: string,
context: EvalContext = {}
): string {
const node = renderToDOM(qweb, t, context);
return node instanceof Text ? node.textContent! : node.outerHTML;
}
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Tests // Tests
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -100,7 +66,7 @@ describe("error handling", () => {
test("loadTemplates throw if parser error", () => { test("loadTemplates throw if parser error", () => {
expect(() => { expect(() => {
qweb.loadTemplates("<templates><abc>></templates>"); qweb.addTemplates("<templates><abc>></templates>");
}).toThrow("Invalid XML in template"); }).toThrow("Invalid XML in template");
}); });
@@ -119,6 +85,10 @@ describe("error handling", () => {
}); });
test("error when compiled code is invalid", () => { test("error when compiled code is invalid", () => {
const consoleWarn = console.warn;
const consoleGroupCollapsed = console.groupCollapsed;
console.warn = jest.fn();
console.groupCollapsed = jest.fn();
qweb.addTemplate( qweb.addTemplate(
"templatename", "templatename",
`<div t-att-hey="}/^function invalid{{>'"></div>` `<div t-att-hey="}/^function invalid{{>'"></div>`
@@ -126,6 +96,9 @@ describe("error handling", () => {
expect(() => qweb.render("templatename")).toThrow( expect(() => qweb.render("templatename")).toThrow(
"Invalid generated code while compiling template 'templatename': Unexpected token }" "Invalid generated code while compiling template 'templatename': Unexpected token }"
); );
expect(console.warn).toBeCalledTimes(1);
console.warn = consoleWarn;
console.groupCollapsed = consoleGroupCollapsed;
}); });
test("error when unknown directive", () => { test("error when unknown directive", () => {
@@ -511,6 +484,12 @@ describe("attributes", () => {
expect(result).toBe(`<div foo="42"></div>`); 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", () => { test("format multiple", () => {
qweb.addTemplate( qweb.addTemplate(
"test", "test",
@@ -550,6 +529,12 @@ describe("attributes", () => {
expect(result).toBe(`<div class="hello world"></div>`); expect(result).toBe(`<div class="hello world"></div>`);
}); });
test("class and t-attf-class with ternary operation", () => {
qweb.addTemplate("test", `<div class="hello" t-attf-class="#{value ? 'world' : ''}"/>`);
const result = renderToString(qweb, "test", { value: true });
expect(result).toBe(`<div class="hello world"></div>`);
});
test("t-att-class with object", () => { test("t-att-class with object", () => {
qweb.addTemplate( qweb.addTemplate(
"test", "test",
@@ -748,7 +733,6 @@ describe("foreach", () => {
expect(console.warn).toHaveBeenCalledTimes(1); expect(console.warn).toHaveBeenCalledTimes(1);
console.warn = consoleWarn; console.warn = consoleWarn;
}); });
}); });
describe("misc", () => { describe("misc", () => {
@@ -912,7 +896,7 @@ describe("t-on", () => {
(<HTMLElement>node).click(); (<HTMLElement>node).click();
}); });
test("can bind handlers with empty object (with non empty inner string", () => { test("can bind handlers with empty object (with non empty inner string)", () => {
expect.assertions(2); expect.assertions(2);
qweb.addTemplate( qweb.addTemplate(
"test", "test",
@@ -964,32 +948,147 @@ describe("t-on", () => {
const node = renderToDOM(qweb, "test", owner, { handlers: [] }); const node = renderToDOM(qweb, "test", owner, { handlers: [] });
(<HTMLElement>node).click(); (<HTMLElement>node).click();
}); });
test("t-on with prevent and/or stop modifiers", async () => {
expect.assertions(7);
qweb.addTemplate(
"test",
`<div>
<button t-on-click.prevent="onClickPrevented">Button 1</button>
<button t-on-click.stop="onClickStopped">Button 2</button>
<button t-on-click.prevent.stop="onClickPreventedAndStopped">Button 3</button>
</div>`
);
let owner = {
onClickPrevented(e) {
expect(e.defaultPrevented).toBe(true);
expect(e.cancelBubble).toBe(false);
},
onClickStopped(e) {
expect(e.defaultPrevented).toBe(false);
expect(e.cancelBubble).toBe(true);
},
onClickPreventedAndStopped(e) {
expect(e.defaultPrevented).toBe(true);
expect(e.cancelBubble).toBe(true);
}
};
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
const buttons = (<HTMLElement>node).getElementsByTagName("button");
buttons[0].click();
buttons[1].click();
buttons[2].click();
});
test("t-on with self modifier", async () => {
expect.assertions(2);
qweb.addTemplate(
"test",
`<div>
<button t-on-click="onClick"><span>Button</span></button>
<button t-on-click.self="onClickSelf"><span>Button</span></button>
</div>`
);
let steps: string[] = [];
let owner = {
onClick(e) {
steps.push("onClick");
},
onClickSelf(e) {
steps.push("onClickSelf");
}
};
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
const buttons = (<HTMLElement>node).getElementsByTagName("button");
const spans = (<HTMLElement>node).getElementsByTagName("span");
spans[0].click();
spans[1].click();
buttons[0].click();
buttons[1].click();
expect(steps).toEqual(["onClick", "onClick", "onClickSelf"]);
});
test("t-on with self and prevent modifiers (order matters)", async () => {
expect.assertions(2);
qweb.addTemplate(
"test",
`<div>
<button t-on-click.self.prevent="onClick"><span>Button</span></button>
</div>`
);
let steps: boolean[] = [];
let owner = {
onClick() {}
};
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
(<HTMLElement>node).addEventListener("click", function(e) {
steps.push(e.defaultPrevented);
});
const button = (<HTMLElement>node).getElementsByTagName("button")[0];
const span = (<HTMLElement>node).getElementsByTagName("span")[0];
span.click();
button.click();
expect(steps).toEqual([false, true]);
});
test("t-on with prevent and self modifiers (order matters)", async () => {
expect.assertions(2);
qweb.addTemplate(
"test",
`<div>
<button t-on-click.prevent.self="onClick"><span>Button</span></button>
</div>`
);
let steps: boolean[] = [];
let owner = {
onClick() {}
};
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
(<HTMLElement>node).addEventListener("click", function(e) {
steps.push(e.defaultPrevented);
});
const button = (<HTMLElement>node).getElementsByTagName("button")[0];
const span = (<HTMLElement>node).getElementsByTagName("span")[0];
span.click();
button.click();
expect(steps).toEqual([true, true]);
});
}); });
describe("t-ref", () => { describe("t-ref", () => {
test("can get a ref on a node", () => { test("can get a ref on a node", () => {
qweb.addTemplate("test", `<div><span t-ref="'myspan'"/></div>`); qweb.addTemplate("test", `<div><span t-ref="myspan"/></div>`);
let refs: any = {}; let refs: any = {};
renderToDOM(qweb, "test", { refs }); renderToDOM(qweb, "test", { refs });
expect(refs.myspan.tagName).toBe("SPAN"); expect(refs.myspan.tagName).toBe("SPAN");
}); });
test("can get a dynamic ref on a node", () => { test("can get a dynamic ref on a node", () => {
qweb.addTemplate("test", `<div><span t-ref="'myspan' + 3"/></div>`); qweb.addTemplate("test", `<div><span t-ref="myspan{{id}}"/></div>`);
let refs: any = {}; let refs: any = {};
renderToDOM(qweb, "test", { refs }); renderToDOM(qweb, "test", { refs, id: 3 });
expect(refs.myspan3.tagName).toBe("SPAN"); expect(refs.myspan3.tagName).toBe("SPAN");
}); });
test("refs in a loop", () => { test("refs in a loop", () => {
qweb.addTemplate("test", ` qweb.addTemplate(
"test",
`
<div> <div>
<t t-foreach="items" t-as="item"> <t t-foreach="items" t-as="item">
<div t-ref="item" t-key="item"><t t-esc="item"/></div> <div t-ref="{{item}}" t-key="item"><t t-esc="item"/></div>
</t> </t>
</div>`); </div>`
);
let refs: any = {}; 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"]); expect(Object.keys(refs)).toEqual(["1", "2", "3"]);
}); });
}); });
@@ -1014,15 +1113,15 @@ describe("loading templates", () => {
<ul t-name="main"><t t-call="items"/></ul> <ul t-name="main"><t t-call="items"/></ul>
</templates>`; </templates>`;
qweb.loadTemplates(data); qweb.addTemplates(data);
const result = renderToString(qweb, "main"); const result = renderToString(qweb, "main");
expect(result).toBe("<ul><li>ok</li><li>foo</li></ul>"); expect(result).toBe("<ul><li>ok</li><li>foo</li></ul>");
}); });
test("does not crash if string does not have templates", () => { test("does not crash if string does not have templates", () => {
const data = ""; const data = "";
qweb.loadTemplates(data); qweb.addTemplates(data);
expect(qweb.templates).toEqual({}); expect(Object.keys(qweb.templates)).toEqual(["default"]);
}); });
}); });
@@ -1159,16 +1258,3 @@ describe("debugging", () => {
console.log = consoleLog; console.log = consoleLog;
}); });
}); });
describe("animations", () => {
test("t-transition, on a simple node", async () => {
// this test does not test much, because it is not easy to test timing
// transitions... we should do a little more effort for these tests.
qweb.addTemplate(
"test",
`<div><span t-transition="chimay">blue</span></div>`
);
let dom: HTMLElement = <HTMLElement>renderToDOM(qweb, "test");
expect(dom.innerHTML).toMatchSnapshot();
});
});
+119 -90
View File
@@ -2,7 +2,7 @@ import { Component, Env } from "../src/component";
import { connect, Store } from "../src/store"; import { connect, Store } from "../src/store";
import { import {
makeTestFixture, makeTestFixture,
makeTestWEnv, makeTestEnv,
nextMicroTick, nextMicroTick,
nextTick nextTick
} from "./helpers"; } from "./helpers";
@@ -176,15 +176,15 @@ describe("basic use", () => {
1: { 1: {
id: 1, id: 1,
name: "bertinchamps", name: "bertinchamps",
tasterID: 1, tasterID: 1
}, }
}, },
tasters: { tasters: {
1: { 1: {
id: 1, id: 1,
name: 'aaron', name: "aaron"
} }
}, }
}; };
const getters = { const getters = {
beerTasterName({ state }, beerID) { beerTasterName({ state }, beerID) {
@@ -207,15 +207,15 @@ describe("basic use", () => {
1: { 1: {
id: 1, id: 1,
name: "bertinchamps", name: "bertinchamps",
tasterID: 1, tasterID: 1
}, }
}, },
tasters: { tasters: {
1: { 1: {
id: 1, id: 1,
name: 'aaron', name: "aaron"
} }
}, }
}; };
const getters = { const getters = {
beerTasterName({ state }, beerID) { beerTasterName({ state }, beerID) {
@@ -243,15 +243,15 @@ describe("basic use", () => {
1: { 1: {
id: 1, id: 1,
name: "bertinchamps", name: "bertinchamps",
tasterID: 1, tasterID: 1
}, }
}, },
tasters: { tasters: {
1: { 1: {
id: 1, id: 1,
name: 'aaron', name: "aaron"
} }
}, }
}; };
const getters = { const getters = {
beerTasterName({ state }, beerID) { beerTasterName({ state }, beerID) {
@@ -278,15 +278,15 @@ describe("basic use", () => {
return `${getters.b()}${getters.c(1)}`; return `${getters.b()}${getters.c(1)}`;
}, },
b() { b() {
return 'b'; return "b";
}, },
c({}, i) { c({}, i) {
return `c${i}`; return `c${i}`;
}, }
}; };
const store = new Store({ getters }); 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(() => { beforeEach(() => {
fixture = makeTestFixture(); fixture = makeTestFixture();
env = makeTestWEnv(); env = makeTestEnv();
}); });
afterEach(() => { afterEach(() => {
fixture.remove(); fixture.remove();
}); });
class App extends Component<any, any, any> { test("connecting a component works", async () => {
inlineTemplate = ` env.qweb.addTemplate(
"App",
`
<div> <div>
<t t-foreach="props.todos" t-as="todo" t-key="todo"> <t t-foreach="props.todos" t-as="todo" t-key="todo">
<t t-widget="Todo" t-props="todo"/> <t t-widget="Todo" msg="todo.msg"/>
</t> </t>
</div>`; </div>`
widgets = { Todo }; );
} env.qweb.addTemplate("Todo", `<span><t t-esc="props.msg"/></span>`);
class Todo extends Component<any, any, any> { class App extends Component<any, any, any> {
inlineTemplate = `<span><t t-esc="props.msg"/></span>`; widgets = { Todo };
} }
class Todo extends Component<any, any, any> {}
test("connecting a component works", async () => {
const state = { todos: [] }; const state = { todos: [] };
const mutations = { const mutations = {
addTodo({ state }, msg) { addTodo({ state }, msg) {
@@ -456,14 +457,16 @@ describe("connecting a component to store", () => {
} }
const store = new Store({ state, mutations }); const store = new Store({ state, mutations });
class App extends Component<any, any, any> { env.qweb.addTemplate(
inlineTemplate = ` "App",
`
<div> <div>
<span t-foreach="props.todos" t-as="todo" t-key="todo"> <span t-foreach="props.todos" t-as="todo" t-key="todo">
<t t-esc="todo.title"/> <t t-esc="todo.title"/>
</span> </span>
</div>`; </div>`
} );
class App extends Component<any, any, any> {}
const DeepTodoApp = connect( const DeepTodoApp = connect(
mapStateToProps, mapStateToProps,
@@ -493,8 +496,8 @@ describe("connecting a component to store", () => {
test("connected child components with custom hooks", async () => { test("connected child components with custom hooks", async () => {
let steps: any = []; let steps: any = [];
env.qweb.addTemplate("Child", `<div/>`);
class Child extends Component<any, any, any> { class Child extends Component<any, any, any> {
inlineTemplate = `<div/>`;
mounted() { mounted() {
steps.push("child:mounted"); steps.push("child:mounted");
} }
@@ -505,11 +508,14 @@ describe("connecting a component to store", () => {
const ConnectedChild = connect(s => s)(Child); const ConnectedChild = connect(s => s)(Child);
class Parent extends Component<any, any, any> { env.qweb.addTemplate(
inlineTemplate = ` "Parent",
`
<div> <div>
<t t-if="state.child" t-widget="ConnectedChild"/> <t t-if="state.child" t-widget="ConnectedChild"/>
</div>`; </div>`
);
class Parent extends Component<any, any, any> {
widgets = { ConnectedChild }; widgets = { ConnectedChild };
constructor(env: Env) { constructor(env: Env) {
@@ -540,20 +546,22 @@ describe("connecting a component to store", () => {
}; };
const store = new Store({ state, mutations }); const store = new Store({ state, mutations });
class TodoItem extends Component<any, any, any> { env.qweb.addTemplate("TodoItem", `<span><t t-esc="props.text"/></span>`);
inlineTemplate = `<span><t t-esc="props.text"/></span>`; class TodoItem extends Component<any, any, any> {}
}
const ConnectedTodo = connect((state, props) => { const ConnectedTodo = connect((state, props) => {
const todo = state.todos.find(t => t.id === props.id); const todo = state.todos.find(t => t.id === props.id);
return todo; return todo;
})(TodoItem); })(TodoItem);
class TodoList extends Component<any, any, any> { env.qweb.addTemplate(
inlineTemplate = `<div> "TodoList",
`<div>
<t t-foreach="props.todos" t-as="todo"> <t t-foreach="props.todos" t-as="todo">
<t t-widget="ConnectedTodo" t-props="todo"/> <t t-widget="ConnectedTodo" id="todo.id"/>
</t> </t>
</div>`; </div>`
);
class TodoList extends Component<any, any, any> {
widgets = { ConnectedTodo }; widgets = { ConnectedTodo };
} }
@@ -578,10 +586,7 @@ describe("connecting a component to store", () => {
test("connect receives store getters as third argument", async () => { test("connect receives store getters as third argument", async () => {
const state = { const state = {
importantID: 1, importantID: 1,
todos: [ todos: [{ id: 1, text: "jupiler" }, { id: 2, text: "bertinchamps" }]
{ id: 1, text: "jupiler" },
{ id: 2, text: "bertinchamps" },
],
}; };
const getters = { const getters = {
importantTodoText({ state }) { importantTodoText({ state }) {
@@ -589,30 +594,35 @@ describe("connecting a component to store", () => {
}, },
text({ state }, id) { text({ state }, id) {
return state.todos.find(todo => todo.id === id).text; return state.todos.find(todo => todo.id === id).text;
}, }
}; };
const store = new Store({ state, getters }); const store = new Store({ state, getters });
class TodoItem extends Component<any, any, any> { env.qweb.addTemplate(
inlineTemplate = `<div> "TodoItem",
`<div>
<span><t t-esc="props.activeTodoText"/></span> <span><t t-esc="props.activeTodoText"/></span>
<span><t t-esc="props.importantTodoText"/></span> <span><t t-esc="props.importantTodoText"/></span>
</div>`; </div>`
} );
class TodoItem extends Component<any, any, any> {}
const ConnectedTodo = connect((state, props, getters) => { const ConnectedTodo = connect((state, props, getters) => {
const todo = state.todos.find(t => t.id === props.id); const todo = state.todos.find(t => t.id === props.id);
return { return {
activeTodoText: getters.text(todo.id), activeTodoText: getters.text(todo.id),
importantTodoText: getters.importantTodoText(), importantTodoText: getters.importantTodoText()
}; };
})(TodoItem); })(TodoItem);
class TodoList extends Component<any, any, any> { env.qweb.addTemplate(
inlineTemplate = `<div> "TodoList",
`<div>
<t t-foreach="props.todos" t-as="todo"> <t t-foreach="props.todos" t-as="todo">
<t t-widget="ConnectedTodo" t-props="todo"/> <t t-widget="ConnectedTodo" id="todo.id"/>
</t> </t>
</div>`; </div>`
);
class TodoList extends Component<any, any, any> {
widgets = { ConnectedTodo }; widgets = { ConnectedTodo };
} }
@@ -625,21 +635,25 @@ describe("connecting a component to store", () => {
const app = new ConnectedTodoList(env); const app = new ConnectedTodoList(env);
await app.mount(fixture); 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 () => { test("connected component is updated when props are updated", async () => {
class Beer extends Component<any, any, any> { env.qweb.addTemplate("Beer", `<span><t t-esc="props.name"/></span>`);
inlineTemplate = `<span><t t-esc="props.name"/></span>`; class Beer extends Component<any, any, any> {}
}
const ConnectedBeer = connect((state, props) => { const ConnectedBeer = connect((state, props) => {
return state.beers[props.id]; return state.beers[props.id];
})(Beer); })(Beer);
env.qweb.addTemplate(
"App",
`<div>
<t t-widget="ConnectedBeer" id="state.beerId"/>
</div>`
);
class App extends Component<any, any, any> { class App extends Component<any, any, any> {
inlineTemplate = `<div>
<t t-widget="ConnectedBeer" t-props="{id: state.beerId}"/>
</div>`;
widgets = { ConnectedBeer }; widgets = { ConnectedBeer };
state = { beerId: 1 }; state = { beerId: 1 };
} }
@@ -658,12 +672,14 @@ describe("connecting a component to store", () => {
}); });
test("connected component is updated when store is changed", async () => { test("connected component is updated when store is changed", async () => {
class App extends Component<any, any, any> { env.qweb.addTemplate(
inlineTemplate = ` "App",
`
<div> <div>
<span t-foreach="props.beers" t-as="beer" t-key="beer.name"><t t-esc="beer.name"/></span> <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 = { const mutations = {
addBeer({ state }, name) { addBeer({ state }, name) {
@@ -692,13 +708,15 @@ describe("connecting a component to store", () => {
}); });
test("connected component with undefined, null and string props", async () => { test("connected component with undefined, null and string props", async () => {
class Beer extends Component<any, any, any> { env.qweb.addTemplate(
inlineTemplate = `<div> "Beer",
`<div>
<span>taster:<t t-esc="props.taster"/></span> <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.selected">selected:<t t-esc="props.selected.name"/></span>
<span t-if="props.consumed">consumed:<t t-esc="props.consumed.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) => { const ConnectedBeer = connect((state, props) => {
return { return {
selected: state.beers[props.id], selected: state.beers[props.id],
@@ -707,10 +725,13 @@ describe("connecting a component to store", () => {
}; };
})(Beer); })(Beer);
env.qweb.addTemplate(
"App",
`<div>
<t t-widget="ConnectedBeer" id="state.beerId"/>
</div>`
);
class App extends Component<any, any, any> { class App extends Component<any, any, any> {
inlineTemplate = `<div>
<t t-widget="ConnectedBeer" t-props="{id: state.beerId}"/>
</div>`;
widgets = { ConnectedBeer }; widgets = { ConnectedBeer };
state = { beerId: 0 }; 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 () => { test("connected component deeply reactive with undefined, null and string props", async () => {
class Beer extends Component<any, any, any> { env.qweb.addTemplate(
inlineTemplate = `<div> "Beer",
`<div>
<span>taster:<t t-esc="props.taster"/></span> <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.selected">selected:<t t-esc="props.selected.name"/></span>
<span t-if="props.consumed">consumed:<t t-esc="props.consumed.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) => { const ConnectedBeer = connect((state, props) => {
return { return {
selected: state.beers[props.id], selected: state.beers[props.id],
@@ -771,10 +794,13 @@ describe("connecting a component to store", () => {
}; };
})(Beer); })(Beer);
env.qweb.addTemplate(
"App",
`<div>
<t t-widget="ConnectedBeer" id="state.beerId"/>
</div>`
);
class App extends Component<any, any, any> { class App extends Component<any, any, any> {
inlineTemplate = `<div>
<t t-widget="ConnectedBeer" t-props="{id: state.beerId}"/>
</div>`;
widgets = { ConnectedBeer }; widgets = { ConnectedBeer };
state = { beerId: 0 }; state = { beerId: 0 };
} }
@@ -845,12 +871,16 @@ describe("connecting a component to store", () => {
test("correct update order when parent/children are connected", async () => { test("correct update order when parent/children are connected", async () => {
const steps: string[] = []; const steps: string[] = [];
class Parent extends Component<any, any, any> {
inlineTemplate = ` env.qweb.addTemplate(
"Parent",
`
<div> <div>
<t t-widget="Child" t-ref="'child'" t-props="{key: props.current}"/> <t t-widget="Child" key="props.current"/>
</div> </div>
`; `
);
class Parent extends Component<any, any, any> {
widgets = { Child: ConnectedChild }; widgets = { Child: ConnectedChild };
} }
const ConnectedParent = connect(function(s) { const ConnectedParent = connect(function(s) {
@@ -858,9 +888,8 @@ describe("connecting a component to store", () => {
return { current: s.current, isvisible: s.isvisible }; return { current: s.current, isvisible: s.isvisible };
})(Parent); })(Parent);
class Child extends Component<any, any, any> { env.qweb.addTemplate("Child", `<span><t t-esc="props.msg"/></span>`);
inlineTemplate = `<span><t t-esc="props.msg"/></span>`; class Child extends Component<any, any, any> {}
}
const ConnectedChild = connect(function(s, props) { const ConnectedChild = connect(function(s, props) {
steps.push("child"); 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 () => { test("connected component willpatch/patch hooks are called on store updates", async () => {
const steps: string[] = []; const steps: string[] = [];
env.qweb.addTemplate("App", `<div><t t-esc="props.msg"/></div>`);
class App extends Component<any, any, any> { class App extends Component<any, any, any> {
inlineTemplate = `<div><t t-esc="props.msg"/></div>`;
willPatch() { willPatch() {
steps.push("willpatch"); steps.push("willpatch");
} }
+1 -80
View File
@@ -1,10 +1,4 @@
import { import { escape, debounce } from "../src/utils";
escape,
memoize,
debounce,
patch,
unpatch
} from "../src/utils";
describe("escape", () => { describe("escape", () => {
test("normal strings", () => { 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", () => { describe("debounce", () => {
test("works as expected", () => { test("works as expected", () => {
jest.useFakeTimers(); jest.useFakeTimers();
@@ -54,56 +28,3 @@ describe("debounce", () => {
expect(n).toBe(1); 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);
});
});
+1 -1
View File
@@ -16,5 +16,5 @@
"strictPropertyInitialization": true, "strictPropertyInitialization": true,
"strictNullChecks": true "strictNullChecks": true
}, },
"include": ["demo/static/**/*.ts", "src/*.ts", "tests/*.ts"] "include": ["src/*.ts"]
} }