mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
84 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d3c374b78 | |||
| 1dd257f33c | |||
| 1206afe6be | |||
| 5842ed51b0 | |||
| d22219f084 | |||
| 0be1a54e03 | |||
| 32921ecc20 | |||
| 1c290c1172 | |||
| 2f9d7ea58f | |||
| fe23e76341 | |||
| 73023d4869 | |||
| ff51443b8e | |||
| f8e07f6d30 | |||
| 00da778753 | |||
| 187eb922c0 | |||
| 466c12a0e6 | |||
| 8dc3ec94bf | |||
| a554436d03 | |||
| 34b5049dee | |||
| 0eccfd08fe | |||
| 98b063fd70 | |||
| e9ba94ae55 | |||
| 4d72a4f240 | |||
| a08deb895d | |||
| 60cea14b01 | |||
| f60904b740 | |||
| 376f801541 | |||
| 26f14180e2 | |||
| bced9f3d04 | |||
| 94208d9cbb | |||
| 681983142d | |||
| a525b42537 | |||
| a4cc085b6d | |||
| 837dacf1cb | |||
| 7cdd1aa9a1 | |||
| 24c569e6d5 | |||
| 050694414c | |||
| 639908366a | |||
| 59f401303a | |||
| 649e20b956 | |||
| c2280ff1bb | |||
| 8a32d823cd | |||
| 13b31458da | |||
| 92b3ab32e2 | |||
| 4ca0a157e3 | |||
| 1dec8df715 | |||
| 8f29cfc996 | |||
| da275e74fd | |||
| 3d9f4e028c | |||
| 13577997bc | |||
| 671965026d | |||
| bc7fe9023d | |||
| c2c7997df3 | |||
| a1d7e6c987 | |||
| a1e982aba5 | |||
| 2ce027a0a6 | |||
| 61f21511a5 | |||
| 5682577870 | |||
| 25b633cc63 | |||
| f16b267931 | |||
| 30ec952588 | |||
| 46ba77d0cc | |||
| 042e376c2b | |||
| b9f1af2ad5 | |||
| 7df0fdcc0a | |||
| 73d743a1c3 | |||
| 8758dfcedf | |||
| 0a10bbc023 | |||
| 9c1d369e94 | |||
| 9556696fa8 | |||
| 04eb720619 | |||
| 8e098e8ed6 | |||
| c836cead58 | |||
| 361ecbe90b | |||
| ae8d7d3be2 | |||
| ed4f92211c | |||
| f8804d6501 | |||
| de7c680387 | |||
| 6dd7391cdc | |||
| dfa7d5932d | |||
| 53911de00a | |||
| 1760d0d336 | |||
| 0aba9748c9 | |||
| 8ee0973dab |
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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.
|
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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();
|
||||||
@@ -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>
|
||||||
@@ -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();
|
||||||
@@ -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>
|
||||||
@@ -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();
|
||||||
@@ -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>
|
||||||
@@ -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: [] };
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 + "`";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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, "`");
|
.replace(/`/g, "`");
|
||||||
}
|
}
|
||||||
|
|
||||||
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
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}"
|
||||||
|
`;
|
||||||
@@ -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
@@ -0,0 +1,33 @@
|
|||||||
|
function anonymous(context, extra) {
|
||||||
|
var h = this.utils.h;
|
||||||
|
let c1 = [], p1 = {key:1};
|
||||||
|
var vn1 = h('div', p1, c1);
|
||||||
|
if (context['state'].display) {
|
||||||
|
//WIDGET
|
||||||
|
let _2_index = c1.length;
|
||||||
|
c1.push(null);
|
||||||
|
let def3;
|
||||||
|
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
|
||||||
|
let props4 = {};
|
||||||
|
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
|
||||||
|
w4.destroy();
|
||||||
|
w4 = false
|
||||||
|
}
|
||||||
|
if (!w4) {
|
||||||
|
let W4 = context.widgets['Child'];
|
||||||
|
if (!W4) {throw new Error(`Cannot find the definition of widget "Child"`)}
|
||||||
|
w4 = new W4(owner, props4);
|
||||||
|
context.__owl__.cmap[4] = w4.__owl__.id;
|
||||||
|
def3 = w4._prepare();
|
||||||
|
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert: (vn) => {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;this.utils.transitionInsert(vn.elm, 'chimay');},remove: () => {},destroy: (vn) => {let finalize = () => {
|
||||||
|
w4.destroy();
|
||||||
|
};
|
||||||
|
this.utils.transitionRemove(vn.elm, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
||||||
|
} else {
|
||||||
|
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
||||||
|
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
||||||
|
}
|
||||||
|
extra.promises.push(def3);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,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
File diff suppressed because it is too large
Load Diff
+51
-2
@@ -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
@@ -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 };
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -16,5 +16,5 @@
|
|||||||
"strictPropertyInitialization": true,
|
"strictPropertyInitialization": true,
|
||||||
"strictNullChecks": true
|
"strictNullChecks": true
|
||||||
},
|
},
|
||||||
"include": ["demo/static/**/*.ts", "src/*.ts", "tests/*.ts"]
|
"include": ["src/*.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user