Compare commits

...

25 Commits

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

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

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

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

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

Fixes #132
2019-05-29 13:31:48 +02:00
Géry Debongnie a08deb895d [ADD] benchmarks: add 0.12.0 to the list 2019-05-29 13:24:58 +02:00
Géry Debongnie 60cea14b01 [IMP] extras: improve landing page 2019-05-29 13:16:35 +02:00
30 changed files with 4880 additions and 805 deletions
+5
View File
@@ -1,5 +1,6 @@
/node_modules
/dist
npm-debug.log
# misc
@@ -14,7 +15,11 @@ yarn-debug.log*
yarn-error.log*
package-lock.json
#ide's
.vscode
.idea
node_modules
# Extras temp file
+5 -2
View File
@@ -59,8 +59,8 @@ More interesting examples can be found on the [playground](https://odoo.github.i
If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-0.12.0.js](https://github.com/odoo/owl/releases/download/v0.12.0/owl.js)
- [owl-0.12.0.min.js](https://github.com/odoo/owl/releases/download/v0.12.0/owl.min.js)
- [owl-0.13.0.js](https://github.com/odoo/owl/releases/download/v0.13.0/owl.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:
@@ -82,6 +82,9 @@ The complete documentation can be found [here](doc/readme.md). The most importan
- [Component](doc/component.md)
- [QWeb](doc/qweb.md)
Found an issue in the documentation? A broken link? Some outdated information?
Submit a PR!
## License
OWL is [GPL licensed](./LICENSE).
+57 -6
View File
@@ -1,16 +1,14 @@
# 🦉 Animations 🦉
Animation is a complex topic. There are many different use cases, and many
solutions and technologies. Owl only supports some basic use cases.
## Simple CSS effects
Sometimes, using pure CSS is enough. For these use cases, Owl is not really
Sometimes, using pure CSS is enough. For these use cases, Owl is not really
necessary: it just needs to render a DOM element with a specific class. For
example:
```xml
<a class="btn flash" t-on-click="doSomething">Click</a>
```
@@ -19,7 +17,7 @@ with the following CSS:
```css
btn {
background-color: gray;
background-color: gray;
}
.flash {
@@ -35,10 +33,63 @@ btn {
will produce a nice flash effect whenever the user click (or activate with the
keyboard) the button.
## CSS Transitions (single element)
## CSS Transitions
A more complex situation occurs when we want to transition an element in or out
of the page. For example, we may want a fade-in and fade-out effect.
of the page. For example, we may want a fade-in and fade-out effect.
The `t-transition` directive is here to help us (see [QWeb documentation](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)
+289 -53
View File
@@ -4,15 +4,18 @@
- [Overview](#overview)
- [Example](#example)
- [Composition](#composition)
- [Reference](#reference)
- [Properties](#properties)
- [Static Properties](#static-properties)
- [Methods](#methods)
- [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)
- [Asynchronous rendering](#asynchronous-rendering)
- [Keeping References](#keeping-references])
- [Asynchronous rendering](#asynchronous-rendering)
## Overview
@@ -69,55 +72,6 @@ a state object is defined. It is not mandatory to use the state object, but it
is certainly encouraged. The state object is [observed](observer.md), and any
change to it will cause a rerendering.
## Composition
The example above shows a QWeb template with a `t-on-click` directive. Widget
templates are standard [QWeb](qweb.md) templates, but with an extra directive:
`t-widget`. With the `t-widget` directive, widget templates can declare sub
widgets:
```xml
<div t-name="ParentWidget">
<span>some text</span>
<t t-widget="MyWidget" t-props="{info: 13}"/>
</div>
```
```js
class ParentWidget extends owl.Component {
widgets = { MyWidget: MyWidget};
...
}
```
In this example, the `ParentWidget`'s template creates a widget `MyWidget` just
after the span. See the [QWeb](qweb.md) documentation for more information on the
`t-widget` directive.
Note that the rendering context for the template is the widget itself. This means
that the template can access `state`, `props`, `env`, or any methods defined in the widget.
**CSS and style:** there is some specific support to allow the parent to declare
additional css classes or style for the sub widget: css declared in `class`, `style`, `t-att-class` or `t-att-style` will be added to the
root widget element.
```xml
<div t-name="ParentWidget">
<t t-widget="MyWidget" class="someClass" style="font-weight:bold;" t-props="{info: 13}"/>
</div>
```
Warning: there is a small caveat with dynamic class attributes: since Owl needs
to be able to add/remove proper classes whenever necessary, it needs to be aware
of the possible classes. Otherwise, it will not be able to make the difference
between a valid css class added by the component, or some custom code, and a
class that need to be removed. This is why we only support the explicit syntax
with a class object:
```xml
<t t-widget="MyWidget" t-att-class="{a: state.flagA, b: state.flagB}" />
```
## Reference
An Owl component is a small class which represent a widget or some UI element.
@@ -358,6 +312,241 @@ the DOM. This is a good place to remove some listeners, for example.
This is the opposite method of `mounted`.
## Composition
The example above shows a QWeb template with a `t-on-click` directive. Widget
templates are standard [QWeb](qweb.md) templates, but with an extra directive:
`t-widget`. With the `t-widget` directive, widget templates can declare sub
widgets:
```xml
<div t-name="ParentWidget">
<span>some text</span>
<t t-widget="MyWidget" info="13"/>
</div>
```
```js
class ParentWidget extends owl.Component {
widgets = { MyWidget: MyWidget};
...
}
```
In this example, the `ParentWidget`'s template creates a widget `MyWidget` just
after the span. The `info` key will be added to the subwidget's props. Each
props is a string which represents a javascript (QWeb) expression, so it is
dynamic. If it is necessary to give a string, this can be done by quoting it:
`someString="'somevalue'"`. See the
[QWeb](qweb.md) documentation for more information on the `t-widget` directive.
Note that the rendering context for the template is the widget itself. This means
that the template can access `state`, `props`, `env`, or any methods defined in the widget.
The `t-widget` directive is the key to a declarative component
system. It allows a template to define where and how a sub widget is created
and/or updated. For example:
```xml
<div t-name="ParentWidget">
<t t-widget="ChildWidget" count="state.val"/>
</div>
```
```js
class ParentWidget {
widgets = { ChildWidget };
state = { val: 4 };
}
```
Whenever the template is rendered, it will automatically create the subwidget
`ChildWidget` at the correct place. It needs to find the reference to the
actual component class in the special `widgets` key, or the class registered in
QWeb's global registry (see `register` function of QWeb). It first looks inside
the local `widgets` key, then fallbacks on the global registry.
_Props_: In this example, the child widget will receive the object `{count: 4}` in its
constructor. This will be assigned to the `props` variable, which can be accessed
on the widget (and also, in the template). Whenever the state is updated, then
the subwidget will also be updated automatically.
Note that there are some restrictions on prop names: `class`, `style` and any
string which starts with `t-` are not allowed.
The `t-widget` directive also accepts dynamic values with string interpolation
(like the [`t-attf-`](#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
We give here an informal description of the way components are created/updated
@@ -532,7 +721,54 @@ Examples:
};
```
## Asynchronous rendering
### 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
+216 -386
View File
@@ -3,44 +3,76 @@
## Content
- [Overview](#overview)
- [Directives](#directives)
- [QWeb Engine](#qweb-engine)
- [QWeb Specification](#qweb-specification)
- [Static html nodes](#static-html-nodes)
- [`t-esc` directive](#t-esc-directive)
- [`t-raw` directive](#t-raw-directive)
- [`t-set` directive](#t-set-directive)
- [`t-if` directive](#t-if-directive)
- [Expression evaluation](#expression-evaluation)
- [Dynamic attributes (`t-att` and `t-attf` directives)](#dynamic-attributes-t-att-and-t-attf-directives)
- [`t-call` directive (sub templates)](#t-call-directive-sub-templates)
- [JS/OWL Specific Extensions](#jsowl-specific-extensions)
- [`t-on` directive](#t-on-directive)
- [Component: `t-widget`, `t-props`](#component-t-widget-t-props)
- [`t-ref` directive](#t-ref-directive)
- [`t-key` directive](#t-key-directive)
- [`t-transition` directive](#t-transition-directive)
- [`t-mounted` directive](#t-mounted-directive)
- [Debugging (`t-debug` and `t-log`)](#debugging-t-debug-and-t-log)
- [Reference](#qweb-specification)
- [White spaces](#white-spaces)
- [Root nodes](#root-nodes)
- [Expression evaluation](#expression-evaluation)
- [Static html nodes](#static-html-nodes)
- [Outputting data](#outputting-data)
- [Setting Variables](#setting-variables)
- [Conditionals](#conditionals)
- [Dynamic attributes](#dynamic-attributes)
- [Loops](#loops)
- [Rendering Sub Templates](#rendering-sub-templates)
- [Debugging](#debugging)
## Overview
[QWeb](https://www.odoo.com/documentation/12.0/reference/qweb.html) is the primary templating engine used by Odoo. It is based on the XML format, and used
mostly to generate html. In OWL, QWeb templates are compiled into functions that
generate a virtual dom representation of the html.
mostly to generate HTML. In OWL, QWeb templates are compiled into functions that
generate a virtual dom representation of the HTML.
Template directives are specified as XML attributes prefixed with `t-`, for instance `t-if` for conditionals, with elements and other attributes being rendered directly.
To avoid element rendering, a placeholder element `<t>` is also available, which executes its directive but doesnt generate any output in and of itself.
The QWeb implementation in the OWL project is slightly different. It compiles
templates into functions that output a virtual DOM instead of a string. This is
necessary for the component system. In addition, it has a few extra directives
(see [OWL Specific Extensions](#owlspecificextensions))
```xml
<div>
<span t-if="somecondition">Some string</span>
<ul t-else="1">
<li t-foreach="messages" t-as="message">
<t t-esc="message">
</li>
</ul>
</div>
```
The QWeb class in the OWL project is an implementation of that specification
with a few interesting points:
- it compiles templates into functions that output a virtual DOM instead of a
string. This is necessary for the component system.
- it has a few extra directives: `t-widget`, `t-on`, ...
## Directives
We present here a list of all standard QWeb directives:
| Name | Description |
| ------------------------------ | ------------------------------------------------------------ |
| `t-esc` | [Outputting safely a value](#outputting-data) |
| `t-raw` | [Outputting value, without escaping](#outputting-data) |
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
| `t-if`, `t-elif`, `t-else`, | [conditionally rendering](#conditionals) |
| `t-foreach`, `t-as` | [Loops](#loops) |
| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
| `t-call` | [Rendering sub templates](#rendering-sub-templates) |
| `t-debug`, `t-log` | [Debugging](#debugging) |
| `t-name` | [Defining a template (not really a directive)](#qweb-engine) |
The component system in Owl requires additional directives, to express various
needs. Here is a list of all Owl specific directives:
| Name | Description |
| ------------------------- | --------------------------------------------------------------------------------------- |
| `t-widget`, `t-keepalive` | [Defining a sub component](component.md#composition) |
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#keeping-references) |
| `t-key` | [Defining a key (to help virtual dom reconciliation)](component.md#t-key-directive) |
| `t-on-*` | [Event handling](component.md#event-handling) |
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
| `t-mounted` | [Callback when a node or component is mounted](#component.md#t-mounted-directive) |
## QWeb Engine
@@ -101,12 +133,92 @@ It's API is quite simple:
qweb.addTemplate("ParentWidget", "<div><t t-widget='Dialog'/></div>");
```
## QWeb Specification
## Reference
We define in this section the specification of how `QWeb` templates should be
rendered.
rendered. Note that we only document here the standard QWeb specification. Owl
specific extensions are documented in various other parts of the documentation.
### Static html nodes
### White spaces
White spaces in a templates are handled in a special way:
- consecutive whitespaces are always condensed to a single whitespace
- if a whitespace-only text node contains a linebreak, it is ignored
- the previous rules do not apply if we are in a `<pre>` tag
### Root nodes
For many reasons, Owl QWeb templates should have a single root node. More
precisely, the result of a template rendering should have a single root node:
```xml
<!–– not ok: two root nodes ––>
<t>
<div>foo</div>
<div>bar</div>
</t>
<!–– ok: result has one single root node ––>
<t>
<div t-if="someCondition">foo</div>
<span t-else="1">bar</span>
</t>
```
Extra root nodes will actually be ignored (even though they will be rendered
in memory).
Note: this does not apply to subtemplates (see the `t-call` directive). In that
case, they will be inlined in the main template, and can actually have many
root nodes.
### Expression evaluation
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:
@@ -114,7 +226,7 @@ Normal, regular html nodes are rendered into themselves:
<div>hello</div> <!–– rendered as itself ––>
```
### `t-esc` directive
### Outputting Data
The `t-esc` directive is necessary whenever you want to add a dynamic text
expression in a template. The text is escaped to avoid security issues.
@@ -129,14 +241,12 @@ rendered with the value `value` set to `42` in the rendering context yields:
<p>42</p>
```
### `t-raw` directive
The `t-raw` directive is almost the same as `t-esc`, but without the escaping.
This is mostly useful to inject a raw html string somewhere. Obviously, this
is unsafe to do in general, and should only be used for strings known to be safe.
```xml
<p><t t-esc="value"/></p>
<p><t t-raw="value"/></p>
```
rendered with the value `value` set to `<span>foo</span>` in the rendering context yields:
@@ -145,7 +255,7 @@ rendered with the value `value` set to `<span>foo</span>` in the rendering conte
<p><span>foo</span></p>
```
### `t-set` directive
### Setting Variables
QWeb allows creating variables from within the template, to memoize a computation (to use it multiple times), give a piece of data a clearer name, ...
@@ -177,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.
It is lexically scoped (inner nodes are sub scopes), can be shadowed, ...
### `t-if` directive
### Conditionals
The `t-if` directive is useful to conditionally render something. It evaluates
the expression given as attribute value, and then acts accordingly.
@@ -227,52 +337,7 @@ Extra conditional branching directives `t-elif` and `t-else` are also available:
</div>
```
### Expression evaluation
It is useful to explain the various rules that applies on QWeb expressions. These
expressions are strings that will be converted to a javascript expression at
compile time.
1. it should be a simple expression which returns a value. It cannot be a statement.
```xml
<div><p t-if="1 + 2 === 3">ok</p></div>
```
is valid, but the following is not valid:
```xml
<div><p t-if="console.log(1)">NOT valid</p></div>
```
2. it can use anything in the rendering context:
```xml
<p t-if="user.birthday == today()">Happy bithday!</p>
```
is valid, and will read the `user` object from the context, and call the
`today` function.
3. it can use a few special operators to avoid using symbols such as `<`, `>`,
`&` or `|`. This is useful to make sure that we still write valid XML.
| Word | will be replaced by |
| ----- | ------------------- |
| `and` | `&&` |
| `or` | `\|\|` |
| `gt` | `>` |
| `gte` | `>=` |
| `lt` | `<` |
| `lte` | `<=` |
So, one can write this:
```xml
<div><p t-if="10 + 2 gt 5">ok</p></div>
```
### Dynamic attributes (`t-att` and `t-attf` directives)
### Dynamic attributes
One can use the `t-att-` directive to add dynamic attributes. Its main use is to
evaluate an expression (at rendering time) and bind an attribute to its result:
@@ -304,7 +369,75 @@ For historical reason, there is an alternate form of string interpolation:
<!-- result if values are set to 1,2 and 3: <div foo="a 0 is 1 of 2 ]"></div> -->
```
### `t-call` directive (sub templates)
### 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 directives 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 iteratees 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
from within another template (to avoid duplication or give names to parts of
@@ -352,276 +485,7 @@ will result in :
</div>
```
## JS/OWL Specific Extensions
### `t-on` directive
In a component's template, it is useful to be able to register handlers on some
elements to some specific events. This
is what makes a template _alive_. There are two different use cases.
1. Register an event handler on a DOM node
```xml
<button t-on-click="someMethod">Do something</button>
```
This will be roughly translated in javascript like this:
```js
button.addEventListener("click", widget.someMethod.bind(widget));
```
The suffix (`click` in this example) is simply the name of the actual DOM
event.
In order to remove the DOM event details from the event handlers (like calls
to `event.preventDefault`) and let them focus on data logic, _modifiers_ can
be specified as additional suffixes of the `t-on` directive.
| Modifier | Description |
| ---------- | ----------------------------------------------------------------- |
| `.stop` | calls `event.stopPropagation()` before calling the method |
| `.prevent` | calls `event.preventDefault()` before calling the method |
| `.self` | calls the method only if the `event.target` is the element itself |
```xml
<button t-on-click.stop="someMethod">Do something</button>
```
Note that modifiers can be combined (ex: `t-on-click.stop.prevent`), and that
the order may matter. For instance `t-on-click.prevent.self` will prevent all
clicks while `t-on-click.self.prevent` will only prevent clicks on the
element itself.
2. Register an event handler on a component. This will not capture a DOM event,
but rather a _business_ event:
```xml
<t t-widget="MyWidget" t-on-menuLoaded="someMethod"/>
```
```js
class MyWidget {
someWhere() {
const payload = ...;
this.trigger('menuLoaded', payload);
}
}
```
Here, the parent widget will receive the payload in its `someMethod` handler,
whenever the event is triggered.
The `t-on` directive also allows to prebind some arguments. For example,
```xml
<button t-on-click="someMethod(expr)">Do something</button>
```
Here, `expr` is a valid Owl expression, so it could be `true` or some variable
from the rendering context.
### Component: `t-widget`, `t-props`
The `t-widget` and the `t-props` directives are the key to a declarative component
system. They allow a template to define where and how a sub widget is created
and/or updated. For example:
```xml
<div t-name="ParentWidget">
<t t-widget="ChildWidget" t-props="{count: state.val}"/>
</div>
```
```js
class ParentWidget {
widgets = { ChildWidget };
state = { val: 4 };
}
```
Whenever the template is rendered, it will automatically create the subwidget
`ChildWidget` at the correct place. It needs to find the reference to the
actual component class in the special `widgets` key, or the class registered in
QWeb's global registry (see `register` function of QWeb). It first looks inside
the local `widgets` key, then fallbacks on the global registry.
In this example, the child widget will receive the object `{count: 4}` in its
constructor. This will be assigned to the `props` variable, which can be accessed
on the widget (and also, in the template). Whenever the state is updated, then
the subwidget will also be updated automatically.
The `t-widget` directive also accepts dynamic values with string interpolation
(like the [`t-attf-`](#dynamic-attributes-t-att-and-t-attf-directives) directive):
```xml
<div t-name="ParentWidget">
<t t-widget="ChildWidget#{id}"/>
</div>
```
```js
class ParentWidget {
widgets = { ChildWidget1, ChildWidget2 };
state = { id: 1 };
}
```
Similarly to `t-attf-`, there is an alternate form of string interpolation:
```xml
<div t-name="ParentWidget">
<t t-widget="ChildWidget{{id}}"/>
</div>
```
### `t-ref` directive
The `t-ref` directive helps a component keep reference to some inside part of it.
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-t-props) directives). For example, if we have
`id` set to 44 in the rendering context,
```xml
<div t-ref="widget_#{id}"/>
```
```js
this.refs.widget_44;
```
Similarly to `t-attf-` and `t-widget`, there is an alternate form of string
interpolation:
```xml
<div t-ref="widget_{{id}}"/>
```
### `t-key` directive
Even though Owl tries to be as declarative as possible, some DOM state is still
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 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).
- the value for `t-transition` must be a single class name. Owl does not support
more than one transition on a single node.
### `t-mounted` directive
The `t-mounted` directive allows to register a callback to execute whenever the node
is inserted into the DOM.
```xml
<div><input t-ref="someInput" t-mounted="focusMe"/></div>
```
```js
class MyWidget extends owl.Component {
...
focusMe() {
this.refs.someInput.focus();
}
}
```
### Debugging (`t-debug` and `t-log`)
### Debugging
The javascript QWeb implementation provides two useful debugging directives:
@@ -643,37 +507,3 @@ will stop execution if the browser dev tools are open.
```
will print 42 to the console
### White spaces
White spaces in a templates are handled in a special way:
- consecutive whitespaces are always condensed to a single whitespace
- if a whitespace-only text node contains a linebreak, it is ignored
- the previous rules do not apply if we are in a `<pre>` tag
### Root nodes
For many reasons, Owl QWeb templates should have a single root node. More
precisely, the result of a template rendering should have a single root node:
```xml
<!–– not ok: two root nodes ––>
<t>
<div>foo</div>
<div>bar</div>
</t>
<!–– ok: result has one single root node ––>
<t>
<div t-if="someCondition">foo</div>
<span t-else="1">bar</span>
</t>
```
Extra root nodes will actually be ignored (even though they will be rendered
in memory).
Note: this does not apply to subtemplates (see the `t-call` directive). In that
case, they will be inlined in the main template, and can actually have many
root nodes.
+84
View File
@@ -0,0 +1,84 @@
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
template = "counter";
state = { counter: 0 };
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
template = "message";
widgets = { Counter };
shouldUpdate(nextProps) {
return nextProps !== this.props;
}
removeMessage() {
this.trigger("remove_message", {
id: this.props.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
template = "root";
widgets = { Message };
state = { messages: [] };
addMessages(n) {
startMeasure("add " + n);
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
stopMeasure();
}
clear() {
startMeasure("clear");
this.state.messages = [];
stopMeasure();
}
updateSomeMessages() {
startMeasure("update every 10th");
const messages = this.state.messages;
for (let i = 0; i < this.state.messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += '!!!';
this.set(messages, i, msg);
}
stopMeasure();
}
removeMessage(data) {
startMeasure("remove message");
const index = this.state.messages.findIndex(m => m.id === data.id);
this.state.messages.splice(index, 1);
stopMeasure();
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.12.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<div id='main'></div>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,32 @@
<templates>
<div t-name="root" class="main">
<div class="left-thing">
<div>Number of msg: <t t-esc="state.messages.length"/></div>
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1000 messages</button>
<button t-on-click="addMessages(10000)">Add 10000 messages</button>
<button t-on-click="addMessages(50000)">Add 50000 messages</button>
<button t-on-click="updateSomeMessages">Update every 10th message</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="right-thing">
<div class="content">
<t t-foreach="state.messages" t-as="message">
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
</t>
</div>
</div>
</div>
<div t-name="message" class="message">
<span class="author"><t t-esc="props.author"/></span>
<span class="msg"><t t-esc="props.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<t t-widget="Counter"/>
</div>
<div t-name="counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
+1 -1
View File
@@ -12,7 +12,7 @@
<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 t-widget="Message" t-key="message.id" message="message" t-on-remove_message="removeMessage"/>
</t>
</div>
</div>
+18 -13
View File
@@ -22,21 +22,26 @@
</div>
</div>
<div class="section benchmarks">
<div class="section">
<h2>Benchmarks</h2>
<ul>
<li><a href="benchmarks/odoo-widgets-12.0">Odoo Widgets (12.0)</a></li>
<li><a href="benchmarks/odoo-widgets-12.3">Odoo Widgets (12.3)</a></li>
<li><a href="benchmarks/owl-0.7.0">OWL 0.7.0</a></li>
<li><a href="benchmarks/owl-0.8.0">OWL 0.8.0</a></li>
<li><a href="benchmarks/owl-0.9.0">OWL 0.9.0</a></li>
<li><a href="benchmarks/owl-0.10.0">OWL 0.10.0</a></li>
<li><a href="benchmarks/owl-0.11.0">OWL 0.11.0</a></li>
<li><a href="benchmarks/owl-master">OWL Master</a></li>
<li><a href="benchmarks/vue">Vue</a></li>
<li><a href="benchmarks/react">React</a></li>
</ul>
<div class="benchmarks">
<ul>
<li><a href="benchmarks/owl-0.7.0">OWL 0.7.0</a></li>
<li><a href="benchmarks/owl-0.8.0">OWL 0.8.0</a></li>
<li><a href="benchmarks/owl-0.9.0">OWL 0.9.0</a></li>
<li><a href="benchmarks/owl-0.10.0">OWL 0.10.0</a></li>
<li><a href="benchmarks/owl-0.11.0">OWL 0.11.0</a></li>
<li><a href="benchmarks/owl-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>
</body>
+19 -12
View File
@@ -1,23 +1,30 @@
body {
font-family: sans-serif;
font-family: sans-serif;
}
.title, .section {
width: 768px;
margin: auto;
.title,
.section {
width: 768px;
margin: auto;
}
.title div {
color: #333333;
font-style: italic;
color: #333333;
font-style: italic;
}
.section {
background-color: #d5d5d5;
padding: 16px;
margin-top: 10px;
margin-bottom: 10px;
background-color: #d5d5d5;
padding: 16px;
margin-top: 10px;
margin-bottom: 10px;
}
.links a {
margin: 10px 5px;
}
margin: 10px 5px;
}
.benchmarks {
display: grid;
grid-template-columns: auto auto;
}
+30 -27
View File
@@ -39,26 +39,32 @@ const DEFAULT_HTML = `<!DOCTYPE html>
</html>
`;
const APP_PY = `import sys
import thread
import webbrowser
const APP_PY = `#!/usr/bin/env python3
import threading
import time
import BaseHTTPServer, SimpleHTTPServer
from http.server import SimpleHTTPRequestHandler, HTTPServer
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()
thread.start_new_thread(start_server,())
url = 'http://127.0.0.1:3600'
webbrowser.open_new(url)
while True:
try:
time.sleep(1)
except KeyboardInterrupt:
sys.exit(0)
if __name__ == "__main__":
print("Owl Application")
print("---------------")
print("Server running on: {}".format(url))
threading.Thread(target=start_server, daemon=True).start()
while True:
try:
time.sleep(1)
except KeyboardInterrupt:
httpd.server_close()
quit(0)
`;
/**
@@ -242,20 +248,20 @@ class App extends owl.Component {
});
}
updateCode(ev) {
this.state[ev.type] = ev.value;
this.state[ev.detail.type] = ev.detail.value;
}
toggleLayout() {
this.state.splitLayout = !this.state.splitLayout;
}
updatePanelHeight(ev) {
if (!ev.delta) {
if (!ev.detail.delta) {
return;
}
let height = this.state.topPanelHeight;
if (!height) {
height = document.getElementsByClassName("tabbed-editor")[0].clientHeight;
}
this.state.topPanelHeight = height + ev.delta;
this.state.topPanelHeight = height + ev.detail.delta;
}
async downloadCode() {
@@ -287,10 +293,11 @@ class TabbedEditor extends owl.Component {
this.sessions[tab].setUndoManager(new ace.UndoManager());
}
}
this.editor = null;
}
mounted() {
this.editor = ace.edit(this.refs.editor);
this.editor = this.editor || ace.edit(this.refs.editor);
this.editor.setValue(this.props[this.state.currentTab], -1);
this.editor.setFontSize("12px");
@@ -314,20 +321,16 @@ class TabbedEditor extends owl.Component {
const session = this.sessions[this.state.currentTab];
session.setValue(this.props[this.state.currentTab], -1);
this.editor.setSession(session);
}
willUnmount() {
this.editor.destroy();
delete this.editor;
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);
}
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) {
+2
View File
@@ -185,4 +185,6 @@ body {
font-size: 30px;
color: darkred;
text-align: center;
padding-left: 30px;
padding-right: 30px;
}
+3 -3
View File
@@ -85,7 +85,7 @@ const WIDGET_COMPOSITION_XML = `<templates>
<div t-name="App">
<t t-widget="ClickCounter"/>
<t t-widget="InputWidget" t-props="{reverse: true}"/>
<t t-widget="InputWidget" reverse="true"/>
</div>
</templates>`;
@@ -287,7 +287,7 @@ const LIFECYCLE_DEMO_XML = `<templates>
<button t-on-click="increment">Increment</button>
<button t-on-click="toggleSubWidget">ToggleSubWidget</button>
<div t-if="state.flag">
<t t-widget="HookWidget" t-props="{n:state.n}"/>
<t t-widget="HookWidget" n="state.n"/>
</div>
</div>
<div t-name="HookWidget" t-on-click="increment">Demo Sub Widget. Props: <t t-esc="props.n"/>. State: <t t-esc="state.n"/>. (click on me to update me)</div>
@@ -515,7 +515,7 @@ const TODO_APP_STORE_XML = `<templates>
<label for="toggle-all"></label>
<ul class="todo-list">
<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>
</ul>
</section>
+12 -10
View File
@@ -11,7 +11,9 @@
</div>
<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">
<a class="btn run-code flash" t-on-click="runCode" title="Execute this Code">▶ Run</a>
<select t-on-change="setSample">
@@ -22,20 +24,20 @@
<a class="btn flash" t-on-click="downloadCode" title="Download a Zip with this Code"><i class="fas fa-download"></i></a>
<a class="layout-selector flash" t-on-click="toggleLayout" title="Toggle Layout"><i class="fas" t-att-class="state.splitLayout ? 'fa-toggle-on' : 'fa-toggle-off'"></i></a>
</div>
<t t-widget="TabbedEditor"
js="state.js"
css="!state.splitLayout and state.css"
xml="!state.splitLayout and state.js"
t-att-style="topEditorStyle"/>
<t t-if="state.splitLayout">
<t t-widget="TabbedEditor"
t-props="{js:state.js, css:false, xml: false}"
t-on-updateCode="updateCode"
t-att-style="topEditorStyle"/>
<div class="separator horizontal"/>
<t t-widget="TabbedEditor" t-keepalive="1"
t-props="{js:false, css:state.css, xml: state.xml, resizeable: true}"
t-on-updateCode="updateCode"
js="false"
css="state.css"
xml="state.xml"
resizeable="true"
t-on-updatePanelHeight="updatePanelHeight"/>
</t>
<t t-else="1">
<t t-widget="TabbedEditor" t-props="{js:state.js, css:state.css, xml: state.xml, display: 'js|xml|css'}" t-on-updateCode="updateCode"/>
</t>
</div>
<div class="separator vertical" t-on-mousedown="onMouseDown"/>
<div class="right-pane" t-att-style="rightPaneStyle">
+23 -15
View File
@@ -1,10 +1,8 @@
import sys
import thread
import webbrowser
import time
#!/usr/bin/env python3
import BaseHTTPServer
import SimpleHTTPServer
import threading
import time
from http.server import SimpleHTTPRequestHandler, HTTPServer
HOST = '127.0.0.1'
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.
# With this, we can simply copy the playground folder as is in the gh-page when
# we want to update the playground.
class OWLHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
class OWLHandler(SimpleHTTPRequestHandler):
def do_GET(self):
if self.path == '/extras/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():
httpd = BaseHTTPServer.HTTPServer((HOST, PORT), OWLHandler)
httpd.serve_forever()
OWLHandler.extensions_map['.js'] = 'application/javascript'
if __name__ == "__main__":
thread.start_new_thread(start_server, ())
webbrowser.open_new(URL)
print("Owl Extras")
print("----------")
print("Server running on: {}".format(URL))
httpd = HTTPServer((HOST, PORT), OWLHandler)
threading.Thread(target=httpd.serve_forever, daemon=True).start()
while True:
try:
time.sleep(1)
except KeyboardInterrupt:
sys.exit(0)
httpd.server_close()
quit(0)
+4 -3
View File
@@ -1,17 +1,18 @@
{
"name": "owl",
"version": "0.12.0",
"version": "0.13.0",
"description": "Odoo Web Library (OWL)",
"main": "src/index.ts",
"scripts": {
"build:js": "tsc --target esnext --module es6 --outDir dist/owl src/*",
"build:js": "tsc --target esnext --module es6 --outDir dist/owl",
"build:bundle": "rollup -c",
"build": "npm run build:js && npm run build:bundle",
"minify": "uglifyjs dist/owl.js -o dist/owl.min.js --compress --mangle",
"test": "jest",
"test:watch": "jest --watch",
"extras:serve": "python extras/server.py",
"extras:serve": "python3 extras/server.py || 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": {
+116 -64
View File
@@ -1,4 +1,3 @@
import { EventBus } from "./event_bus";
import { Observer } from "./observer";
import { QWeb, CompiledTemplate } from "./qweb_core";
import { h, patch, VNode } from "./vdom";
@@ -18,10 +17,24 @@ import { h, patch, VNode } from "./vdom";
// Types/helpers
//------------------------------------------------------------------------------
/**
* An Env (environment) is an object that will be (mostly) shared between all
* components of an Owl application. It is the location which should contain
* the qweb instance necessary to render all components.
*
* Note that it is totally fine to extend the environment with application
* specific keys/objects/whatever. For example, a key `isMobile` (to declare
* if we are in "mobile" mode), or a shared bus could be useful.
*/
export interface Env {
qweb: QWeb;
}
/**
* This is mostly an internal detail of implementation. The Meta interface is
* useful to typecheck and describe the internal keys used by Owl to manage the
* component tree.
*/
export interface Meta<T extends Env, Props> {
readonly id: number;
vnode: VNode | null;
@@ -53,14 +66,14 @@ const TEMPLATE_MAP: { [key: number]: { [name: string]: string } } = {};
//------------------------------------------------------------------------------
let nextId = 1;
export class Component<
T extends Env,
Props extends {},
State extends {}
> extends EventBus {
export class Component<T extends Env, Props extends {}, State extends {}> {
readonly __owl__: Meta<Env, Props>;
template?: string;
/**
* The `el` is the root element of the widget. Note that it could be null:
* this is the case if the widget is not mounted yet, or is destroyed.
*/
get el(): HTMLElement | null {
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
}
@@ -101,8 +114,6 @@ export class Component<
* the t-widget directive in a template)
*/
constructor(parent: Component<T, any, any> | T, props?: Props) {
super();
const defaultProps = (<any>this.constructor).defaultProps;
if (defaultProps) {
props = this._applyDefaultProps(props, defaultProps);
@@ -220,10 +231,16 @@ export class Component<
// Public
//--------------------------------------------------------------------------
/**
* Mount the component to a target element.
*
* This should only be done if the component was created manually. Components
* created declaratively in templates are managed by the Owl system.
*/
async mount(target: HTMLElement): Promise<void> {
const vnode = await this._prepare();
if (this.__owl__.isDestroyed) {
// widget was destroyed before we get here...
// component was destroyed before we get here...
return;
}
this._patch(vnode);
@@ -234,36 +251,6 @@ export class Component<
}
}
_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();
}
}
}
unmount() {
if (this.__owl__.isMounted) {
this._callWillUnmount();
@@ -304,6 +291,15 @@ export class Component<
}
}
/**
* 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() {
const __owl__ = this.__owl__;
if (!__owl__.isDestroyed) {
@@ -315,27 +311,11 @@ export class Component<
}
}
_destroy(parent) {
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;
}
this.clear();
__owl__.isDestroyed = true;
delete __owl__.vnode;
}
/**
* This method is called by the component system whenever its props are
* updated. If it returns true, then the component will be rendered.
* Otherwise, it will skip the rendering (also, its props will not be updated)
*/
shouldUpdate(nextProps: Props): boolean {
return true;
}
@@ -360,14 +340,85 @@ export class Component<
}
}
/**
* 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) {
this.__owl__.observer!.set(target, key, value);
}
/**
* Emit a custom event of type 'eventType' with the given 'payload' on the
* component's el, if it exists. However, note that the event will only bubble
* up to the parent DOM nodes. Thus, it must be called between mounted() and
* willUnmount().
*/
trigger(eventType: string, payload?: any) {
if (this.el) {
const ev = new CustomEvent(eventType, {
bubbles: true,
cancelable: true,
detail: payload
});
this.el.dispatchEvent(ev);
}
}
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
_destroy(parent: Component<any, any, any> | null) {
const __owl__ = this.__owl__;
const isMounted = __owl__.isMounted;
if (isMounted) {
this.willUnmount();
__owl__.isMounted = false;
}
const children = __owl__.children;
for (let key in children) {
children[key]._destroy(this);
}
if (parent) {
let id = __owl__.id;
delete parent.__owl__.children[id];
__owl__.parent = null;
}
__owl__.isDestroyed = true;
delete __owl__.vnode;
}
_callMounted() {
const __owl__ = this.__owl__;
const children = __owl__.children;
for (let id in children) {
const comp = children[id];
if (!comp.__owl__.isMounted && this.el!.contains(comp.el)) {
comp._callMounted();
}
}
__owl__.isMounted = true;
const handlers = __owl__.mountedHandlers;
for (let key in handlers) {
handlers[key]();
}
this.mounted();
}
_callWillUnmount() {
this.willUnmount();
const __owl__ = this.__owl__;
__owl__.isMounted = false;
const children = __owl__.children;
for (let id in children) {
const comp = children[id];
if (comp.__owl__.isMounted) {
comp._callWillUnmount();
}
}
}
async _updateProps(
nextProps: Props,
forceUpdate: boolean = false,
@@ -403,7 +454,7 @@ export class Component<
async _prepareAndRender(): Promise<VNode> {
await this.willStart();
const __owl__ = this.__owl__;
const __owl__ = this.__owl__;
if (__owl__.isDestroyed) {
return Promise.resolve(h("div"));
}
@@ -439,11 +490,12 @@ export class Component<
this._observeState();
return this._render();
}
async _render(
force: boolean = false,
patchQueue: any[] = []
): Promise<VNode> {
const __owl__ = this.__owl__;
const __owl__ = this.__owl__;
__owl__.renderId++;
const promises: Promise<void>[] = [];
const patch: any[] = [this];
@@ -480,7 +532,7 @@ export class Component<
* Only called by qweb t-widget directive
*/
_mount(vnode: VNode, elm: HTMLElement): VNode {
const __owl__ = this.__owl__;
const __owl__ = this.__owl__;
__owl__.vnode = patch(elm, vnode);
if (__owl__.parent!.__owl__.isMounted && !__owl__.isMounted) {
this._callMounted();
@@ -492,7 +544,7 @@ export class Component<
* Only called by qweb t-widget directive (when t-keepalive is set)
*/
_remount() {
const __owl__ = this.__owl__;
const __owl__ = this.__owl__;
if (!__owl__.isMounted) {
__owl__.isMounted = true;
this.mounted();
+18 -9
View File
@@ -108,9 +108,15 @@ export class Observer {
}
set(target: any, key: number | string, value: any) {
this.rev++;
this._addProp(target, key, value);
target.__owl__.rev++;
let alreadyDefined =
key in target &&
Object.getOwnPropertyDescriptor(target, key)!.configurable === false;
if (alreadyDefined) {
target[key] = value;
} else {
this._addProp(target, key, value);
this._updateRevNumber(target);
}
this.notifyChange();
}
@@ -145,23 +151,26 @@ export class Observer {
},
set(newVal) {
if (newVal !== value) {
self.rev++;
if (!self.allowMutations) {
throw new Error(
`Observed state cannot be changed here! (key: "${key}", val: "${newVal}")`
);
}
self._updateRevNumber(obj);
value = newVal;
self.observe(newVal, obj);
obj.__owl__.rev!++;
let parent = obj;
do {
parent.__owl__.deepRev++;
} while ((parent = parent.__owl__.parent) && parent !== obj);
self.notifyChange();
}
}
});
this.observe(value, obj);
}
_updateRevNumber(target: any) {
this.rev++;
target.__owl__.rev!++;
let parent = target;
do {
parent.__owl__.deepRev++;
} while ((parent = parent.__owl__.parent) && parent !== target);
}
}
+21 -8
View File
@@ -99,6 +99,7 @@ const NODE_HOOKS_PARAMS = {
interface Utils {
h: typeof h;
objectToAttrString(obj: Object): string;
shallowEqual(p1: Object, p2: Object): boolean;
[key: string]: any;
}
@@ -112,6 +113,14 @@ export const UTILS: Utils = {
}
}
return classes.join(" ");
},
shallowEqual(p1, p2) {
for (let k in p1) {
if (p1[k] !== p2[k]) {
return false;
}
}
return true;
}
};
@@ -296,11 +305,14 @@ export class QWeb {
ctx.code.join("\n")
) as CompiledTemplate;
} catch (e) {
const templateName = ctx.templateName.replace(/`/g, "'");
console.groupCollapsed(`Invalid Code generated by ${templateName}`);
console.warn(ctx.code.join("\n"));
console.groupEnd();
throw new Error(
`Invalid generated code while compiling template '${ctx.templateName.replace(
/`/g,
"'"
)}': ${e.message}`
`Invalid generated code while compiling template '${templateName}': ${
e.message
}`
);
}
if (isDebug) {
@@ -754,19 +766,20 @@ export class Context {
/**
* Perform string interpolation on the given string. Note that if the whole
* string is an expression, it simply returns it (formatted).
* string is an expression, it simply returns it (formatted and enclosed in
* parentheses).
* For instance:
* 'Hello {{x}}!' -> `Hello ${x}`
* '{{x}}' -> 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));
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));
return `(${this.formatExpression(s.slice(2, -1))})`;
}
let formatter = expr => "${" + this.formatExpression(expr) + "}";
+83 -35
View File
@@ -9,7 +9,7 @@ import { QWeb, UTILS } from "./qweb_core";
* - t-on
* - t-ref
* - t-transition
* - t-widget/t-props/t-keepalive
* - t-widget/t-keepalive
* - t-mounted
*/
@@ -182,6 +182,10 @@ QWeb.addDirective({
// t-widget
//------------------------------------------------------------------------------
const T_WIDGET_MODS_CODE = Object.assign({}, MODS_CODE, {
self: "if (e.target !== vn.elm) {return}"
});
/**
* The t-widget directive is certainly a complicated and hard to maintain piece
* of code. To help you, fellow developer, if you have to maintain it, I offer
@@ -194,11 +198,15 @@ QWeb.addDirective({
* ```xml
* <t t-widget="child"
* t-key="'somestring'"
* t-props="{flag:state.flag}"
* 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);
@@ -213,7 +221,7 @@ QWeb.addDirective({
* let _2_index = c1.length;
* c1.push(null);
*
* // def3 is the deferred that will contain later either the new widget
* // def3 is the promise that will contain later either the new widget
* // creation, or the props update...
* let def3;
*
@@ -232,31 +240,32 @@ QWeb.addDirective({
* // computation, so it is certainly better to do it only once
* let props4 = { flag: context["state"].flag };
*
* // If we have a widget, currently rendering, but not ready yet, and which was
* // rendered with different props, we do not want to wait for it to be ready,
* // then update it. We simply destroy it, and start anew.
* if (
* w4 &&
* w4.__owl__.renderPromise &&
* !w4.__owl__.isStarted &&
* props4 !== w4.__owl__.renderProps
* ) {
* w4.destroy();
* w4 = false;
* // If we have a widget, currently rendering, but not ready yet, we do not want
* // to wait for it to be ready if we can avoid it
* if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
* // we check if the props are the same. In that case, we can simply reuse
* // the previous rendering and skip all useless work
* if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
* def3 = w4.__owl__.renderPromise;
* } else {
* // if the props are not the same, we destroy the widget and starts anew.
* // this will be faster than waiting for its rendering, then updating it
* w4.destroy();
* w4 = false;
* }
* }
*
* if (!w4) {
* // in this situation, we need to create a new widget. First step is
* // to get a reference to the class, then create an instance with
* // current context as parent, and the props.
* let W4 = context.widgets["child"];
* let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
* if (!W4) {
* throw new Error("Cannot find the definition of widget 'child'");
* }
* w4 = new W4(owner, props4);
*
* let utils = this.utils;
*
* // Whenever we rerender the parent widget, we need to be sure that we
* // are able to find the widget instance. To do that, we register it to
* // the parent cmap (children map). Note that the 'template' key is
@@ -313,7 +322,9 @@ QWeb.addDirective({
* } else {
* // this is the 'update' path of the directive.
* // the call to _updateProps is the actual widget update
* def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
* // Note that we only update the props if we cannot reuse the previous
* // rendering work (in the case it was rendered with the same props)
* def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
* def3 = def3.then(() => {
* // if widget was destroyed in the meantime, we do nothing (so, this
* // means that the parent's element children list will have a null in
@@ -345,19 +356,31 @@ QWeb.addDirective({
ctx.rootContext.shouldDefineOwner = true;
ctx.rootContext.shouldDefineQWeb = true;
ctx.rootContext.shouldDefineUtils = true;
let props = node.getAttribute("t-props");
let keepAlive = node.getAttribute("t-keepalive") ? true : false;
// t-on- events and t-transition
const events: [string, string][] = [];
const events: [string, string[], string, string][] = [];
let transition: string = "";
const attributes = (<Element>node).attributes;
const props: { [key: string]: string } = {};
for (let i = 0; i < attributes.length; i++) {
const name = attributes[i].name;
const value = attributes[i].textContent!;
if (name.startsWith("t-on-")) {
events.push([name.slice(5), attributes[i].textContent!]);
const [eventName, ...mods] = name.slice(5).split(".");
let extraArgs;
let handlerName = value.replace(/\(.*\)/, function(args) {
extraArgs = args.slice(1, -1);
return "";
});
events.push([eventName, mods, handlerName, extraArgs]);
} else if (name === "t-transition") {
transition = attributes[i].textContent!;
transition = value;
} else if (!name.startsWith("t-")) {
if (name !== "class" && name !== "style") {
// this is a prop!
props[name] = ctx.formatExpression(value);
}
}
}
@@ -365,9 +388,11 @@ QWeb.addDirective({
if (key) {
key = ctx.formatExpression(key);
}
if (props) {
props = ctx.formatExpression(props);
}
// computing the props string representing the props object
let propStr = Object.keys(props)
.map(k => k + ":" + props[k])
.join(",");
let dummyID = ctx.generateID();
let defID = ctx.generateID();
let widgetID = ctx.generateID();
@@ -422,7 +447,7 @@ QWeb.addDirective({
tattStyle = attVar;
}
let updateClassCode = "";
if (classAttr || tattClass || styleAttr || tattStyle) {
if (classAttr || tattClass || styleAttr || tattStyle || events.length) {
let classCode = "";
if (classAttr) {
classCode =
@@ -441,20 +466,46 @@ QWeb.addDirective({
}`;
updateClassCode = `let cl=w${widgetID}.el.classList;for (let k in ${attVar}) {if (${attVar}[k]) {cl.add(k)} else {cl.remove(k)}}`;
}
let eventsCode = events
.map(function([eventName, mods, handlerName, extraArgs]) {
let params = extraArgs
? `owner, ${ctx.formatExpression(extraArgs)}`
: "owner";
let handler;
if (mods.length > 0) {
handler = `function (e) {`;
handler += mods
.map(function(mod) {
return T_WIDGET_MODS_CODE[mod];
})
.join("");
handler += `owner['${handlerName}'].call(${params}, e);}`;
} else {
handler = `owner['${handlerName}'].bind(${params})`;
}
return `vn.elm.addEventListener('${eventName}', ${handler});`;
})
.join("");
const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false);
const styleCode = styleExpr ? `vn.elm.style = ${styleExpr}` : "";
createHook = `vnode.data.hook = {create(_, vn){${classCode}${styleCode}}};`;
const styleCode = styleExpr ? `vn.elm.style = ${styleExpr};` : "";
createHook = `vnode.data.hook = {create(_, vn){${classCode}${styleCode}${eventsCode}}};`;
}
ctx.addLine(
`let w${widgetID} = ${templateID} in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[${templateID}]] : false;`
);
ctx.addLine(`let props${widgetID} = ${props || "{}"};`);
ctx.addLine(`let props${widgetID} = {${propStr}};`);
ctx.addIf(
`w${widgetID} && w${widgetID}.__owl__.renderPromise && !w${widgetID}.__owl__.vnode && props${widgetID} !== w${widgetID}.__owl__.renderProps`
`w${widgetID} && w${widgetID}.__owl__.renderPromise && !w${widgetID}.__owl__.vnode`
);
ctx.addIf(
`utils.shallowEqual(props${widgetID}, w${widgetID}.__owl__.renderProps)`
);
ctx.addLine(`def${defID} = w${widgetID}.__owl__.renderPromise;`);
ctx.addElse();
ctx.addLine(`w${widgetID}.destroy();`);
ctx.addLine(`w${widgetID} = false`);
ctx.addLine(`w${widgetID} = false;`);
ctx.closeIf();
ctx.closeIf();
ctx.addIf(`!w${widgetID}`);
@@ -472,9 +523,6 @@ QWeb.addDirective({
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();`);
// 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
@@ -487,7 +535,7 @@ QWeb.addDirective({
ctx.addElse();
// need to update widget
ctx.addLine(
`def${defID} = w${widgetID}._updateProps(props${widgetID}, extra.forceUpdate, extra.patchQueue);`
`def${defID} = def${defID} || w${widgetID}._updateProps(props${widgetID}, extra.forceUpdate, extra.patchQueue);`
);
let keepAliveCode = "";
if (keepAlive) {
+16 -8
View File
@@ -15,9 +15,13 @@ exports[`animations t-transition combined with t-widget 1`] = `
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
w4.destroy();
w4 = false
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false;
}
}
if (!w4) {
let widgetKey4 = \`Child\`;
@@ -31,7 +35,7 @@ exports[`animations t-transition combined with t-widget 1`] = `
};
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 || 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);
@@ -55,9 +59,13 @@ exports[`animations t-transition combined with t-widget and t-if 1`] = `
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
w4.destroy();
w4 = false
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false;
}
}
if (!w4) {
let widgetKey4 = \`Child\`;
@@ -71,7 +79,7 @@ exports[`animations t-transition combined with t-widget and t-if 1`] = `
};
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 || 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);
+371 -63
View File
@@ -16,9 +16,13 @@ exports[`class and style attributes with t-widget dynamic t-att-style is properl
const _5 = context['state'].style;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
w4.destroy();
w4 = false
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false;
}
}
if (!w4) {
let widgetKey4 = \`child\`;
@@ -27,9 +31,9 @@ exports[`class and style attributes with t-widget dynamic t-att-style is properl
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4._prepare();
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.style = _5}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.style = _5;}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};w4.el.style=_5;let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
@@ -53,9 +57,13 @@ exports[`class and style attributes with t-widget t-att-class is properly added/
const _5 = {a: context['state'].a,b: context['state'].b};
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
w4.destroy();
w4 = false
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false;
}
}
if (!w4) {
let widgetKey4 = \`child\`;
@@ -70,7 +78,7 @@ exports[`class and style attributes with t-widget t-att-class is properly added/
}
}}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let cl=w4.el.classList;for (let k in _5) {if (_5[k]) {cl.add(k)} else {cl.remove(k)}}let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
@@ -108,9 +116,13 @@ exports[`composition sub widgets with some state rendered in a loop 1`] = `
let def6;
let w7 = key8 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[key8]] : false;
let props7 = {};
if (w7 && w7.__owl__.renderPromise && !w7.__owl__.vnode && props7 !== w7.__owl__.renderProps) {
w7.destroy();
w7 = false
if (w7 && w7.__owl__.renderPromise && !w7.__owl__.vnode) {
if (utils.shallowEqual(props7, w7.__owl__.renderProps)) {
def6 = w7.__owl__.renderPromise;
} else {
w7.destroy();
w7 = false;
}
}
if (!w7) {
let widgetKey7 = \`ChildWidget\`;
@@ -121,7 +133,7 @@ exports[`composition sub widgets with some state rendered in a loop 1`] = `
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;});
} else {
def6 = w7._updateProps(props7, extra.forceUpdate, extra.patchQueue);
def6 = def6 || w7._updateProps(props7, extra.forceUpdate, extra.patchQueue);
def6 = def6.then(()=>{if (w7.__owl__.isDestroyed) {return};let pvnode=w7.__owl__.pvnode;c1[_5_index]=pvnode;});
}
extra.promises.push(def6);
@@ -145,12 +157,16 @@ exports[`composition t-widget with dynamic value 1`] = `
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
w4.destroy();
w4 = false
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false;
}
}
if (!w4) {
let widgetKey4 = context['state'].widget;
let widgetKey4 = (context['state'].widget);
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
w4 = new W4(owner, props4);
@@ -158,7 +174,7 @@ exports[`composition t-widget with dynamic value 1`] = `
def3 = w4._prepare();
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
@@ -181,9 +197,13 @@ exports[`composition t-widget with dynamic value 2 1`] = `
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
w4.destroy();
w4 = false
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false;
}
}
if (!w4) {
let widgetKey4 = \`Widget\${context['state'].widget}\`;
@@ -194,7 +214,327 @@ exports[`composition t-widget with dynamic value 2 1`] = `
def3 = w4._prepare();
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
return vn1;
}"
`;
exports[`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);
@@ -217,10 +557,14 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
c1.push(null);
let def3;
let w4 = key5 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[key5]] : false;
let props4 = {flag: context['state'].flag};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
w4.destroy();
w4 = false
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\`;
@@ -231,43 +575,7 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
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 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
return vn1;
}"
`;
exports[`random stuff/miscellaneous t-props should not be undefined (snapshotting) 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let QWeb = this.constructor;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
w4.destroy();
w4 = false
}
if (!w4) {
let widgetKey4 = \`child\`;
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4];
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4._prepare();
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
+15 -4
View File
@@ -1,5 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`attributes class and t-attf-class with ternary operation 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var _1 = 'hello ' + (context['value'] ? 'world' : '');
let c2 = [], p2 = {key:2,attrs:{class: _1}};
var vn2 = h('div', p2, c2);
return vn2;
}"
`;
exports[`attributes dynamic attribute falsy variable 1`] = `
"function anonymous(context,extra
) {
@@ -59,7 +70,7 @@ exports[`attributes format expression 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var _1 = context['value'] + 37;
var _1 = (context['value'] + 37);
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
var vn2 = h('div', p2, c2);
return vn2;
@@ -70,7 +81,7 @@ exports[`attributes format expression, other format 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var _1 = context['value'] + 37;
var _1 = (context['value'] + 37);
let c2 = [], p2 = {key:2,attrs:{foo: _1}};
var vn2 = h('div', p2, c2);
return vn2;
@@ -1162,7 +1173,7 @@ exports[`t-on can bind handlers with arguments 1`] = `
}"
`;
exports[`t-on can bind handlers with empty object (with non empty inner string 1`] = `
exports[`t-on can bind handlers with empty object (with non empty inner string) 1`] = `
"function anonymous(context,extra
) {
let owner = context;
@@ -1546,7 +1557,7 @@ exports[`t-ref refs in a loop 1`] = `
let c5 = [], p5 = {key:context['item']};
var vn5 = h('div', p5, c5);
c1.push(vn5);
const ref6 = context['item'];
const ref6 = (context['item']);
p5.hook = {
create: (_, n) => {
context.refs[ref6] = n.elm;
+309 -74
View File
@@ -286,7 +286,7 @@ describe("lifecycle hooks", () => {
env.qweb.addTemplate(
"ParentWidget",
`<div>><t t-widget="child" t-props="{n:state.n}"/></div>`
`<div><t t-widget="child" n="state.n"/></div>`
);
class ParentWidget extends Widget {
widgets = { child: ChildWidget };
@@ -300,7 +300,7 @@ describe("lifecycle hooks", () => {
}
env.qweb.addTemplate(
"ChildWidget",
`<div><t t-widget="childchild" t-props="{n:props.n}"/></div>`
`<div><t t-widget="childchild" n="props.n"/></div>`
);
class ChildWidget extends Widget {
widgets = { childchild: ChildChildWidget };
@@ -453,7 +453,7 @@ describe("lifecycle hooks", () => {
`
<div>
<div t-if="state.flag">
<t t-widget="ChildWidget" t-props="{n: state.n}"/>
<t t-widget="ChildWidget" n="state.n"/>
</div>
</div>`
);
@@ -534,7 +534,7 @@ describe("lifecycle hooks", () => {
let def = makeDeferred();
env.qweb.addTemplate(
"Parent",
'<span><t t-widget="Child" t-props="{n: state.n}"/></span>'
'<span><t t-widget="Child" n="state.n"/></span>'
);
class Parent extends Widget {
state = { n: 1 };
@@ -586,7 +586,7 @@ describe("lifecycle hooks", () => {
env.qweb.addTemplate(
"Parent",
'<div><t t-widget="Child" t-props="{a:state.a}"/></div>'
'<div><t t-widget="Child" a="state.a"/></div>'
);
class Parent extends Widget {
state = { a: 1 };
@@ -629,7 +629,7 @@ describe("lifecycle hooks", () => {
let shouldUpdate = false;
env.qweb.addTemplate(
"Parent",
`<div><t t-widget="Child" t-props="{val:state.val}"/></div>`
`<div><t t-widget="Child" val="state.val"/></div>`
);
class Parent extends Widget {
state = { val: 42 };
@@ -696,7 +696,7 @@ describe("lifecycle hooks", () => {
"ParentWidget",
`
<div>
<t t-widget="child" t-props="{v: state.n}"/>
<t t-widget="child" v="state.n"/>
</div>`
);
class ParentWidget extends Widget {
@@ -743,7 +743,7 @@ describe("lifecycle hooks", () => {
"ParentWidget",
`
<div>
<t t-if="state.flag" t-widget="child" t-props="{v: state.n}" t-keepalive="1"/>
<t t-if="state.flag" t-widget="child" v="state.n" t-keepalive="1"/>
</div>`
);
class ParentWidget extends Widget {
@@ -1153,7 +1153,7 @@ describe("composition", () => {
`
<div>
<t t-foreach="state.numbers" t-as="number">
<t t-widget="ChildWidget" t-key="number" t-props="{n: number}"/>
<t t-widget="ChildWidget" t-key="number" n="number"/>
</t>
</div>`
);
@@ -1286,11 +1286,11 @@ describe("composition", () => {
});
});
describe("props evaluation (with t-props directive)", () => {
describe("props evaluation ", () => {
test("explicit object prop", async () => {
env.qweb.addTemplate(
"Parent",
`<div><t t-widget="child" t-props="{value: state.val}"/></div>`
`<div><t t-widget="child" value="state.val"/></div>`
);
class Parent extends Widget {
widgets = { child: Child };
@@ -1311,37 +1311,13 @@ describe("props evaluation (with t-props directive)", () => {
expect(fixture.innerHTML).toBe("<div><span>42</span></div>");
});
test("object prop value", async () => {
env.qweb.addTemplate(
"Parent",
`<div><t t-widget="child" t-props="state"/></div>`
);
class Parent extends Widget {
widgets = { child: Child };
state = { val: 42 };
}
env.qweb.addTemplate("Child", `<span><t t-esc="state.someval"/></span>`);
class Child extends Widget {
state: { someval: number };
constructor(parent: Parent, props: { val: number }) {
super(parent);
this.state = { someval: props.val };
}
}
const widget = new Parent(env);
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>42</span></div>");
});
test("accept ES6-like syntax for props (with getters)", async () => {
env.qweb.addTemplate("Child", `<span><t t-esc="props.greetings"/></span>`);
class Child extends Widget {}
env.qweb.addTemplate(
"Parent",
`<div><t t-widget="child" t-props="{greetings}"/></div>`
`<div><t t-widget="child" greetings="greetings"/></div>`
);
class Parent extends Widget {
widgets = { child: Child };
@@ -1354,13 +1330,13 @@ describe("props evaluation (with t-props directive)", () => {
expect(fixture.innerHTML).toBe("<div><span>hello aaron</span></div>");
});
test("t-set works with t-props", async () => {
test("t-set works ", async () => {
env.qweb.addTemplate(
"Parent",
`
<div>
<t t-set="val" t-value="42"/>
<t t-widget="child" t-props="{val:val}"/>
<t t-widget="child" val="val"/>
</div>`
);
class Parent extends Widget {
@@ -1474,28 +1450,257 @@ describe("class and style attributes with t-widget", () => {
describe("other directives with t-widget", () => {
test("t-on works as expected", async () => {
let n = 0;
env.qweb.addTemplate(
"ParentWidget",
`<div><t t-widget="child" t-on-customevent="someMethod"/></div>`
);
expect.assertions(4);
env.qweb.addTemplates(`
<templates>
<div t-name="ParentWidget"><t t-widget="child" t-on-custom-event="someMethod"/></div>
</templates>
`);
class ParentWidget extends Widget {
widgets = { child: Child };
someMethod(arg) {
expect(arg).toBe(43);
n++;
n = 0;
someMethod(ev) {
expect(ev.detail).toBe(43);
this.n++;
}
}
class Child extends Widget {}
const widget = new ParentWidget(env);
await widget.mount(fixture);
let child = children(widget)[0];
expect(n).toBe(0);
child.trigger("customevent", 43);
expect(n).toBe(1);
expect(widget.n).toBe(0);
child.trigger("custom-event", 43);
expect(widget.n).toBe(1);
child.destroy();
child.trigger("customevent", 43);
expect(n).toBe(1);
child.trigger("custom-event", 43);
expect(widget.n).toBe(1);
});
test("t-on with handler bound to argument", async () => {
expect.assertions(3);
env.qweb.addTemplates(`
<templates>
<div t-name="ParentWidget"><t t-widget="child" t-on-ev="onEv(3)"/></div>
</templates>
`);
class ParentWidget extends Widget {
widgets = { child: Child };
onEv(n, ev) {
expect(n).toBe(3);
expect(ev.detail).toBe(43);
}
}
class Child extends Widget {}
const widget = new ParentWidget(env);
await widget.mount(fixture);
let child = children(widget)[0];
child.trigger("ev", 43);
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
});
test("t-on with handler bound to object", async () => {
expect.assertions(3);
env.qweb.addTemplates(`
<templates>
<div t-name="ParentWidget"><t t-widget="child" t-on-ev="onEv({val: 3})"/></div>
</templates>
`);
class ParentWidget extends Widget {
widgets = { child: Child };
onEv(o, ev) {
expect(o).toEqual({ val: 3 });
expect(ev.detail).toBe(43);
}
}
class Child extends Widget {}
const widget = new ParentWidget(env);
await widget.mount(fixture);
let child = children(widget)[0];
child.trigger("ev", 43);
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
});
test("t-on with handler bound to empty object", async () => {
expect.assertions(3);
env.qweb.addTemplates(`
<templates>
<div t-name="ParentWidget"><t t-widget="child" t-on-ev="onEv({})"/></div>
</templates>
`);
class ParentWidget extends Widget {
widgets = { child: Child };
onEv(o, ev) {
expect(o).toEqual({});
expect(ev.detail).toBe(43);
}
}
class Child extends Widget {}
const widget = new ParentWidget(env);
await widget.mount(fixture);
let child = children(widget)[0];
child.trigger("ev", 43);
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
});
test("t-on with handler bound to empty object (with non empty inner string)", async () => {
expect.assertions(3);
env.qweb.addTemplates(`
<templates>
<div t-name="ParentWidget"><t t-widget="child" t-on-ev="onEv({ })"/></div>
</templates>
`);
class ParentWidget extends Widget {
widgets = { child: Child };
onEv(o, ev) {
expect(o).toEqual({});
expect(ev.detail).toBe(43);
}
}
class Child extends Widget {}
const widget = new ParentWidget(env);
await widget.mount(fixture);
let child = children(widget)[0];
child.trigger("ev", 43);
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
});
test("t-on with stop and/or prevent modifiers", async () => {
expect.assertions(7);
env.qweb.addTemplates(`
<templates>
<div t-name="ParentWidget">
<t t-widget="child"
t-on-ev-1.stop="onEv1"
t-on-ev-2.prevent="onEv2"
t-on-ev-3.stop.prevent="onEv3"/>
</div>
</templates>
`);
class ParentWidget extends Widget {
widgets = { child: Child };
onEv1(ev) {
expect(ev.defaultPrevented).toBe(false);
expect(ev.cancelBubble).toBe(true);
}
onEv2(ev) {
expect(ev.defaultPrevented).toBe(true);
expect(ev.cancelBubble).toBe(false);
}
onEv3(ev) {
expect(ev.defaultPrevented).toBe(true);
expect(ev.cancelBubble).toBe(true);
}
}
class Child extends Widget {}
const widget = new ParentWidget(env);
await widget.mount(fixture);
const child = children(widget)[0];
child.trigger("ev-1");
child.trigger("ev-2");
child.trigger("ev-3");
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
});
test("t-on with self modifier", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="ParentWidget">
<t t-widget="child" t-on-ev-1="onEv1" t-on-ev-2.self="onEv2"/>
</div>
<div t-name="Child"><t t-widget="child"/></div>
</templates>
`);
const steps: string[] = [];
class ParentWidget extends Widget {
widgets = { child: Child };
onEv1(ev) {
steps.push("onEv1");
}
onEv2(ev) {
steps.push("onEv2");
}
}
class Child extends Widget {
widgets = { child: GrandChild };
}
class GrandChild extends Widget {}
const widget = new ParentWidget(env);
await widget.mount(fixture);
const child = children(widget)[0];
const grandChild = children(child)[0];
child.trigger("ev-1");
child.trigger("ev-2");
expect(steps).toEqual(["onEv1", "onEv2"]);
grandChild.trigger("ev-1");
grandChild.trigger("ev-2");
expect(steps).toEqual(["onEv1", "onEv2", "onEv1"]);
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
});
test("t-on with self and prevent modifiers (order matters)", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="ParentWidget">
<t t-widget="child" t-on-ev.self.prevent="onEv"/>
</div>
<div t-name="Child"><t t-widget="child"/></div>
</templates>
`);
const steps: boolean[] = [];
class ParentWidget extends Widget {
widgets = { child: Child };
onEv() {}
}
class Child extends Widget {
widgets = { child: GrandChild };
}
class GrandChild extends Widget {}
const widget = new ParentWidget(env);
await widget.mount(fixture);
(<HTMLElement>fixture).addEventListener("ev", function(e) {
steps.push(e.defaultPrevented);
});
const child = children(widget)[0];
const grandChild = children(child)[0];
child.trigger("ev");
grandChild.trigger("ev");
expect(steps).toEqual([true, false]);
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
});
test("t-on with prevent and self modifiers (order matters)", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="ParentWidget">
<t t-widget="child" t-on-ev.prevent.self="onEv"/>
</div>
<div t-name="Child"><t t-widget="child"/></div>
</templates>
`);
const steps: boolean[] = [];
class ParentWidget extends Widget {
widgets = { child: Child };
onEv() {}
}
class Child extends Widget {
widgets = { child: GrandChild };
}
class GrandChild extends Widget {}
const widget = new ParentWidget(env);
await widget.mount(fixture);
(<HTMLElement>fixture).addEventListener("ev", function(e) {
steps.push(e.defaultPrevented);
});
const child = children(widget)[0];
const grandChild = children(child)[0];
child.trigger("ev");
grandChild.trigger("ev");
expect(steps).toEqual([true, true]);
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
});
test("t-if works with t-widget", async () => {
@@ -1626,7 +1831,7 @@ describe("random stuff/miscellaneous", () => {
// twice.
env.qweb.addTemplate(
"Parent",
`<div><t t-widget="child" t-props="{flag:state.flag}"/></div>`
`<div><t t-widget="child" flag="state.flag"/></div>`
);
class Parent extends Widget {
widgets = { child: Child };
@@ -1650,7 +1855,7 @@ describe("random stuff/miscellaneous", () => {
test("snapshotting compiled code", async () => {
env.qweb.addTemplate(
"Parent",
`<div><t t-widget="child" t-key="'somestring'" t-props="{flag:state.flag}"/></div>`
`<div><t t-widget="child" t-key="'somestring'" flag="state.flag"/></div>`
);
class Parent extends Widget {
widgets = { child: Child };
@@ -1668,20 +1873,6 @@ describe("random stuff/miscellaneous", () => {
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
});
test("t-props should not be undefined (snapshotting)", async () => {
env.qweb.addTemplate("Parent", `<div><t t-widget="child"/></div>`);
class Parent extends Widget {
widgets = { child: Child };
}
env.qweb.addTemplate("Child", `<span>abc</span>`);
class Child extends Widget {}
const widget = new Parent(env);
await widget.mount(fixture);
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
});
test("component semantics", async () => {
let steps: string[] = [];
let c: C;
@@ -1863,7 +2054,7 @@ describe("async rendering", () => {
let n = 0;
env.qweb.addTemplate(
"W",
`<div><t t-if="state.val > 1"><t t-widget="Child" t-props="{val: state.val}"/></t></div>`
`<div><t t-if="state.val > 1"><t t-widget="Child" val="state.val"/></t></div>`
);
class W extends Widget {
widgets = { Child };
@@ -1964,8 +2155,8 @@ describe("async rendering", () => {
"Parent",
`
<div>
<t t-widget="ChildA" t-props="{val:state.valA}"/>
<t t-if="state.flagB"><t t-widget="ChildB" t-props="{val:state.valB}"/></t>
<t t-widget="ChildA" val="state.valA"/>
<t t-if="state.flagB"><t t-widget="ChildB" val="state.valB"/></t>
</div>`
);
class Parent extends Widget {
@@ -2015,7 +2206,7 @@ describe("async rendering", () => {
<ul>
<t t-foreach="items" t-as="item">
<li t-key="'li_'+item">
<t t-widget="Child" t-props="{ item }"/>
<t t-widget="Child" item="item"/>
</li>
</t>
</ul>
@@ -2059,7 +2250,7 @@ describe("async rendering", () => {
env.qweb.addTemplate(
"Parent",
`
<div><t t-if="state.flag"><t t-widget="Child" t-props="{val: state.val}"/></t></div>`
<div><t t-if="state.flag"><t t-widget="Child" val="state.val"/></t></div>`
);
class Parent extends Widget {
widgets = { Child };
@@ -2084,6 +2275,50 @@ describe("async rendering", () => {
await nextTick();
expect(fixture.innerHTML).toBe("<div></div>");
});
test("reuse widget if possible, in some async situation", async () => {
env.qweb.addTemplates(`
<templates>
<span t-name="ChildA">a<t t-esc="props.val"/></span>
<span t-name="ChildB">b<t t-esc="props.val"/></span>
<span t-name="Parent">
<t t-if="state.flag">
<t t-widget="ChildA" val="state.valA"/>
<t t-widget="ChildB" val="state.valB"/>
</t>
</span>
</templates>
`);
let destroyCount = 0;
class ChildA extends Widget {
destroy() {
destroyCount++;
super.destroy();
}
}
class ChildB extends Widget {
willStart(): any {
return new Promise(function() {});
}
}
class Parent extends Widget {
widgets = { ChildA, ChildB };
state = { valA: 1, valB: 2, flag: false };
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(destroyCount).toBe(0);
parent.state.flag = true;
await nextTick();
expect(destroyCount).toBe(0);
parent.state.valB = 3;
await nextTick();
expect(destroyCount).toBe(0);
});
});
describe("updating environment", () => {
@@ -2209,7 +2444,7 @@ describe("widget and observable state", () => {
expect.assertions(1);
env.qweb.addTemplate(
"Parent",
`<div><t t-widget="Child" t-props="state.obj"/></div>`
`<div><t t-widget="Child" obj="state.obj"/></div>`
);
class Parent extends Widget {
state = { obj: { coffee: 1 } };
@@ -2218,7 +2453,7 @@ describe("widget and observable state", () => {
class Child extends Widget {
constructor(parent, props) {
super(parent, props);
props.coffee = 2;
props.obj.coffee = 2;
}
}
const parent = new Parent(env);
+20
View File
@@ -230,6 +230,26 @@ describe("observer", () => {
expect(obj2.__owl__.rev).toBe(3);
});
test("can set a property more than once", () => {
const observer = new Observer();
const obj: any = {};
observer.observe(obj);
expect(obj.__owl__.rev).toBe(1);
expect(observer.rev).toBe(1);
expect(obj.__owl__.deepRev).toBe(1);
observer.set(obj, "aku", "always finds annoying problems");
expect(observer.rev).toBe(2);
expect(obj.__owl__.rev).toBe(2);
expect(obj.__owl__.deepRev).toBe(2);
observer.set(obj, "aku", "always finds good problems");
expect(observer.rev).toBe(3);
expect(obj.__owl__.rev).toBe(3);
expect(obj.__owl__.deepRev).toBe(3);
});
test("properly handle swapping elements", () => {
const observer = new Observer();
const obj: any = { a: { arr: [] }, b: 1 };
+14 -1
View File
@@ -85,6 +85,10 @@ describe("error handling", () => {
});
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(
"templatename",
`<div t-att-hey="}/^function invalid{{>'"></div>`
@@ -92,6 +96,9 @@ describe("error handling", () => {
expect(() => qweb.render("templatename")).toThrow(
"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", () => {
@@ -522,6 +529,12 @@ describe("attributes", () => {
expect(result).toBe(`<div class="hello world"></div>`);
});
test("class and t-attf-class with ternary operation", () => {
qweb.addTemplate("test", `<div class="hello" t-attf-class="#{value ? 'world' : ''}"/>`);
const result = renderToString(qweb, "test", { value: true });
expect(result).toBe(`<div class="hello world"></div>`);
});
test("t-att-class with object", () => {
qweb.addTemplate(
"test",
@@ -883,7 +896,7 @@ describe("t-on", () => {
(<HTMLElement>node).click();
});
test("can bind handlers with empty object (with non empty inner string", () => {
test("can bind handlers with empty object (with non empty inner string)", () => {
expect.assertions(2);
qweb.addTemplate(
"test",
+7 -7
View File
@@ -414,7 +414,7 @@ describe("connecting a component to store", () => {
`
<div>
<t t-foreach="props.todos" t-as="todo" t-key="todo">
<t t-widget="Todo" t-props="todo"/>
<t t-widget="Todo" msg="todo.msg"/>
</t>
</div>`
);
@@ -557,7 +557,7 @@ describe("connecting a component to store", () => {
"TodoList",
`<div>
<t t-foreach="props.todos" t-as="todo">
<t t-widget="ConnectedTodo" t-props="todo"/>
<t t-widget="ConnectedTodo" id="todo.id"/>
</t>
</div>`
);
@@ -618,7 +618,7 @@ describe("connecting a component to store", () => {
"TodoList",
`<div>
<t t-foreach="props.todos" t-as="todo">
<t t-widget="ConnectedTodo" t-props="todo"/>
<t t-widget="ConnectedTodo" id="todo.id"/>
</t>
</div>`
);
@@ -650,7 +650,7 @@ describe("connecting a component to store", () => {
env.qweb.addTemplate(
"App",
`<div>
<t t-widget="ConnectedBeer" t-props="{id: state.beerId}"/>
<t t-widget="ConnectedBeer" id="state.beerId"/>
</div>`
);
class App extends Component<any, any, any> {
@@ -728,7 +728,7 @@ describe("connecting a component to store", () => {
env.qweb.addTemplate(
"App",
`<div>
<t t-widget="ConnectedBeer" t-props="{id: state.beerId}"/>
<t t-widget="ConnectedBeer" id="state.beerId"/>
</div>`
);
class App extends Component<any, any, any> {
@@ -797,7 +797,7 @@ describe("connecting a component to store", () => {
env.qweb.addTemplate(
"App",
`<div>
<t t-widget="ConnectedBeer" t-props="{id: state.beerId}"/>
<t t-widget="ConnectedBeer" id="state.beerId"/>
</div>`
);
class App extends Component<any, any, any> {
@@ -876,7 +876,7 @@ describe("connecting a component to store", () => {
"Parent",
`
<div>
<t t-widget="Child" t-props="{key: props.current}"/>
<t t-widget="Child" key="props.current"/>
</div>
`
);
+1 -1
View File
@@ -16,5 +16,5 @@
"strictPropertyInitialization": true,
"strictNullChecks": true
},
"include": ["demo/static/**/*.ts", "src/*.ts", "tests/*.ts"]
"include": ["src/*.ts"]
}