Compare commits

...

39 Commits

Author SHA1 Message Date
Géry Debongnie 9f9b6b174e [REL] bump to v0.17.0 2019-07-13 17:06:43 +02:00
Géry Debongnie 545ceefc3d [REF] store: remove connect function, add ConnectedComponent
closes #238
closes #235
2019-07-13 17:04:36 +02:00
Géry Debongnie 591508e769 [IMP] component: allow top level widgets
closes #166
2019-07-12 14:31:34 +02:00
Géry Debongnie d9cbd23cd9 [IMP] component: add catchError hook 2019-07-12 09:52:09 +02:00
Géry Debongnie f4cd111cc8 [IMP] qweb: more information in an error message 2019-07-11 16:22:30 +02:00
Géry Debongnie ea015af741 [FIX] component: in slots, qweb was looking in wrong component
In slots, the actual parent is where the system should look for existing
widgets, not the rendering context.

closes #239
2019-07-11 15:00:05 +02:00
Géry Debongnie bfc7c81c0d [FIX] component: slots should preserve parented relation
closes #234
2019-07-10 11:01:15 +02:00
Géry Debongnie e24ff8f8aa [IMP] qweb: add renderToString method
closes #230
2019-07-09 10:16:27 +02:00
Géry Debongnie 9f1e64d399 [FIX] component: issue with t-slot,t-set,t-value, but no t-foreach
closes #232
2019-07-08 17:04:06 +02:00
Géry Debongnie 8e1aa71436 [FIX] component: scope issue with slots
Slot templates need to be able to access variables from the parent
scope.

closes #228
2019-07-08 16:24:57 +02:00
Géry Debongnie be2fc965b1 [IMP] playground: reduce duplication in responsive sample 2019-07-08 14:16:12 +02:00
Géry Debongnie 7245ccf8f9 [IMP] component: various prop validation improvements
- add optional form for list props: ['optionalField?']
- accept undefined values for optional props
- allow declaring props with only boolean true
- throw error if extra prop is given to component

closes #223
2019-06-28 16:53:14 +02:00
Géry Debongnie 1c8e1af86d [IMP] tools: improve error message display 2019-06-28 14:54:24 +02:00
Mathieu Duckerts-Antoine 60a6cca960 [IMP] qweb: better error messages for bad xml templates
closes #185
2019-06-28 14:53:19 +02:00
Géry Debongnie ad55b42ccb [IMP] playground: improve first example
Previous example did not illustrate props
2019-06-28 14:31:19 +02:00
Géry Debongnie 50c0a4b126 [DOC] improve component documentation 2019-06-28 13:00:38 +02:00
Géry Debongnie 2ca42e7470 [ADD] tools: add v0.16.0 benchmark 2019-06-28 11:51:37 +02:00
Géry Debongnie d381e85d94 [REL] bump to v0.16.0 2019-06-28 11:20:29 +02:00
Géry Debongnie af7520d869 [REF] *: reformat with prettier and printWidth=100
closes #214
2019-06-28 10:33:13 +02:00
Alexandre Kühn 0095bfa61f [IMP] store: positional arguments with dispatch/commit 2019-06-28 10:12:56 +02:00
Géry Debongnie 1b12cf9b91 [FIX] qweb/component: better handling of class attribute
This changes requires using the class module of vdom.

closes #192
2019-06-28 09:49:41 +02:00
Géry Debongnie dbfc7e4acd [FIX] store/connect: fix bugs with parent/child connected
There were big issues when we use parent/child connected widgets.

- children was rendered twice (and mapStoreToProps was called twice)
- if children was supposed to be destroyed, it was rendered once.

We solve them in this commit by waiting for parent widgets to be ready
before updating children.

closes #216
2019-06-28 09:45:43 +02:00
Pierre Paridans e838e879c0 [FIX] qweb: add missing not equals operators
Implement "!=" and "!==" operator support in QWeb.
2019-06-26 16:14:32 +02:00
Géry Debongnie ee63f6bb0f [IMP] playground: log error in console 2019-06-26 15:50:10 +02:00
Géry Debongnie 16bbb8bc9f [FIX] qweb/component: better handling for t-debug
- fix issue with t-debug on a t-set (defining a slot)
- put t-debug and t-log at a very low (high) priority to make sure they
  are executed before other directives

closes #201
2019-06-25 14:03:05 +02:00
Alexandre Kühn 4a5db0c283 [FIX] qweb: t-component + class with extra whitespaces
Crashed due to `DOMTokenList.add(string)` requiring non-empty string.
2019-06-25 13:08:14 +02:00
Géry Debongnie cc50a2e3bb [FIX] playground: fix some issues with tab handling
closes #200
2019-06-25 09:38:00 +02:00
Géry Debongnie 1e2b204fdd [FIX] component: default slot now works with textnodes
closes #209
2019-06-25 09:37:26 +02:00
Géry Debongnie 4a889b7b6b [IMP] qweb: add option to allow duplicate templates 2019-06-24 22:41:02 +02:00
Géry Debongnie 34695883c2 [REF] component: use __ for private methods
closes #202
2019-06-24 13:42:20 +02:00
Géry Debongnie 3d2e2a1873 [FIX] components: ignore slot if no definition is given
closes #203
2019-06-24 13:38:32 +02:00
Géry Debongnie af6aca83a2 [IMP] components: allow multiple roots in slots
closes #199
2019-06-24 13:36:11 +02:00
Géry Debongnie 8c8ffb6a6b [IMP] qweb,component: make QWeb an event bus
closes #197
2019-06-24 09:47:11 +02:00
Géry Debongnie 63a8fcd7e2 [FIX] component: scoping issue with t-on and t-foreach
closes #193
2019-06-21 16:31:24 +02:00
Géry Debongnie e6a5934162 [REF] component: use 'component' instead of 'widget' 2019-06-20 12:31:00 +02:00
Pierre Paridans 5524c2e323 [IMP] store: connected component reuse base component name
Connected component should have a unique name. To achieve this the
connected component currently have an id based generated name (like
`ConnectedComponent1`).

To make it clearer and ease debugging this commit reuse the extended
component's name to include it in the generated name in the form of
`Connected<ComponentName>`.
2019-06-20 12:26:13 +02:00
Géry Debongnie 35c1de26b8 [IMP] component: allow component name in templates
closes #186
2019-06-18 15:02:35 +02:00
Géry Debongnie d1cf6b1b8d [FIX] playground: sample was using old t-foreach syntax 2019-06-17 16:14:49 +02:00
Géry Debongnie bfa2c681cd [ADD] tools: add v0.15.0 benchmark 2019-06-17 16:07:06 +02:00
55 changed files with 11758 additions and 2603 deletions
+3 -3
View File
@@ -32,7 +32,7 @@ find some more information [here](doc/comparison.md).
## Example ## Example
Here is a short example to illustrate interactive widgets: Here is a short example to illustrate interactive components:
```xml ```xml
<templates> <templates>
@@ -64,8 +64,8 @@ string. More interesting examples can be found on the
If you want to use a simple `<script>` tag, the last release can be downloaded here: If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-0.15.0.js](https://github.com/odoo/owl/releases/download/v0.15.0/owl.js) - [owl-0.17.0.js](https://github.com/odoo/owl/releases/download/v0.17.0/owl.js)
- [owl-0.15.0.min.js](https://github.com/odoo/owl/releases/download/v0.15.0/owl.min.js) - [owl-0.17.0.min.js](https://github.com/odoo/owl/releases/download/v0.17.0/owl.min.js)
Some npm scripts are available: Some npm scripts are available:
+1 -1
View File
@@ -87,7 +87,7 @@ For example, a simple fade in/out effect can be done with this:
} }
``` ```
The `t-transition` directive can be combined with `t-widget`. The `t-transition` directive can be applied on a node element or on a component.
Notes: Notes:
+4 -4
View File
@@ -91,17 +91,17 @@ there is a syntax highlighter for jsx here on github!
This is actually a big difference between OWL and React/Vue: components in OWL This is actually a big difference between OWL and React/Vue: components in OWL
are totally asynchronous. They have two asynchronous hooks in their lifecycle: are totally asynchronous. They have two asynchronous hooks in their lifecycle:
- `willStart` (before the widget starts rendering) - `willStart` (before the component starts rendering)
- `willUpdateProps` (before new props are set) - `willUpdateProps` (before new props are set)
Both these methods can be implemented and return a promise. The rendering will Both these methods can be implemented and return a promise. The rendering will
then wait for these promises to be completed before patching the DOM. This is then wait for these promises to be completed before patching the DOM. This is
useful for some use cases: for example, a widget may want to fetch an external useful for some use cases: for example, a component may want to fetch an external
library (a calendar widget may need a specialized calendar rendering library), library (a calendar component may need a specialized calendar rendering library),
in its willStart hook. in its willStart hook.
```javascript ```javascript
class MyCalendarWidget extends owl.Component { class MyCalendarComponent extends owl.Component {
... ...
willStart() { willStart() {
+269 -165
View File
@@ -4,12 +4,13 @@
- [Overview](#overview) - [Overview](#overview)
- [Example](#example) - [Example](#example)
- [Root Widget And Environment](#root-widget-and-environment)
- [Reference](#reference) - [Reference](#reference)
- [Properties](#properties) - [Properties](#properties)
- [Static Properties](#static-properties) - [Static Properties](#static-properties)
- [Methods](#methods) - [Methods](#methods)
- [Lifecycle](#lifecycle) - [Lifecycle](#lifecycle)
- [Root Component](#root-component)
- [Environment](#environment)
- [Composition](#composition) - [Composition](#composition)
- [Event Handling](#event-handling) - [Event Handling](#event-handling)
- [Form Input Bindings](#form-input-bindings) - [Form Input Bindings](#form-input-bindings)
@@ -20,6 +21,7 @@
- [References](#references) - [References](#references)
- [Slots](#slots) - [Slots](#slots)
- [Asynchronous Rendering](#asynchronous-rendering) - [Asynchronous Rendering](#asynchronous-rendering)
- [Error Handling](#error-handling)
## Overview ## Overview
@@ -28,11 +30,11 @@ OWL components are the building blocks for user interface. They are designed to
1. **declarative:** the user interface should be described in term of the state 1. **declarative:** the user interface should be described in term of the state
of the application, not as a sequence of imperative steps. of the application, not as a sequence of imperative steps.
2. **composable:** each widget can seamlessly be created in a parent widget by 2. **composable:** each component can seamlessly be created in a parent component by
a simple directive in its template. a simple tag or directive in its template.
3. **asynchronous rendering:** the framework will transparently wait for each 3. **asynchronous rendering:** the framework will transparently wait for each
subwidgets to be ready before applying the rendering. It uses native promises sub components to be ready before applying the rendering. It uses native promises
under the hood. under the hood.
4. **uses QWeb as a template system:** the templates are described in XML 4. **uses QWeb as a template system:** the templates are described in XML
@@ -41,7 +43,7 @@ OWL components are the building blocks for user interface. They are designed to
OWL components are defined as a subclass of Component. The rendering is OWL components are defined as a subclass of Component. The rendering is
exclusively done by a [QWeb](qweb.md) template (which needs to be preloaded in QWeb). exclusively done by a [QWeb](qweb.md) template (which needs to be preloaded in QWeb).
Rendering a component generates a virtual dom representation Rendering a component generates a virtual dom representation
of the widget, which is then patched to the DOM, in order to apply the changes in an efficient way. of the component, which is then patched to the DOM, in order to apply the changes in an efficient way.
OWL components observe their states, and rerender themselves whenever it is OWL components observe their states, and rerender themselves whenever it is
changed. This is done by an [observer](observer.md). changed. This is done by an [observer](observer.md).
@@ -76,33 +78,9 @@ a state object is defined. It is not mandatory to use the state object, but it
is certainly encouraged. The state object is [observed](observer.md), and any is certainly encouraged. The state object is [observed](observer.md), and any
change to it will cause a rerendering. change to it will cause a rerendering.
## Root Widget And Environment
Most of the time, Owl widget will be created automatically by the `t-widget`
directive in a template. There is however an obvious exception: the root widget
of an Owl application has to be created manually:
```js
class App extends owl.Component { ... }
const qweb = new owl.QWeb(TEMPLATES);
const env = { qweb: qweb };
const app = new App(env);
app.mount(document.body);
```
The root widget needs an environment. In Owl, an environment is an object with
a `qweb` key, which has to be a [QWeb](qweb.md) instance. This qweb instance will
be used to render everything.
The environment will be given to each child, unchanged, in the `env` property.
This can be very useful to share common information/methods. For example, all
rpcs can be made through a `rpc` method in the environment. This makes it very
easy to test a component.
## Reference ## Reference
An Owl component is a small class which represent a widget or some UI element. An Owl component is a small class which represent a component or some UI element.
It exists in the context of an environment (`env`), which is propagated from a It exists in the context of an environment (`env`), which is propagated from a
parent to its children. The environment needs to have a QWeb instance, which parent to its children. The environment needs to have a QWeb instance, which
will be used to render the component template. will be used to render the component template.
@@ -123,7 +101,7 @@ find a template with the component name (or one of its ancestor).
- **`state`** (Object): this is the location of the component's state, if there is - **`state`** (Object): this is the location of the component's state, if there is
any. After the willStart method, the `state` property is observed, and each any. After the willStart method, the `state` property is observed, and each
change will cause the widget to rerender itself. change will cause the component to rerender itself.
- **`props`** (Object): this is an object given (in the constructor) by the parent - **`props`** (Object): this is an object given (in the constructor) by the parent
to configure the component. It can be dynamically changed later by the parent, to configure the component. It can be dynamically changed later by the parent,
@@ -131,7 +109,7 @@ find a template with the component name (or one of its ancestor).
As such, it should not ever be modified by the component!! As such, it should not ever be modified by the component!!
- **`refs`** (Object): the `refs` object contains all references to sub DOM nodes - **`refs`** (Object): the `refs` object contains all references to sub DOM nodes
or sub widgets defined by a `t-ref` directive in the component's template. or sub components defined by a `t-ref` directive in the component's template.
### Static Properties ### Static Properties
@@ -139,13 +117,33 @@ find a template with the component name (or one of its ancestor).
type and shape of the (actual) props given to the component. If Owl mode is type and shape of the (actual) props given to the component. If Owl mode is
`dev`, this will be used to validate the props each time the component is `dev`, this will be used to validate the props each time the component is
created/updated. See [Props Validation](#props-validation) for more information. created/updated. See [Props Validation](#props-validation) for more information.
- **`defaultProps`** (Object, optional): if given, this object define default
```js
class Counter extends owl.Component {
static props = {
initialValue: Number,
optional: true
};
}
```
* **`defaultProps`** (Object, optional): if given, this object define default
values for (top-level) props. Whenever `props` are given to the object, they values for (top-level) props. Whenever `props` are given to the object, they
will be altered to add default value (if missing). Note that it does not will be altered to add default value (if missing). Note that it does not
change the initial object, a new object will be created instead. change the initial object, a new object will be created instead.
```js
class Counter extends owl.Component {
static defaultProps = {
initialValue: 0
};
}
```
### Methods ### Methods
We explain here all the public methods of the `Component` class.
- **`mount(target)`** (async): this is the main way a component's hierarchy is added to the - **`mount(target)`** (async): this is the main way a component's hierarchy is added to the
DOM: the root component is mounted to a target HTMLElement. Obviously, this DOM: the root component is mounted to a target HTMLElement. Obviously, this
is asynchronous, since each children need to be created as well. Most applications is asynchronous, since each children need to be created as well. Most applications
@@ -163,7 +161,7 @@ find a template with the component name (or one of its ancestor).
DOM in the same stack frame. DOM in the same stack frame.
- **`shouldUpdate(nextProps)`**: this method is called each time a component's props - **`shouldUpdate(nextProps)`**: this method is called each time a component's props
are updated. It returns a boolean, which indicates if the widget should are updated. It returns a boolean, which indicates if the component should
ignore a props update. If it returns false, then `willUpdateProps` will not ignore a props update. If it returns false, then `willUpdateProps` will not
be called, and no rendering will occur. Its default implementation is to be called, and no rendering will occur. Its default implementation is to
always return true. This is an optimization, similar to React's `shouldComponentUpdate`. Most of the time, this should not be used, but it always return true. This is an optimization, similar to React's `shouldComponentUpdate`. Most of the time, this should not be used, but it
@@ -180,21 +178,27 @@ find a template with the component name (or one of its ancestor).
called directly (except maybe on the root component), but should be done by the called directly (except maybe on the root component), but should be done by the
framework instead. framework instead.
Obviously, these methods are reserved for Owl, and should not be used by Owl
users, unless they want to override them. Also, Owl reserves all method names
starting with `__`, in order to prevent possible future conflicts with user code
whenever Owl needs to change.
### Lifecycle ### Lifecycle
A solid and robust component system needs useful hooks/methods to help A solid and robust component system needs useful hooks/methods to help
developers write components. Here is a complete description of the lifecycle of developers write components. Here is a complete description of the lifecycle of
a owl component: a owl component:
| Method | Description | | Method | Description |
| ------------------------------------------------ | ----------------------------------------------------- | | ------------------------------------------------ | ------------------------------------------------------------ |
| **[constructor](#constructorparent-props)** | constructor | | **[constructor](#constructorparent-props)** | constructor |
| **[willStart](#willstart)** | async, before first rendering | | **[willStart](#willstart)** | async, before first rendering |
| **[mounted](#mounted)** | just after component is rendered and added to the DOM | | **[mounted](#mounted)** | just after component is rendered and added to the DOM |
| **[willUpdateProps](#willupdatepropsnextprops)** | async, before props update | | **[willUpdateProps](#willupdatepropsnextprops)** | async, before props update |
| **[willPatch](#willpatch)** | just before the DOM is patched | | **[willPatch](#willpatch)** | just before the DOM is patched |
| **[patched](#patchedsnapshot)** | just after the DOM is patched | | **[patched](#patchedsnapshot)** | just after the DOM is patched |
| **[willUnmount](#willunmount)** | just before removing component from DOM | | **[willUnmount](#willunmount)** | just before removing component from DOM |
| **[catchError](#catcherrorerror)** | catch errors (see [error handling section](#error-handling)) |
Notes: Notes:
@@ -239,7 +243,7 @@ perform some action before the initial rendering of a component.
It will be called exactly once before the initial rendering. It is useful It will be called exactly once before the initial rendering. It is useful
in some cases, for example, to load external assets (such as a JS library) in some cases, for example, to load external assets (such as a JS library)
before the widget is rendered. Another use case is to load data from a server. before the component is rendered. Another use case is to load data from a server.
```javascript ```javascript
async willStart() { async willStart() {
@@ -251,7 +255,7 @@ At this point, the component is not yet rendered. Note that a slow `willStart` m
interface. Therefore, some care should be made to make this method as interface. Therefore, some care should be made to make this method as
fast as possible. fast as possible.
The widget rendering will take place after `willStart` is completed. The component rendering will take place after `willStart` is completed.
#### `mounted()` #### `mounted()`
@@ -296,8 +300,8 @@ scrollbar.
Note that modifying the state object is not allowed here. This method is called just Note that modifying the state object is not allowed here. This method is called just
before an actual DOM patch, and is only intended to be used to save some local before an actual DOM patch, and is only intended to be used to save some local
DOM state. Also, it will not be called if the widget is not in the DOM (this can DOM state. Also, it will not be called if the component is not in the DOM (this can
happen with widgets with `t-keepalive`). happen with components with `t-keepalive`).
The return value of this method will be given as the first argument of the The return value of this method will be given as the first argument of the
corresponding `patched` call. corresponding `patched` call.
@@ -309,12 +313,12 @@ likely via a change in its state/props or environment).
This method is not called on the initial render. It is useful to interact This method is not called on the initial render. It is useful to interact
with the DOM (for example, through an external library) whenever the with the DOM (for example, through an external library) whenever the
component was patched. Note that this hook will not be called if the widget is component was patched. Note that this hook will not be called if the compoent is
not in the DOM (this can happen with widgets with `t-keepalive`). not in the DOM (this can happen with components with `t-keepalive`).
The `snapshot` parameter is the result of the previous `willPatch` call. The `snapshot` parameter is the result of the previous `willPatch` call.
Updating the widget state in this hook is possible, but not encouraged. Updating the compoent state in this hook is possible, but not encouraged.
One need to be careful, because updates here will cause rerender, which in One need to be careful, because updates here will cause rerender, which in
turn will cause other calls to patched. So, we need to be particularly turn will cause other calls to patched. So, we need to be particularly
careful at avoiding endless cycles. careful at avoiding endless cycles.
@@ -335,91 +339,143 @@ the DOM. This is a good place to remove some listeners, for example.
This is the opposite method of `mounted`. This is the opposite method of `mounted`.
#### `catchError(error)`
The `catchError` method is useful when we need to intercept and properly react
to (rendering) errors that occur in some sub components. See the section on
[error handling](#error-handling)
### Root Component
Most of the time, an Owl component will be created automatically by a tag (or the `t-component`
directive) in a template. There is however an obvious exception: the root component
of an Owl application has to be created manually:
```js
class App extends owl.Component { ... }
const qweb = new owl.QWeb(TEMPLATES);
const env = { qweb: qweb };
const app = new App(env);
app.mount(document.body);
```
The root component needs an environment.
### Environment
In Owl, an environment is an object with a `qweb` key, which has to be a
[QWeb](qweb.md) instance. This qweb instance will be used to render everything.
The environment is meant to contain (mostly) static global information and
methods for the whole application. For example, settings keys (`mode` to determine
if we are in desktop or mobile mode, or `theme`: dark or light), `rpc` methods,
session information, ...
The environment will be given to each child, unchanged, in the `env` property.
This can be very useful to share common information/methods. For example, all
rpcs can be made through a `rpc` method in the environment. This makes it very
easy to test a component.
Updating the environment is not as simple as changing a component's state: its
content is not observed, so updates will not be reflected immediately in the
user interface. There is however a mechanism to force root widgets to rerender
themselves whenever the environment is modified: one only needs to trigger the
`update` event on the QWeb instance. For example, a responsive environment
could be programmed like this:
```js
function setupResponsivePlugin(env) {
const isMobile = () => window.innerWidth <= 768;
env.isMobile = isMobile();
const updateEnv = owl.utils.debounce(() => {
if (env.isMobile !== isMobile()) {
env.isMobile = !env.isMobile;
env.qweb.trigger("update");
}
}, 15);
window.addEventListener("resize", updateEnv);
}
```
### Composition ### Composition
The example above shows a QWeb template with a `t-on-click` directive. Widget The example above shows a QWeb template with a sub component. In a template,
templates are standard [QWeb](qweb.md) templates, but with an extra directive: components are declared with a tagname corresponding to the class name. It has
`t-widget`. With the `t-widget` directive, widget templates can declare sub to be capitalized.
widgets:
```xml ```xml
<div t-name="ParentWidget"> <div t-name="ParentComponent">
<span>some text</span> <span>some text</span>
<t t-widget="MyWidget" info="13"/> <MyComponent info="13" />
</div> </div>
``` ```
```js ```js
class ParentWidget extends owl.Component { class ParentComponent extends owl.Component {
widgets = { MyWidget: MyWidget}; components = { MyComponent: MyComponent};
... ...
} }
``` ```
In this example, the `ParentWidget`'s template creates a widget `MyWidget` just In this example, the `ParentComponent`'s template creates a component `MyComponent` just
after the span. The `info` key will be added to the subwidget's props. Each after the span. The `info` key will be added to the subcomponent's `props`. Each
props is a string which represents a javascript (QWeb) expression, so it is `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: dynamic. If it is necessary to give a string, this can be done by quoting it:
`someString="'somevalue'"`. See the `someString="'somevalue'"`.
[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 Note that the rendering context for the template is the component itself. This means
that the template can access `state`, `props`, `env`, or any methods defined in the widget. that the template can access `state`, `props`, `env`, or any methods defined in the component.
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 ```xml
<div t-name="ParentWidget"> <div t-name="ParentComponent">
<t t-widget="ChildWidget" count="state.val"/> <ChildComponent count="state.val" />
</div> </div>
``` ```
```js ```js
class ParentWidget { class ParentComponent {
widgets = { ChildWidget }; components = { ChildComponent };
state = { val: 4 }; state = { val: 4 };
} }
``` ```
Whenever the template is rendered, it will automatically create the subwidget Whenever the template is rendered, it will automatically create the subcomponent
`ChildWidget` at the correct place. It needs to find the reference to the `ChildComponent` 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 actual component class in the special `components` key, or the class registered in
QWeb's global registry (see `register` function of QWeb). It first looks inside QWeb's global registry (see `register` function of QWeb). It first looks inside
the local `widgets` key, then fallbacks on the global registry. the local `components` key, then fallbacks on the global registry.
_Props_: In this example, the child widget will receive the object `{count: 4}` in its _Props_: In this example, the child component will receive the object `{count: 4}` in its
constructor. This will be assigned to the `props` variable, which can be accessed 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 on the component (and also, in the template). Whenever the state is updated, then
the subwidget will also be updated automatically. the sub component will also be updated automatically.
Note that there are some restrictions on prop names: `class`, `style` and any Note that there are some restrictions on prop names: `class`, `style` and any
string which starts with `t-` are not allowed. string which starts with `t-` are not allowed.
The `t-widget` directive also accepts dynamic values with string interpolation The `t-component` directive can also be used to accept dynamic values with string interpolation (like the [`t-attf-`](qweb.md#dynamic-attributes) directive):
(like the [`t-attf-`](qweb.md#dynamic-attributes) directive):
```xml ```xml
<div t-name="ParentWidget"> <div t-name="ParentComponent">
<t t-widget="ChildWidget{{id}}"/> <t t-component="ChildComponent{{id}}" />
</div> </div>
``` ```
```js ```js
class ParentWidget { class ParentComponent {
widgets = { ChildWidget1, ChildWidget2 }; components = { ChildComponent1, ChildComponent2 };
state = { id: 1 }; state = { id: 1 };
} }
``` ```
**CSS and style:** there is some specific support to allow the parent to declare **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 additional css classes or style for the sub component: css declared in `class`, `style`, `t-att-class` or `t-att-style` will be added to the
root widget element. root component element.
```xml ```xml
<div t-name="ParentWidget"> <div t-name="ParentComponent">
<t t-widget="MyWidget" class="someClass" style="font-weight:bold;" info="13"/> <MyComponent class="someClass" style="font-weight:bold;" info="13" />
</div> </div>
``` ```
@@ -431,7 +487,7 @@ class that need to be removed. This is why we only support the explicit syntax
with a class object: with a class object:
```xml ```xml
<t t-widget="MyWidget" t-att-class="{a: state.flagA, b: state.flagB}" /> <MyComponent t-att-class="{a: state.flagA, b: state.flagB}" />
``` ```
### Event Handling ### Event Handling
@@ -454,7 +510,7 @@ A _pure_ DOM event is directly triggered by a user interaction (e.g. a `click`).
This will be roughly translated in javascript like this: This will be roughly translated in javascript like this:
```js ```js
button.addEventListener("click", widget.someMethod.bind(widget)); button.addEventListener("click", component.someMethod.bind(component));
``` ```
The suffix (`click` in this example) is simply the name of the actual DOM The suffix (`click` in this example) is simply the name of the actual DOM
@@ -463,11 +519,11 @@ event.
A _business_ DOM event is triggered by a call to `trigger` on a component. A _business_ DOM event is triggered by a call to `trigger` on a component.
```xml ```xml
<t t-widget="MyWidget" t-on-menu-loaded="someMethod"/> <MyComponent t-on-menu-loaded="someMethod" />
``` ```
```js ```js
class MyWidget { class MyComponent {
someWhere() { someWhere() {
const payload = ...; const payload = ...;
this.trigger('menu-loaded', payload); this.trigger('menu-loaded', payload);
@@ -477,12 +533,12 @@ A _business_ DOM event is triggered by a call to `trigger` on a component.
The call to `trigger` generates a [_CustomEvent_](https://developer.mozilla.org/docs/Web/Guide/Events/Creating_and_triggering_events) 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 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 (`this.el`). The event bubbles and is cancelable. The parent component listening
to event `menu-loaded` will receive the payload in its `someMethod` handler to event `menu-loaded` will receive the payload in its `someMethod` handler
(in the `detail` property of the event), whenever the event is triggered. (in the `detail` property of the event), whenever the event is triggered.
```js ```js
class ParentWidget { class ParentComponent {
someMethod(ev) { someMethod(ev) {
const payload = ev.detail; const payload = ev.detail;
... ...
@@ -538,8 +594,8 @@ class Form extends owl.Component {
```xml ```xml
<div> <div>
<input t-on-input="_updateInputValue"/> <input t-on-input="_updateInputValue" />
<span t-esc="state.text"/> <span t-esc="state.text" />
</div> </div>
``` ```
@@ -559,8 +615,8 @@ class Form extends owl.Component {
```xml ```xml
<div> <div>
<input t-model="text"/> <input t-model="text" />
<span t-esc="state.text"/> <span t-esc="state.text" />
</div> </div>
``` ```
@@ -586,7 +642,7 @@ The `t-model` directive works with `<input>`, `<input type="checkbox">`,
<label for="red">Red</label> <label for="red">Red</label>
</span> </span>
<span> <span>
<input type="radio" name="color" id="blue" value="blue" t-model="color"/> <input type="radio" name="color" id="blue" value="blue" t-model="color" />
<label for="blue">Blue</label> <label for="blue">Blue</label>
</span> </span>
</div> </div>
@@ -604,7 +660,7 @@ Like event handling, the `t-model` directive accepts some modifiers:
For example: For example:
```xml ```xml
<input t-model.lazy="someVal"/> <input t-model.lazy="someVal" />
``` ```
These modifiers can be combined. For instance, `t-model.lazy.number` will only These modifiers can be combined. For instance, `t-model.lazy.number` will only
@@ -627,7 +683,7 @@ There are three main use cases:
```xml ```xml
<span t-foreach="todos" t-as="todo" t-key="todo.id"> <span t-foreach="todos" t-as="todo" t-key="todo.id">
<t t-esc="todo.text"/> <t t-esc="todo.text" />
</span> </span>
``` ```
@@ -646,7 +702,7 @@ is inserted into the DOM.
``` ```
```js ```js
class MyWidget extends owl.Component { class MyComponent extends owl.Component {
... ...
focusMe() { focusMe() {
this.refs.someInput.focus(); this.refs.someInput.focus();
@@ -677,27 +733,27 @@ component (with some code like `app.mount(document.body)`).
2. when it is done, template `A` is rendered. 2. when it is done, template `A` is rendered.
- widget `B` is created - component `B` is created
1. `willStart` is called on `B` 1. `willStart` is called on `B`
2. template `B` is rendered 2. template `B` is rendered
- widget `C` is created - component `C` is created
1. `willStart` is called on `C` 1. `willStart` is called on `C`
2. template `C` is rendered 2. template `C` is rendered
- widget `D` is created - component `D` is created
1. `willStart` is called on `D` 1. `willStart` is called on `D`
2. template `D` is rendered 2. template `D` is rendered
- widget `E` is created - component `E` is created
1. `willStart` is called on `E` 1. `willStart` is called on `E`
2. template `E` is rendered 2. template `E` is rendered
3. widget `A` is patched into a detached DOM element. This will create the actual 3. component `A` is patched into a detached DOM element. This will create the actual
widget `A` DOM structure. The patching process will cause recursively the component `A` DOM structure. The patching process will cause recursively the
patching of the `B`, `C`, `D` and `E` DOM trees. (so the actual full DOM tree is created patching of the `B`, `C`, `D` and `E` DOM trees. (so the actual full DOM tree is created
in one pass) in one pass)
4. the widget `A` root element is actually appended to `document.body` 4. the component `A` root element is actually appended to `document.body`
5. The method `mounted` is called recursively on all widgets in the following 5. The method `mounted` is called recursively on all components in the following
order: `B`, `D`, `E`, `C`, `A`. order: `B`, `D`, `E`, `C`, `A`.
**Scenario 2: rerendering a component**. Now, let's assume that the user clicked on some **Scenario 2: rerendering a component**. Now, let's assume that the user clicked on some
@@ -705,7 +761,7 @@ button in `C`, and this results in a state update, which is supposed to:
- update `D`, - update `D`,
- remove `E`, - remove `E`,
- add new widget `F`. - add new component `F`.
So, the component tree should look like this: So, the component tree should look like this:
@@ -722,17 +778,17 @@ Here is what Owl will do:
1. because of a state change, the method `render` is called on `C` 1. because of a state change, the method `render` is called on `C`
2. template `C` is rendered again 2. template `C` is rendered again
- widget `D` is updated: - component `D` is updated:
1. hook `willUpdateProps` is called on `D` (async) 1. hook `willUpdateProps` is called on `D` (async)
2. template `D` is rerendered 2. template `D` is rerendered
- widget `F` is created: - component `F` is created:
1. hook `willStart` is called on `E` (async) 1. hook `willStart` is called on `E` (async)
2. template `F` is rendered 2. template `F` is rendered
3. `willPatch` hooks are called recursively on widgets `C`, `D` (not on `F`, 3. `willPatch` hooks are called recursively on components `C`, `D` (not on `F`,
because it is not mounted yet) because it is not mounted yet)
4. widget `C` is patched, which will cause recursively: 4. component `C` is patched, which will cause recursively:
2. `willUnmount` hook on `E`, then destruction of `E`, 2. `willUnmount` hook on `E`, then destruction of `E`,
3. (initial) patching of `F`, then hook `mounted` is called on `F` 3. (initial) patching of `F`, then hook `mounted` is called on `F`
@@ -756,7 +812,8 @@ of the props. Here is how it works in Owl:
- props are validated whenever a component is created/updated - props are validated whenever a component is created/updated
- props are only validated in `dev` mode (see [tooling page](tooling.md#development-mode)) - props are only validated in `dev` mode (see [tooling page](tooling.md#development-mode))
- if a key does not match the description, an error is thrown - if a key does not match the description, an error is thrown
- it only validates keys defined in (static) `props`. Additional keys in (component) `props` are not validated. - it validates keys defined in (static) `props`. Additional keys given by the
parent will cause an error.
For example: For example:
@@ -784,15 +841,16 @@ class ComponentB extends owl.Component {
- it is an object or a list of strings - it is an object or a list of strings
- a list of strings is a simplified props definition, which only lists the name - a list of strings is a simplified props definition, which only lists the name
of the props. of the props. Also, if the name ends with `?`, it is considered optional.
- all props are by default required, unless they are defined with `optional: true` - all props are by default required, unless they are defined with `optional: true`
(in that case, validation is only done if there is a value) (in that case, validation is only done if there is a value)
- valid types are: `Number, String, Boolean, Object, Array, Date, Function`, and all - valid types are: `Number, String, Boolean, Object, Array, Date, Function`, and all
constructor functions (so, if you have a `Person` class, it can be used as a type) constructor functions (so, if you have a `Person` class, it can be used as a type)
- arrays are homogeneous (all elements have the same type/shape) - arrays are homogeneous (all elements have the same type/shape)
For each key, a `prop` definition is either a constructor, a list of constructors, or an object: For each key, a `prop` definition is either a boolean, a constructor, a list of constructors, or an object:
- a boolean: indicate that the props exists, and is mandatory.
- a constructor: this should describe the type, for example: `id: Number` describe - a constructor: this should describe the type, for example: `id: Number` describe
the props `id` as a number the props `id` as a number
- a list of constructors. In that case, this means that we allow more than one - a list of constructors. In that case, this means that we allow more than one
@@ -810,6 +868,11 @@ Examples:
static props = ['message', 'id', 'date']; static props = ['message', 'id', 'date'];
``` ```
```js
// size is optional
static props = ['message', 'size?'];
```
```js ```js
static props = { static props = {
messageIds: {type: Array, element: Number}, // list of number messageIds: {type: Array, element: Number}, // list of number
@@ -824,7 +887,8 @@ Examples:
url: String url: String
]}, // object, with keys id (number), name (string, optional) and url (string) ]}, // object, with keys id (number), name (string, optional) and url (string)
someFlag: Boolean, // a boolean, mandatory (even if `false`) someFlag: Boolean, // a boolean, mandatory (even if `false`)
someVal: [Boolean, Date] // either a boolean or a date someVal: [Boolean, Date], // either a boolean or a date
otherValue: true, // indicates that it is a prop
}; };
``` ```
@@ -836,42 +900,42 @@ Like the `t-on` directive, it can work either on a DOM node, or on a component:
```xml ```xml
<div> <div>
<div t-ref="someDiv"/> <div t-ref="someDiv"/>
<t t-widget="SubWidget" t-ref="someWidget"/> <SubComponent t-ref="someComponent"/>
</div> </div>
``` ```
In this example, the widget will be able to access the `div` and the component In this example, the component will be able to access the `div` and the component
inside the special `refs` variable: inside the special `refs` variable:
```js ```js
this.refs.someDiv; this.refs.someDiv;
this.refs.someWidget; this.refs.someComponent;
``` ```
This is useful for various usecases: for example, integrating with an external 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 library that needs to render itself inside an actual DOM node. Or for calling
some method on a sub widget. some method on a sub component.
Note: if used on a component, the reference will be set in the `refs` Note: if used on a component, the reference will be set in the `refs`
variable between `willPatch` and `patched`. variable between `willPatch` and `patched`.
The `t-ref` directive also accepts dynamic values with string interpolation The `t-ref` directive also accepts dynamic values with string interpolation
(like the [`t-attf-`](qweb.md#dynamic-attributes) and (like the [`t-attf-`](qweb.md#dynamic-attributes) and
`t-widget` directives). For example, if we have `t-component` directives). For example, if we have
`id` set to 44 in the rendering context, `id` set to 44 in the rendering context,
```xml ```xml
<div t-ref="widget_{{id}}"/> <div t-ref="component_{{id}}"/>
``` ```
```js ```js
this.refs.widget_44; this.refs.component_44;
``` ```
### Slots ### Slots
To make generic components, it is useful to be able for a parent widget to _inject_ To make generic components, it is useful to be able for a parent component to _inject_
some sub template, but still be the owner. For example, a generic dialog widget some sub template, but still be the owner. For example, a generic dialog component
will need to render some content, some footer, but with the parent as the will need to render some content, some footer, but with the parent as the
rendering context. rendering context.
@@ -892,50 +956,31 @@ This is what _slots_ are for.
Slots are defined by the caller, with the `t-set` directive: Slots are defined by the caller, with the `t-set` directive:
```xml ```xml
<div t-name="SomeWidget"> <div t-name="SomeComponent">
<div>some widget</div> <div>some component</div>
<t t-widget="Dialog" title="Some Dialog"> <Dialog title="Some Dialog">
<t t-set="content"> <t t-set="content">
<div>hey</div> <div>hey</div>
</t> </t>
<t t-set="footer"> <t t-set="footer">
<button t-on-click="doSomething">ok</button> <button t-on-click="doSomething">ok</button>
</t> </t>
</t> </Dialog>
</div> </div>
``` ```
In this example, the widget `Dialog` will render the slots `content` and `footer` In this example, the component `Dialog` will render the slots `content` and `footer`
with its parent as rendering context. This means that clicking on the button with its parent as rendering context. This means that clicking on the button
will execute the `doSomething` method on the parent, not on the dialog. will execute the `doSomething` method on the parent, not on the dialog.
Warning! Slots have a technical constraint: the result of the slot rendering Default slot: the first element inside the component which is not a named slot will
should have exactly one root node. So,
```xml
<t t-set="content">
<div>A</div>
<div>B</div>
</t>
```
is not allowed. A workaround could be to wrap the content in a div:
```xml
<div t-set="content">
<div>A</div>
<div>B</div>
</div>
```
Default slot: the first element inside the widget which is not a named slot will
be considered the `default` slot. For example: be considered the `default` slot. For example:
```xml ```xml
<div t-name="Parent"> <div t-name="Parent">
<t t-widget="Child"> <Child>
<span>some content</span> <span>some content</span>
</t> </Child>
</div> </div>
<div t-name="Child"> <div t-name="Child">
@@ -952,23 +997,23 @@ components.
There are two different common problems with Owl asynchronous rendering model: There are two different common problems with Owl asynchronous rendering model:
- any widget can delay the rendering (initial and subsequent) of the whole - any component can delay the rendering (initial and subsequent) of the whole
application application
- for a given widget, there are two independant situations that will trigger an - for a given component, there are two independant situations that will trigger an
asynchronous rerendering: a change in the state, or a change in the props. asynchronous rerendering: a change in the state, or a change in the props.
These changes may be done at different times, and Owl has no way of knowing These changes may be done at different times, and Owl has no way of knowing
how to reconcile the resulting renderings. how to reconcile the resulting renderings.
Here are a few tips on how to work with asynchronous widgets: Here are a few tips on how to work with asynchronous components:
1. Minimize the use of asynchronous widgets! 1. Minimize the use of asynchronous components!
2. Maybe move the asynchronous logic in a store, which then triggers (mostly) 2. Maybe move the asynchronous logic in a store, which then triggers (mostly)
synchronous renderings synchronous renderings
3. Lazy loading external libraries is a good use case for async rendering. This 3. Lazy loading external libraries is a good use case for async rendering. This
is mostly fine, because we can assume that it will only takes a fraction of a is mostly fine, because we can assume that it will only takes a fraction of a
second, and only once (see `owl.utils.loadJS`) second, and only once (see `owl.utils.loadJS`)
4. For all the other cases, the `t-asyncroot` directive (to use alongside 4. For all the other cases, the `t-asyncroot` directive (to use alongside
`t-widget`) is there to help you. When this directive is met, a new rendering `t-component`) is there to help you. When this directive is met, a new rendering
sub tree is created, such that the rendering of that component (and its sub tree is created, such that the rendering of that component (and its
children) is not tied to the rendering of the rest of the interface. It can children) is not tied to the rendering of the rest of the interface. It can
be used on an asynchronous component, to prevent it from delaying the be used on an asynchronous component, to prevent it from delaying the
@@ -978,8 +1023,67 @@ Here are a few tips on how to work with asynchronous widgets:
(triggered by state or props changes). (triggered by state or props changes).
```xml ```xml
<div t-name="ParentWidget"> <div t-name="ParentComponent">
<t t-widget="SyncChild"/> <SyncChild />
<t t-widget="AsyncChild" t-asyncroot="1"/> <AsyncChild t-asyncroot="1"/>
</div> </div>
``` ```
### Error Handling
By default, whenever an error occurs in the rendering of an Owl application, we
destroy the whole application. Otherwise, we cannot offer any guarantee on the
state of the resulting component tree. It might be hopelessly corrupted, but
without any user-visible state.
Clearly, it sometimes is a little bit extreme to destroy the application. This
is why we have a builtin mechanism to handle rendering errors (and errors coming
from lifecycle hooks): the `catchError` hook.
Whenever the `catchError` lifecycle hook is implemented, all errors coming from
sub components rendering and/or lifecycle method calls will be caught and given
to the `catchError` method. This allow us to properly handle the error, and to
not break the application.
For example, here is how we could implement an `ErrorBoundary` component:
```xml
<div t-name="ErrorBoundary">
<t t-if="state.error">
Error handled
</t>
<t t-else="1">
<t t-slot="default" />
</t>
</div>
```
```js
class ErrorBoundary extends Widget {
state = { error: false };
catchError() {
this.state.error = true;
}
}
```
Using the `ErrorBoundary` is then extremely simple:
```xml
<ErrorBoundary><SomeOtherComponent/></ErrorBoundary>
```
Note that we need to be careful here: the fallback UI should not throw any
error, otherwise we risk going into an infinite loop.
Also, it may be useful to know that whenever an error is caught, it is then
broadcasted to the application by an event on the `qweb` instance. It may be
useful, for example, to log the error somewhere.
```js
env.qweb.on("error", null, function(error) {
// do something
// react to the error
});
```
+4 -4
View File
@@ -58,16 +58,16 @@ To build an application (or a sub-part of an application), we need two things:
need. In practice, it could context some user session information, some need. In practice, it could context some user session information, some
configuration keys (for example, isMobile = true/false if we are in mobile mode). configuration keys (for example, isMobile = true/false if we are in mobile mode).
- a description of the user interface: there should be a root widget, which can - a description of the user interface: there should be a root component, which can
have sub widgets have sub components
Here are a few steps that we may take to get started: Here are a few steps that we may take to get started:
- get the templates - get the templates
- create a qweb engine, with the templates - create a qweb engine, with the templates
- create an environment - create an environment
- create an instance of the root widget - create an instance of the root component
- mount the root widget to a DOM element - mount the root component to a DOM element
Let us now add the javascript to make it work, in `app.js`: Let us now add the javascript to make it work, in `app.js`:
+32 -18
View File
@@ -44,7 +44,7 @@ with a few interesting points:
- it compiles templates into functions that output a virtual DOM instead of a - it compiles templates into functions that output a virtual DOM instead of a
string. This is necessary for the component system. string. This is necessary for the component system.
- it has a few extra directives: `t-widget`, `t-on`, ... - it has a few extra directives: `t-component`, `t-on`, ...
## Directives ## Directives
@@ -65,16 +65,16 @@ We present here a list of all standard QWeb directives:
The component system in Owl requires additional directives, to express various The component system in Owl requires additional directives, to express various
needs. Here is a list of all Owl specific directives: needs. Here is a list of all Owl specific directives:
| Name | Description | | Name | Description |
| -------------------------------------------- | ----------------------------------------------------------------------------------- | | ------------------------------------------- | ----------------------------------------------------------------------------------- |
| `t-widget`, `t-keepalive`, `t-asyncroot` | [Defining a sub component](component.md#composition) | | `t-component`, `t-keepalive`, `t-asyncroot` | [Defining a sub component](component.md#composition) |
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#references) | | `t-ref` | [Setting a reference to a dom node or a sub component](component.md#references) |
| `t-key` | [Defining a key (to help virtual dom reconciliation)](component.md#t-key-directive) | | `t-key` | [Defining a key (to help virtual dom reconciliation)](component.md#t-key-directive) |
| `t-on-*` | [Event handling](component.md#event-handling) | | `t-on-*` | [Event handling](component.md#event-handling) |
| `t-transition` | [Defining an animation](animations.md#css-transitions) | | `t-transition` | [Defining an animation](animations.md#css-transitions) |
| `t-mounted` | [Callback when a node or component is mounted](component.md#t-mounted-directive) | | `t-mounted` | [Callback when a node or component is mounted](component.md#t-mounted-directive) |
| `t-slot` | [Rendering a slot](component.md#slots) | | `t-slot` | [Rendering a slot](component.md#slots) |
| `t-model` | [Form input bindings](component.md#form-input-bindings) | | `t-model` | [Form input bindings](component.md#form-input-bindings) |
## QWeb Engine ## QWeb Engine
@@ -95,12 +95,14 @@ It's API is quite simple:
const qweb = new owl.QWeb(TEMPLATES); const qweb = new owl.QWeb(TEMPLATES);
``` ```
- **`addTemplate(name, xmlStr)`**: add a specific template. - **`addTemplate(name, xmlStr, allowDuplicate)`**: add a specific template.
```js ```js
qweb.addTemplate("mytemplate", "<div>hello</div>"); qweb.addTemplate("mytemplate", "<div>hello</div>");
``` ```
If the optional `allowDuplicate` is set to `true`, then `QWeb` will simply return whenever a template is added for a second time. Otherwise, `QWeb` will crash.
- **`addTemplates(xmlStr)`**: add a list of templates (identified by `t-name` - **`addTemplates(xmlStr)`**: add a list of templates (identified by `t-name`
attribute). attribute).
@@ -108,7 +110,7 @@ It's API is quite simple:
const TEMPLATES = ` const TEMPLATES = `
<templates> <templates>
<div t-name="App" class="main">main</div> <div t-name="App" class="main">main</div>
<div t-name="OtherWidget">other widget</div> <div t-name="OtherComponent">other component</div>
</templates>`; </templates>`;
qweb.addTemplates(TEMPLATES); qweb.addTemplates(TEMPLATES);
``` ```
@@ -117,12 +119,19 @@ It's API is quite simple:
which is a virtual representation of the DOM (see [vdom doc](vdom.md)). which is a virtual representation of the DOM (see [vdom doc](vdom.md)).
```js ```js
const vnode = qweb.render("App", widget); const vnode = qweb.render("App", component);
```
- **`renderToString(name, context)`**: renders a template, but returns an html
string.
```js
const str = qweb.renderToString("someTemplate", somecontext);
``` ```
- **`register(name, Component)`**: static function to register an OWL Component - **`register(name, Component)`**: static function to register an OWL Component
to QWeb's global registry. Globally registered Components can be used in to QWeb's global registry. Globally registered Components can be used in
templates (see the `t-widget` directive). This is useful for commonly used templates (see the `t-component` directive). This is useful for commonly used
components accross the application. components accross the application.
```js ```js
@@ -131,10 +140,15 @@ It's API is quite simple:
... ...
class ParentWidget extends owl.Component { ... } class ParentComponent extends owl.Component { ... }
qweb.addTemplate("ParentWidget", "<div><t t-widget='Dialog'/></div>"); qweb.addTemplate("ParentComponent", "<div><Dialog/></div>");
``` ```
In some way, a `QWeb` instance is the core of an Owl application. It is the only
mandatory element of an [environment](component.md#environment). As such, it
has an extra responsability: it can act as an event bus for internal communication
between Owl classes. This is the reason why `QWeb` actually extends [EventBus](event_bus.md).
## Reference ## Reference
We define in this section the specification of how `QWeb` templates should be We define in this section the specification of how `QWeb` templates should be
@@ -179,7 +193,7 @@ root nodes.
QWeb expressions are strings that will be processed at compile time. Each variable in QWeb expressions are strings that will be processed at compile time. Each variable in
the javascript expression will be replaced by a lookup in the context (so, the the javascript expression will be replaced by a lookup in the context (so, the
widget). For example, `a + b.c(d)` will be converted into: component). For example, `a + b.c(d)` will be converted into:
```js ```js
context["a"] + context["b"].c(context["d"]); context["a"] + context["b"].c(context["d"]);
+70 -28
View File
@@ -10,6 +10,8 @@
- [Actions](#actions) - [Actions](#actions)
- [Getters](#getters) - [Getters](#getters)
- [Connecting a Component](#connecting-a-component) - [Connecting a Component](#connecting-a-component)
- [Semantics](#semantics)
- [Good Practices](#good-practices)
## Overview ## Overview
@@ -84,6 +86,20 @@ object, or modifying an array with the `arr[i] = newValue` syntax). See the
Mutations are the only way to modify the state. Changing the state outside a Mutations are the only way to modify the state. Changing the state outside a
mutation is not allowed (and should throw an error). Mutations are synchronous. mutation is not allowed (and should throw an error). Mutations are synchronous.
```js
const mutations = {
setLoginState({ state }, loginState) {
state.loginState = loginState;
}
};
```
Mutations are called with the `commit` method on the store, and can receive an arbitrary number of arguments.
```js
store.commit("setLoginState", "error");
```
### Actions ### Actions
Actions are used to coordinate state changes. It is also useful whenever some Actions are used to coordinate state changes. It is also useful whenever some
@@ -92,10 +108,10 @@ in an action.
```js ```js
const actions = { const actions = {
async login({ commit }) { async login({ commit }, info) {
commit("setLoginState", "pending"); commit("setLoginState", "pending");
try { try {
const loginInfo = await doSomeRPC("/login/", "someinfo"); const loginInfo = await doSomeRPC("/login/", info);
commit("setLoginState", loginInfo); commit("setLoginState", loginInfo);
} catch { } catch {
commit("setLoginState", "error"); commit("setLoginState", "error");
@@ -104,6 +120,13 @@ const actions = {
}; };
``` ```
Actions are called with the `dispatch` method on the store, and can receive an
arbitrary number of arguments.
```js
store.dispatch("login", someInfo);
```
### Getters ### Getters
Usually, data contained in the store will be stored in a normalized way. For Usually, data contained in the store will be stored in a normalized way. For
@@ -142,17 +165,16 @@ const getters = {
const post = store.getters.getPost(id); const post = store.getters.getPost(id);
``` ```
Getters take *at most* one argument. Getters take _at most_ one argument.
Note that getters are cached if they don't take any argument, or their argument Note that getters are cached if they don't take any argument, or their argument
is a string or a number. is a string or a number.
### Connecting a Component ### Connecting a Component
By default, an Owl `Component` is not connected to any store. The `connect` At some point, we need a way to access the state in the store from a component.
function is there to create sub Components that are connected versions of By default, an Owl `Component` is not connected to any store. To do that, we
Components. need to create a component inheriting from `OwlComponent`:
```javascript ```javascript
const actions = { const actions = {
@@ -170,19 +192,18 @@ const state = {
}; };
const store = new owl.Store({ state, actions, mutations }); const store = new owl.Store({ state, actions, mutations });
class Counter extends owl.Component { class Counter extends owl.ConnectedComponent {
static mapStoreToProps(state) {
return {
value: state.counter
};
}
increment() { increment() {
this.env.store.dispatch("increment"); this.env.store.dispatch("increment");
} }
} }
function mapStoreToProps(state) {
return {
value: state.counter
};
}
const ConnectedCounter = owl.connect(Counter, mapStoreToProps);
const counter = new ConnectedCounter({ store, qweb }); const counter = new Counter({ store, qweb });
``` ```
```xml ```xml
@@ -191,19 +212,40 @@ const counter = new ConnectedCounter({ store, qweb });
</button> </button>
``` ```
The arguments of `connect` are: The `ConnectedComponent` class can be configured with the following fields:
- `Counter`: an owl `Component` to connect
- `mapStoreToProps`: a function that extracts the `props` of the Component - `mapStoreToProps`: a function that extracts the `props` of the Component
from the `state` of the `Store` and returns them as a dict from the `state` of the `Store` and returns them as a dict.
- `options`: dictionary of optional parameters that may contain - `getStore`: a function that takes the `env` in arguments and returns an
- `getStore`: a function that takes the `env` in arguments and returns an instance of `Store` to connect to (if not given, connects to `env.store`)
instance of `Store` to connect to (if not given, connects to `env.store`) - `hashFunction`: the function to use to detect changes in the state (if not
- `hashFunction`: the function to use to detect changes in the state (if not given, generates a function that uses revision numbers, incremented at
given, generates a function that uses revision numbers, incremented at each state change)
each state change) - `deep` (boolean): [only useful if no hashFunction is given] if `false`, only watch
- `deep`: [only useful if no hashFunction is given] if false, only watch for top level state changes (`true` by default)
for top level state changes (true by default)
The `connect` function returns a sub class of the given `Component` which is ### Semantics
connected to the `store`.
The `Store` and the `ConnectedComponent` try to be smart and to optimize as much
as possible the rendering and update process. What is important to know is:
- components are always updated in the order of their creation (so, parent
before children)
- they are updated only if they are in the DOM
- if a parent is asynchronous, the system will wait for it to complete its
update before updating other components.
- in general, updates are not coordinated. This is not a problem for synchronous
components, but if there are many asynchronous components, this could lead to
a situation where some part of the UI is updated and other parts of the UI is
not updated.
### Good Practices
- avoid asynchronous components as much as possible. Asynchronous components
lead to situations where parts of the UI is not updated immediately.
- do not be afraid to connect many components, parent or children if needed. For
example, a `MessageList` component could get a list of ids in its `mapStoreToProps` and a `Message` component could get the data of its own
message
- since the `mapStoreToProps` function is called for each connected component,
for each state update, it is important to make sure that these functions are
as fast as possible.
+3
View File
@@ -37,6 +37,9 @@ owl.__info__.mode = "dev";
Note that templates compiled with the `prod` settings will not be recompiled. Note that templates compiled with the `prod` settings will not be recompiled.
So, changing this setting is best done at startup. So, changing this setting is best done at startup.
An important job done by the `dev` mode is to validate props for each component
creation and update. Also, extra props will cause an error.
## Playground ## Playground
The playground is an important application designed to help learning and The playground is an important application designed to help learning and
+4 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "owl", "name": "owl",
"version": "0.15.0", "version": "0.17.0",
"description": "Odoo Web Library (OWL)", "description": "Odoo Web Library (OWL)",
"main": "src/index.ts", "main": "src/index.ts",
"scripts": { "scripts": {
@@ -60,5 +60,8 @@
"json", "json",
"node" "node"
] ]
},
"prettier": {
"printWidth": 100
} }
} }
+158 -169
View File
@@ -1,5 +1,5 @@
import { Observer } from "./observer"; import { Observer } from "./observer";
import { QWeb, CompiledTemplate } from "./qweb_core"; import { QWeb, CompiledTemplate, UTILS } from "./qweb_core";
import { h, patch, VNode } from "./vdom"; import { h, patch, VNode } from "./vdom";
/** /**
@@ -42,8 +42,8 @@ export interface Meta<T extends Env, Props> {
isDestroyed: boolean; isDestroyed: boolean;
parent: Component<T, any, any> | null; parent: Component<T, any, any> | null;
children: { [key: number]: Component<T, any, any> }; children: { [key: number]: Component<T, any, any> };
// children mapping: from templateID to widgetID // children mapping: from templateID to componentID
// should it be a map number => Widget? // should it be a map number => Component?
cmap: { [key: number]: number }; cmap: { [key: number]: number };
renderId: number; renderId: number;
@@ -53,6 +53,7 @@ export interface Meta<T extends Env, Props> {
observer?: Observer; observer?: Observer;
render?: CompiledTemplate; render?: CompiledTemplate;
mountedHandlers: { [key: number]: Function }; mountedHandlers: { [key: number]: Function };
classObj?: { [key: string]: boolean };
} }
// If a component does not define explicitely a template // If a component does not define explicitely a template
@@ -62,7 +63,7 @@ export interface Meta<T extends Env, Props> {
const TEMPLATE_MAP: { [key: number]: { [name: string]: string } } = {}; const TEMPLATE_MAP: { [key: number]: { [name: string]: string } } = {};
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Widget // Component
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
let nextId = 1; let nextId = 1;
@@ -71,8 +72,8 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
template?: string; template?: string;
/** /**
* The `el` is the root element of the widget. Note that it could be null: * The `el` is the root element of the component. Note that it could be null:
* this is the case if the widget is not mounted yet, or is destroyed. * this is the case if the component is not mounted yet, or is destroyed.
*/ */
get el(): HTMLElement | null { get el(): HTMLElement | null {
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null; return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
@@ -97,34 +98,31 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
/** /**
* Creates an instance of Component. * Creates an instance of Component.
* *
* The root widget of a component tree needs an environment: * The root component of a component tree needs an environment:
* *
* ```javascript * ```javascript
* const root = new RootWidget(env, props); * const root = new RootComponent(env, props);
* ``` * ```
* *
* Every other widget simply needs a reference to its parent: * Every other component simply needs a reference to its parent:
* *
* ```javascript * ```javascript
* const child = new SomeWidget(parent, props); * const child = new SomeComponent(parent, props);
* ``` * ```
* *
* Note that most of the time, only the root widget needs to be created by * Note that most of the time, only the root component needs to be created by
* hand. Other widgets should be created automatically by the framework (with * hand. Other components should be created automatically by the framework (with
* the t-widget directive in a template) * the t-component directive in a template)
*/ */
constructor(parent: Component<T, any, any> | T, props?: Props) { constructor(parent: Component<T, any, any> | T, props?: Props) {
const defaultProps = (<any>this.constructor).defaultProps; const defaultProps = (<any>this.constructor).defaultProps;
if (defaultProps) { if (defaultProps) {
props = this._applyDefaultProps(props, defaultProps); props = this.__applyDefaultProps(props, defaultProps);
}
if (QWeb.dev) {
this._validateProps(props || {});
} }
// is this a good idea? // is this a good idea?
// Pro: if props is empty, we can create easily a widget // Pro: if props is empty, we can create easily a component
// Con: this is not really safe // Con: this is not really safe
// Pro: but creating widget (by a template) is always unsafe anyway // Pro: but creating component (by a template) is always unsafe anyway
this.props = <Props>props || <Props>{}; this.props = <Props>props || <Props>{};
let id: number = nextId++; let id: number = nextId++;
let p: Component<T, any, any> | null = null; let p: Component<T, any, any> | null = null;
@@ -134,6 +132,24 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
parent.__owl__.children[id] = this; parent.__owl__.children[id] = this;
} else { } else {
this.env = parent; this.env = parent;
if (QWeb.dev) {
// we only validate props for root widgets here. "Regular" widget
// props are validated by the t-component directive
UTILS.validateProps(this.constructor, this.props);
}
this.env.qweb.on("update", this, () => {
if (this.__owl__.isMounted) {
this.render(true);
}
if (this.__owl__.isDestroyed) {
// this is unlikely to happen, but if a root widget is destroyed,
// we want to remove our subscription. The usual way to do that
// would be to perform some check in the destroy method, but since
// it is very performance sensitive, and since this is a rare event,
// we simply do it lazily
this.env.qweb.off("update", this);
}
});
} }
this.__owl__ = { this.__owl__ = {
id: id, id: id,
@@ -157,7 +173,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
* *
* It will be called exactly once before the initial rendering. It is useful * It will be called exactly once before the initial rendering. It is useful
* in some cases, for example, to load external assets (such as a JS library) * in some cases, for example, to load external assets (such as a JS library)
* before the widget is rendered. * before the component is rendered.
* *
* Note that a slow willStart method will slow down the rendering of the user * Note that a slow willStart method will slow down the rendering of the user
* interface. Therefore, some effort should be made to make this method as * interface. Therefore, some effort should be made to make this method as
@@ -207,7 +223,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
* with the DOM (for example, through an external library) whenever the * with the DOM (for example, through an external library) whenever the
* component was updated. * component was updated.
* *
* Updating the widget state in this hook is possible, but not encouraged. * Updating the component state in this hook is possible, but not encouraged.
* One need to be careful, because updates here will cause rerender, which in * One need to be careful, because updates here will cause rerender, which in
* turn will cause other calls to updated. So, we need to be particularly * turn will cause other calls to updated. So, we need to be particularly
* careful at avoiding endless cycles. * careful at avoiding endless cycles.
@@ -227,6 +243,12 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
*/ */
willUnmount() {} willUnmount() {}
/**
* catchError is a method called whenever some error happens in the rendering or
* lifecycle hooks of a child.
*/
catchError(error: Error): void {}
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
// Public // Public
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
@@ -238,27 +260,27 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
* created declaratively in templates are managed by the Owl system. * created declaratively in templates are managed by the Owl system.
*/ */
async mount(target: HTMLElement): Promise<void> { async mount(target: HTMLElement): Promise<void> {
const vnode = await this._prepare(); const vnode = await this.__prepare();
if (this.__owl__.isDestroyed) { if (this.__owl__.isDestroyed) {
// component was destroyed before we get here... // component was destroyed before we get here...
return; return;
} }
this._patch(vnode); this.__patch(vnode);
target.appendChild(this.el!); target.appendChild(this.el!);
if (document.body.contains(target)) { if (document.body.contains(target)) {
this._callMounted(); this.__callMounted();
} }
} }
unmount() { unmount() {
if (this.__owl__.isMounted) { if (this.__owl__.isMounted) {
this._callWillUnmount(); this.__callWillUnmount();
this.el!.remove(); this.el!.remove();
} }
} }
async render(force: boolean = false, patchQueue?: any[]): Promise<void> { async render(force: boolean = false, patchQueue?: any[], scope?: any, vars?: any): Promise<void> {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
if (!__owl__.isMounted) { if (!__owl__.isMounted) {
return; return;
@@ -267,14 +289,14 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
if (shouldPatch) { if (shouldPatch) {
patchQueue = []; patchQueue = [];
} }
const renderVDom = this._render(force, patchQueue); const renderVDom = this.__render(force, patchQueue, scope, vars);
const renderId = __owl__.renderId; const renderId = __owl__.renderId;
await renderVDom; await renderVDom;
if (shouldPatch && __owl__.isMounted && renderId === __owl__.renderId) { if (shouldPatch && __owl__.isMounted && renderId === __owl__.renderId) {
// we only update the vnode and the actual DOM if no other rendering // we only update the vnode and the actual DOM if no other rendering
// occurred between now and when the render method was initially called. // occurred between now and when the render method was initially called.
this._applyPatchQueue(<any[]>patchQueue); this.__applyPatchQueue(<any[]>patchQueue);
} }
} }
@@ -284,14 +306,14 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
* - call the willUnmount hooks if necessary * - call the willUnmount hooks if necessary
* - remove the dom node from the dom * - remove the dom node from the dom
* *
* This should only be called manually if you created the widget. Most widgets * This should only be called manually if you created the component. Most
* will be automatically destroyed. * components will be automatically destroyed.
*/ */
destroy() { destroy() {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
if (!__owl__.isDestroyed) { if (!__owl__.isDestroyed) {
const el = this.el; const el = this.el;
this._destroy(__owl__.parent); this.__destroy(__owl__.parent);
if (el) { if (el) {
el.remove(); el.remove();
} }
@@ -308,8 +330,8 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
} }
/** /**
* This method is the correct way to update the environment of a widget. Doing * This method is the correct way to update the environment of a component. Doing
* this will cause a full rerender of the widget and its children, so this is * this will cause a full rerender of the component and its children, so this is
* an operation that should not be done frequently. * an operation that should not be done frequently.
* *
* A good usecase for updating the environment would be to update some mostly * A good usecase for updating the environment would be to update some mostly
@@ -348,7 +370,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
// Private // Private
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
_destroy(parent: Component<any, any, any> | null) { __destroy(parent: Component<any, any, any> | null) {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
const isMounted = __owl__.isMounted; const isMounted = __owl__.isMounted;
if (isMounted) { if (isMounted) {
@@ -357,7 +379,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
} }
const children = __owl__.children; const children = __owl__.children;
for (let key in children) { for (let key in children) {
children[key]._destroy(this); children[key].__destroy(this);
} }
if (parent) { if (parent) {
let id = __owl__.id; let id = __owl__.id;
@@ -368,13 +390,13 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
delete __owl__.vnode; delete __owl__.vnode;
} }
_callMounted() { __callMounted() {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
const children = __owl__.children; const children = __owl__.children;
for (let id in children) { for (let id in children) {
const comp = children[id]; const comp = children[id];
if (!comp.__owl__.isMounted && this.el!.contains(comp.el)) { if (!comp.__owl__.isMounted && this.el!.contains(comp.el)) {
comp._callMounted(); comp.__callMounted();
} }
} }
__owl__.isMounted = true; __owl__.isMounted = true;
@@ -382,10 +404,14 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
for (let key in handlers) { for (let key in handlers) {
handlers[key](); handlers[key]();
} }
this.mounted(); try {
this.mounted();
} catch (e) {
errorHandler(e, this);
}
} }
_callWillUnmount() { __callWillUnmount() {
this.willUnmount(); this.willUnmount();
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
__owl__.isMounted = false; __owl__.isMounted = false;
@@ -393,46 +419,54 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
for (let id in children) { for (let id in children) {
const comp = children[id]; const comp = children[id];
if (comp.__owl__.isMounted) { if (comp.__owl__.isMounted) {
comp._callWillUnmount(); comp.__callWillUnmount();
} }
} }
} }
async _updateProps( async __updateProps(
nextProps: Props, nextProps: Props,
forceUpdate: boolean = false, forceUpdate: boolean = false,
patchQueue?: any[] patchQueue?: any[],
scope?: any,
vars?: any
): Promise<void> { ): Promise<void> {
const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps); const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
if (shouldUpdate) { if (shouldUpdate) {
const defaultProps = (<any>this.constructor).defaultProps; const defaultProps = (<any>this.constructor).defaultProps;
if (defaultProps) { if (defaultProps) {
nextProps = this._applyDefaultProps(nextProps, defaultProps); nextProps = this.__applyDefaultProps(nextProps, defaultProps);
}
if (QWeb.dev) {
this._validateProps(nextProps);
} }
await this.willUpdateProps(nextProps); await this.willUpdateProps(nextProps);
this.props = nextProps; this.props = nextProps;
await this.render(forceUpdate, patchQueue); await this.render(forceUpdate, patchQueue, scope, vars);
} }
} }
_patch(vnode) { __patch(vnode) {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
__owl__.renderPromise = null; __owl__.renderPromise = null;
const target = __owl__.vnode || document.createElement(vnode.sel!); const target = __owl__.vnode || document.createElement(vnode.sel!);
if (this.__owl__.classObj) {
(<any>vnode).data.class = Object.assign((<any>vnode).data.class || {}, this.__owl__.classObj);
}
__owl__.vnode = patch(target, vnode); __owl__.vnode = patch(target, vnode);
} }
_prepare(): Promise<VNode> {
__prepare(scope?: Object, vars?: any): Promise<VNode> {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
__owl__.renderProps = this.props; __owl__.renderProps = this.props;
__owl__.renderPromise = this._prepareAndRender(); __owl__.renderPromise = this.__prepareAndRender(scope, vars);
return __owl__.renderPromise; return __owl__.renderPromise;
} }
async _prepareAndRender(): Promise<VNode> { async __prepareAndRender(scope?: Object, vars?: any): Promise<VNode> {
await this.willStart(); try {
await this.willStart();
} catch (e) {
errorHandler(e, this);
return Promise.resolve(h("div"));
}
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
if (__owl__.isDestroyed) { if (__owl__.isDestroyed) {
return Promise.resolve(h("div")); return Promise.resolve(h("div"));
@@ -450,17 +484,11 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
if (template) { if (template) {
this.template = template; this.template = template;
} else { } else {
while ( while ((template = p.name) && !(template in qweb.templates) && p !== Component) {
(template = p.name) &&
!(template in qweb.templates) &&
p !== Component
) {
p = p.__proto__; p = p.__proto__;
} }
if (p === Component) { if (p === Component) {
throw new Error( throw new Error(`Could not find template for component "${this.constructor.name}"`);
`Could not find template for component "${this.constructor.name}"`
);
} else { } else {
tmap[name] = template; tmap[name] = template;
this.template = template; this.template = template;
@@ -468,13 +496,15 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
} }
} }
__owl__.render = qweb.render.bind(qweb, this.template); __owl__.render = qweb.render.bind(qweb, this.template);
this._observeState(); this.__observeState();
return this._render(); return this.__render(false, [], scope, vars);
} }
async _render( async __render(
force: boolean = false, force: boolean = false,
patchQueue: any[] = [] patchQueue: any[] = [],
scope?: Object,
vars?: any
): Promise<VNode> { ): Promise<VNode> {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
__owl__.renderId++; __owl__.renderId++;
@@ -486,22 +516,30 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
if (__owl__.observer) { if (__owl__.observer) {
__owl__.observer.allowMutations = false; __owl__.observer.allowMutations = false;
} }
let vnode = __owl__.render!(this, { let vnode;
promises, try {
handlers: __owl__.boundHandlers, vnode = __owl__.render!(this, {
mountedHandlers: __owl__.mountedHandlers, promises,
forceUpdate: force, handlers: __owl__.boundHandlers,
patchQueue mountedHandlers: __owl__.mountedHandlers,
}); forceUpdate: force,
patchQueue,
scope,
vars
});
} catch (e) {
vnode = __owl__.vnode || h("div");
errorHandler(e, this);
}
patch.push(vnode); patch.push(vnode);
if (__owl__.observer) { if (__owl__.observer) {
__owl__.observer.allowMutations = true; __owl__.observer.allowMutations = true;
} }
// this part is critical for the patching process to be done correctly. The // this part is critical for the patching process to be done correctly. The
// tricky part is that a child widget can be rerendered on its own, which // tricky part is that a child component can be rerendered on its own, which
// will update its own vnode representation without the knowledge of the // will update its own vnode representation without the knowledge of the
// parent widget. With this, we make sure that the parent widget will be // parent component. With this, we make sure that the parent component will be
// able to patch itself properly after // able to patch itself properly after
vnode.key = __owl__.id; vnode.key = __owl__.id;
__owl__.renderProps = this.props; __owl__.renderProps = this.props;
@@ -510,21 +548,24 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
} }
/** /**
* Only called by qweb t-widget directive * Only called by qweb t-component directive
*/ */
_mount(vnode: VNode, elm: HTMLElement): VNode { __mount(vnode: VNode, elm: HTMLElement): VNode {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
if (__owl__.classObj) {
(<any>vnode).data.class = Object.assign((<any>vnode).data.class || {}, __owl__.classObj);
}
__owl__.vnode = patch(elm, vnode); __owl__.vnode = patch(elm, vnode);
if (__owl__.parent!.__owl__.isMounted && !__owl__.isMounted) { if (__owl__.parent!.__owl__.isMounted && !__owl__.isMounted) {
this._callMounted(); this.__callMounted();
} }
return __owl__.vnode; return __owl__.vnode;
} }
/** /**
* Only called by qweb t-widget directive (when t-keepalive is set) * Only called by qweb t-component directive (when t-keepalive is set)
*/ */
_remount() { __remount() {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
if (!__owl__.isMounted) { if (!__owl__.isMounted) {
__owl__.isMounted = true; __owl__.isMounted = true;
@@ -532,7 +573,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
} }
} }
_observeState() { __observeState() {
if (this.state) { if (this.state) {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
__owl__.observer = new Observer(); __owl__.observer = new Observer();
@@ -547,7 +588,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
* Note that this method does not modify in place the props, it returns a new * Note that this method does not modify in place the props, it returns a new
* prop object * prop object
*/ */
_applyDefaultProps(props: Object | undefined, defaultProps: Object): Props { __applyDefaultProps(props: Object | undefined, defaultProps: Object): Props {
props = props ? Object.create(props) : {}; props = props ? Object.create(props) : {};
for (let propName in defaultProps) { for (let propName in defaultProps) {
if (props![propName] === undefined) { if (props![propName] === undefined) {
@@ -561,106 +602,54 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
* Apply the given patch queue. A patch is a pair [c, vn], where c is a * Apply the given patch queue. A patch is a pair [c, vn], where c is a
* Component instance and vn a VNode. * Component instance and vn a VNode.
* 1) Call 'willPatch' on the component of each patch * 1) Call 'willPatch' on the component of each patch
* 2) Call '_patch' on the component of each patch * 2) Call '__patch' on the component of each patch
* 3) Call 'patched' on the component of each patch, in inverse order * 3) Call 'patched' on the component of each patch, in inverse order
*/ */
_applyPatchQueue(patchQueue: any[]) { __applyPatchQueue(patchQueue: any[]) {
const patchLen = patchQueue.length; let component = this;
for (let i = 0; i < patchLen; i++) { try {
const patch = patchQueue[i]; const patchLen = patchQueue.length;
patch.push(patch[0].willPatch()); for (let i = 0; i < patchLen; i++) {
} const patch = patchQueue[i];
for (let i = 0; i < patchLen; i++) { component = patch[0];
const patch = patchQueue[i]; patch.push(patch[0].willPatch());
patch[0]._patch(patch[1]);
}
for (let i = patchLen - 1; i >= 0; i--) {
const patch = patchQueue[i];
patch[0].patched(patch[2]);
}
}
/**
* Validate the component props (or next props) against the (static) props
* description. This is potentially an expensive operation: it may needs to
* visit recursively the props and all the children to check if they are valid.
* This is why it is only done in 'dev' mode.
*/
_validateProps(props: Object) {
const propsDef = (<any>this.constructor).props;
if (propsDef instanceof Array) {
// list of strings (prop names)
for (let i = 0, l = propsDef.length; i < l; i++) {
if (!(propsDef[i] in props)) {
throw new Error(
`Missing props '${propsDef[i]}' (widget '${this.constructor.name}')`
);
}
} }
} else if (propsDef) { for (let i = 0; i < patchLen; i++) {
// propsDef is an object now const patch = patchQueue[i];
for (let propName in propsDef) { patch[0].__patch(patch[1]);
if (!(propName in props)) {
if (propsDef[propName] && !propsDef[propName].optional) {
throw new Error(
`Missing props '${propName}' (widget '${this.constructor.name}')`
);
} else {
break;
}
}
let isValid = isValidProp(props[propName], propsDef[propName]);
if (!isValid) {
throw new Error(
`Props '${propName}' of invalid type in widget '${
this.constructor.name
}'`
);
}
} }
for (let i = patchLen - 1; i >= 0; i--) {
const patch = patchQueue[i];
component = patch[0];
patch[0].patched(patch[2]);
}
} catch (e) {
errorHandler(e, component);
} }
} }
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Prop validation helper // Error handling
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
/** function errorHandler(error, component) {
* Check if an invidual prop value matches its (static) prop definition let canCatch = false;
*/ let qweb = component.env.qweb;
function isValidProp(prop, propDef): boolean { let root = component;
if (typeof propDef === "function") { while (component && !(canCatch = component.catchError !== Component.prototype.catchError)) {
// Check if a value is constructed by some Constructor. Note that there is a root = component;
// slight abuse of language: we want to consider primitive values as well. component = component.__owl__.parent;
//
// So, even though 1 is not an instance of Number, we want to consider that
// it is valid.
if (typeof prop === "object") {
return prop instanceof propDef;
}
return typeof prop === propDef.name.toLowerCase();
} else if (propDef instanceof Array) {
// If this code is executed, this means that we want to check if a prop
// matches at least one of its descriptor.
let result = false;
for (let i = 0, iLen = propDef.length; i < iLen; i++) {
result = result || isValidProp(prop, propDef[i]);
}
return result;
} }
// propsDef is an object console.error(error);
let result = isValidProp(prop, propDef.type); // we trigger error on QWeb so it can be logged/handled
if (propDef.type === Array) { qweb.trigger("error", error);
for (let i = 0, iLen = prop.length; i < iLen; i++) {
result = result && isValidProp(prop[i], propDef.element); if (canCatch) {
} setTimeout(() => {
component.catchError(error);
});
} else {
root.destroy();
} }
if (propDef.type === Object) {
const shape = propDef.shape;
for (let key in shape) {
result = result && isValidProp(prop[key], shape[key]);
}
}
return result;
} }
+1 -1
View File
@@ -29,7 +29,7 @@ export class EventBus {
* Add a listener for the 'eventType' events. * Add a listener for the 'eventType' events.
* *
* Note that the 'owner' of this event can be anything, but will more likely * Note that the 'owner' of this event can be anything, but will more likely
* be a widget or a class. The idea is that the callback will be called with * be a component or a class. The idea is that the callback will be called with
* the proper owner bound. * the proper owner bound.
* *
* Also, the owner should be kind of unique. This will be used to remove the * Also, the owner should be kind of unique. This will be used to remove the
+2 -2
View File
@@ -15,7 +15,7 @@ import "./qweb_extensions";
import { QWeb } from "./qweb_core"; import { QWeb } from "./qweb_core";
export { QWeb }; export { QWeb };
export { connect, Store } from "./store"; export { Store, ConnectedComponent } from "./store";
import * as _utils from "./utils"; import * as _utils from "./utils";
export const __info__ = {}; export const __info__ = {};
@@ -32,7 +32,7 @@ Object.defineProperty(__info__, "mode", {
`Owl is running in 'dev' mode. This is not suitable for production use. See ${url} for more information.` `Owl is running in 'dev' mode. This is not suitable for production use. See ${url} for more information.`
); );
} else { } else {
console.log(`Owl is now running in 'prod' mode.`) console.log(`Owl is now running in 'prod' mode.`);
} }
} }
}); });
+5 -22
View File
@@ -21,15 +21,7 @@
// we define here a new modified Array prototype, which basically override all // we define here a new modified Array prototype, which basically override all
// Array methods that change some state to be able to track their changes // Array methods that change some state to be able to track their changes
const methodsToPatch = [ const methodsToPatch = ["push", "pop", "shift", "unshift", "splice", "sort", "reverse"];
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse"
];
const methodLen = methodsToPatch.length; const methodLen = methodsToPatch.length;
const ArrayProto = Array.prototype; const ArrayProto = Array.prototype;
@@ -78,18 +70,14 @@ export class Observer {
static set(target: any, key: number | string, value: any) { static set(target: any, key: number | string, value: any) {
if (!target.__owl__) { if (!target.__owl__) {
throw Error( throw Error("`Observer.set()` can only be called with observed Objects or Arrays");
"`Observer.set()` can only be called with observed Objects or Arrays"
);
} }
target.__owl__.observer.set(target, key, value); target.__owl__.observer.set(target, key, value);
} }
static delete(target: any, key: number | string) { static delete(target: any, key: number | string) {
if (!target.__owl__) { if (!target.__owl__) {
throw Error( throw Error("`Observer.delete()` can only be called with observed Objects");
"`Observer.delete()` can only be called with observed Objects"
);
} }
target.__owl__.observer.delete(target, key); target.__owl__.observer.delete(target, key);
} }
@@ -127,8 +115,7 @@ export class Observer {
set(target: any, key: number | string, value: any) { set(target: any, key: number | string, value: any) {
let alreadyDefined = let alreadyDefined =
key in target && key in target && Object.getOwnPropertyDescriptor(target, key)!.configurable === false;
Object.getOwnPropertyDescriptor(target, key)!.configurable === false;
if (alreadyDefined) { if (alreadyDefined) {
target[key] = value; target[key] = value;
} else { } else {
@@ -172,11 +159,7 @@ export class Observer {
} }
} }
_addProp<T extends { __owl__?: any }>( _addProp<T extends { __owl__?: any }>(obj: T, key: string | number, value: any) {
obj: T,
key: string | number,
value: any
) {
var self = this; var self = this;
Object.defineProperty(obj, key, { Object.defineProperty(obj, key, {
configurable: true, configurable: true,
+219 -111
View File
@@ -1,5 +1,6 @@
import { VNode, h } from "./vdom"; import { VNode, h, patch } from "./vdom";
import { QWebVar, compileExpr } from "./qweb_expressions"; import { QWebVar, compileExpr } from "./qweb_expressions";
import { EventBus } from "./event_bus";
/** /**
* Owl QWeb Engine * Owl QWeb Engine
@@ -58,14 +59,7 @@ export interface Directive {
// Const/global stuff/helpers // Const/global stuff/helpers
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const DISABLED_TAGS = [ const DISABLED_TAGS = ["input", "textarea", "button", "select", "option", "optgroup"];
"input",
"textarea",
"button",
"select",
"option",
"optgroup"
];
const lineBreakRE = /[\r\n]/; const lineBreakRE = /[\r\n]/;
const whitespaceRE = /\s+/g; const whitespaceRE = /\s+/g;
@@ -87,21 +81,27 @@ const NODE_HOOKS_PARAMS = {
interface Utils { interface Utils {
h: typeof h; h: typeof h;
objectToAttrString(obj: Object): string; toObj(expr: any): Object;
shallowEqual(p1: Object, p2: Object): boolean; shallowEqual(p1: Object, p2: Object): boolean;
[key: string]: any; [key: string]: any;
} }
export const UTILS: Utils = { export const UTILS: Utils = {
h: h, h: h,
objectToAttrString(obj: Object): string { toObj(expr) {
let classes: string[] = []; if (typeof expr === "string") {
for (let k in obj) { expr = expr.trim();
if (obj[k]) { if (!expr) {
classes.push(k); return {};
} }
let words = expr.split(/\s+/);
let result = {};
for (let i = 0; i < words.length; i++) {
result[words[i]] = true;
}
return result;
} }
return classes.join(" "); return expr;
}, },
shallowEqual(p1, p2) { shallowEqual(p1, p2) {
for (let k in p1) { for (let k in p1) {
@@ -117,7 +117,27 @@ function parseXML(xml: string): Document {
const parser = new DOMParser(); const parser = new DOMParser();
const doc = parser.parseFromString(xml, "text/xml"); const doc = parser.parseFromString(xml, "text/xml");
if (doc.getElementsByTagName("parsererror").length) { if (doc.getElementsByTagName("parsererror").length) {
throw new Error("Invalid XML in template"); let msg = "Invalid XML in template.";
const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
if (parsererrorText) {
msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
const re = /\d+/g;
const firstMatch = re.exec(parsererrorText);
if (firstMatch) {
const lineNumber = Number(firstMatch[0]);
const line = xml.split("\n")[lineNumber - 1];
const secondMatch = re.exec(parsererrorText);
if (line && secondMatch) {
const columnIndex = Number(secondMatch[0]) - 1;
if (line[columnIndex]) {
msg +=
`\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
`${line}\n${"-".repeat(columnIndex - 1)}^`;
}
}
}
}
throw new Error(msg);
} }
return doc; return doc;
} }
@@ -127,10 +147,10 @@ let nextID = 1;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// QWeb rendering engine // QWeb rendering engine
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
export class QWeb { export class QWeb extends EventBus {
templates: { [name: string]: Template } = {}; templates: { [name: string]: Template } = {};
utils = UTILS; utils = UTILS;
static widgets = Object.create(null); static components = Object.create(null);
// dev mode enables better error messages or more costly validations // dev mode enables better error messages or more costly validations
static dev: boolean = false; static dev: boolean = false;
@@ -140,12 +160,13 @@ export class QWeb {
// able to map a qweb instance to a template name. // able to map a qweb instance to a template name.
id = nextID++; id = nextID++;
// slots contains sub templates defined with t-set inside t-widget nodes, and // slots contains sub templates defined with t-set inside t-component nodes, and
// are meant to be used by the t-slot directive. // are meant to be used by the t-slot directive.
slots = {}; slots = {};
nextSlotId = 1; nextSlotId = 1;
constructor(data?: string) { constructor(data?: string) {
super();
if (data) { if (data) {
this.addTemplates(data); this.addTemplates(data);
} }
@@ -161,17 +182,20 @@ export class QWeb {
} }
static register(name: string, Component: any) { static register(name: string, Component: any) {
if (QWeb.widgets[name]) { if (QWeb.components[name]) {
throw new Error(`Component '${name}' has already been registered`); throw new Error(`Component '${name}' has already been registered`);
} }
QWeb.widgets[name] = Component; QWeb.components[name] = Component;
} }
/** /**
* Add a template to the internal template map. Note that it is not * Add a template to the internal template map. Note that it is not
* immediately compiled. * immediately compiled.
*/ */
addTemplate(name: string, xmlString: string) { addTemplate(name: string, xmlString: string, allowDuplicate?: boolean) {
if (allowDuplicate && name in this.templates) {
return;
}
const doc = parseXML(xmlString); const doc = parseXML(xmlString);
if (!doc.firstChild) { if (!doc.firstChild) {
throw new Error("Invalid template (should not be empty)"); throw new Error("Invalid template (should not be empty)");
@@ -234,9 +258,7 @@ export class QWeb {
return a + b; return a + b;
}) > 1 }) > 1
) { ) {
throw new Error( throw new Error("Only one conditional branching directive is allowed per node");
"Only one conditional branching directive is allowed per node"
);
} }
// All text nodes between branch nodes are removed // All text nodes between branch nodes are removed
let textNode; let textNode;
@@ -266,53 +288,77 @@ export class QWeb {
return template.fn.call(this, context, extra); return template.fn.call(this, context, extra);
} }
_compile(name: string, elem: Element): CompiledTemplate { /**
* Render a template to a html string.
*
* Note that this is more limited than the `render` method: it is not suitable
* to render a full component tree, since this is an asynchronous operation.
* This method can only render templates without components.
*/
renderToString(name: string, context: EvalContext = {}): string {
const vnode = this.render(name, context);
if (vnode.sel === undefined) {
return vnode.text!;
}
const node = document.createElement(vnode.sel);
const result = patch(node, vnode);
return (<HTMLElement>result.elm).outerHTML;
}
_compile(name: string, elem: Element, parentContext?: Context): CompiledTemplate {
const isDebug = elem.attributes.hasOwnProperty("t-debug"); const isDebug = elem.attributes.hasOwnProperty("t-debug");
const ctx = new Context(name); const ctx = new Context(name);
if (parentContext) {
ctx.variables = Object.create(parentContext.variables);
ctx.nextID = parentContext.parentNode! + 1;
ctx.parentNode = parentContext.parentNode!;
ctx.allowMultipleRoots = true;
ctx.hasParentWidget = true;
ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`);
for (let v in parentContext.variables) {
let variable = <any>parentContext.variables[v];
if (variable.id) {
ctx.addLine(`let ${variable.id} = extra.vars.${variable.id}`);
}
}
}
if (parentContext) {
ctx.addLine(" Object.assign(context, extra.scope);");
}
this._compileNode(elem, ctx); this._compileNode(elem, ctx);
if (ctx.shouldProtectContext) { if (!parentContext) {
ctx.code.unshift(" context = Object.create(context);"); if (ctx.shouldDefineResult) {
} ctx.addLine(`return result;`);
if (ctx.shouldDefineOwner) { } else {
// this is necessary to prevent some directives (t-forach for ex) to if (!ctx.rootNode) {
// pollute the rendering context by adding some keys in it. throw new Error(`A template should have one root node (${ctx.templateName})`);
ctx.code.unshift(" let owner = context;"); }
} ctx.addLine(`return vn${ctx.rootNode};`);
if (ctx.shouldDefineQWeb) { }
ctx.code.unshift(" let QWeb = this.constructor;");
}
if (ctx.shouldDefineUtils) {
ctx.code.unshift(" let utils = this.utils;");
} }
if (!ctx.rootNode) { let code = ctx.generateCode();
throw new Error("A template should have one root node");
}
ctx.addLine(`return vn${ctx.rootNode};`);
let template; let template;
try { try {
template = new Function( template = new Function("context", "extra", code.join("\n")) as CompiledTemplate;
"context",
"extra",
ctx.code.join("\n")
) as CompiledTemplate;
} catch (e) { } catch (e) {
const templateName = ctx.templateName.replace(/`/g, "'"); const templateName = ctx.templateName.replace(/`/g, "'");
console.groupCollapsed(`Invalid Code generated by ${templateName}`); console.groupCollapsed(`Invalid Code generated by ${templateName}`);
console.warn(ctx.code.join("\n")); console.warn(code.join("\n"));
console.groupEnd(); console.groupEnd();
throw new Error( throw new Error(
`Invalid generated code while compiling template '${templateName}': ${ `Invalid generated code while compiling template '${templateName}': ${e.message}`
e.message
}`
); );
} }
if (isDebug) { if (isDebug) {
console.log( const tpl = this.templates[name];
`Template: ${this.templates[name].elem.outerHTML}\nCompiled code:\n` + if (tpl) {
template.toString() const msg = `Template: ${tpl.elem.outerHTML}\nCompiled code:\n${template.toString()}`;
); console.log(msg);
}
} }
return template; return template;
} }
@@ -346,6 +392,12 @@ export class QWeb {
return; return;
} }
const firstLetter = node.tagName[0];
if (firstLetter === firstLetter.toUpperCase()) {
// this is a component, we modify in place the xml document to change
// <SomeComponent ... /> to <t t-component="SomeComponent" ... />
node.setAttribute("t-component", node.tagName);
}
const attributes = (<Element>node).attributes; const attributes = (<Element>node).attributes;
const validDirectives: { const validDirectives: {
@@ -384,7 +436,7 @@ export class QWeb {
fullName = name; fullName = name;
value = attributes[j].textContent; value = attributes[j].textContent;
validDirectives.push({ directive, value, fullName }); validDirectives.push({ directive, value, fullName });
if (directive.name === "on" || directive.name === 'model') { if (directive.name === "on" || directive.name === "model") {
withHandlers = true; withHandlers = true;
} }
} }
@@ -453,11 +505,7 @@ export class QWeb {
} }
} }
_compileGenericNode( _compileGenericNode(node: ChildNode, ctx: Context, withHandlers: boolean = true): number {
node: ChildNode,
ctx: Context,
withHandlers: boolean = true
): number {
// nodeType 1 is generic tag // nodeType 1 is generic tag
if (node.nodeType !== 1) { if (node.nodeType !== 1) {
throw new Error("unsupported node type"); throw new Error("unsupported node type");
@@ -481,34 +529,39 @@ export class QWeb {
if (key === "disabled" && DISABLED_TAGS.indexOf(node.nodeName) > -1) { if (key === "disabled" && DISABLED_TAGS.indexOf(node.nodeName) > -1) {
isProp = true; isProp = true;
} }
if ( if ((key === "readonly" && node.nodeName === "input") || node.nodeName === "textarea") {
(key === "readonly" && node.nodeName === "input") ||
node.nodeName === "textarea"
) {
isProp = true; isProp = true;
} }
if (isProp) { if (isProp) {
props.push(`${key}: _${val}`); props.push(`${key}: _${val}`);
} }
} }
let classObj = "";
for (let i = 0; i < attributes.length; i++) { for (let i = 0; i < attributes.length; i++) {
let name = attributes[i].name; let name = attributes[i].name;
const value = attributes[i].textContent!; const value = attributes[i].textContent!;
// regular attributes // regular attributes
if ( if (!name.startsWith("t-") && !(<Element>node).getAttribute("t-attf-" + name)) {
!name.startsWith("t-") &&
!(<Element>node).getAttribute("t-attf-" + name)
) {
const attID = ctx.generateID(); const attID = ctx.generateID();
ctx.addLine(`var _${attID} = '${value}';`); if (name === "class") {
if (!name.match(/^[a-zA-Z]+$/)) { let classDef = value
// attribute contains 'non letters' => we want to quote it .trim()
name = '"' + name + '"'; .split(/\s+/)
.map(a => `'${a}':true`)
.join(",");
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = {${classDef}};`);
} else {
ctx.addLine(`var _${attID} = '${value}';`);
if (!name.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
name = '"' + name + '"';
}
attrs.push(`${name}: _${attID}`);
handleBooleanProps(name, attID);
} }
attrs.push(`${name}: _${attID}`);
handleBooleanProps(name, attID);
} }
// dynamic attributes // dynamic attributes
@@ -516,32 +569,35 @@ export class QWeb {
let attName = name.slice(6); let attName = name.slice(6);
const v = ctx.getValue(value); const v = ctx.getValue(value);
let formattedValue = v.id || ctx.formatExpression(v); let formattedValue = v.id || ctx.formatExpression(v);
if (
formattedValue[0] === "{" && if (attName === "class") {
formattedValue[formattedValue.length - 1] === "}" formattedValue = `this.utils.toObj(${formattedValue})`;
) { if (classObj) {
formattedValue = `this.utils.objectToAttrString(${formattedValue})`; ctx.addLine(`Object.assign(${classObj}, ${formattedValue})`);
} else {
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = ${formattedValue};`);
}
} else {
const attID = ctx.generateID();
if (!attName.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
attName = '"' + attName + '"';
}
// we need to combine dynamic with non dynamic attributes:
// class="a" t-att-class="'yop'" should be rendered as class="a yop"
const attValue = (<Element>node).getAttribute(attName);
if (attValue) {
const attValueID = ctx.generateID();
ctx.addLine(`var _${attValueID} = ${formattedValue};`);
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
const attrIndex = attrs.findIndex(att => att.startsWith(attName + ":"));
attrs.splice(attrIndex, 1);
}
ctx.addLine(`var _${attID} = ${formattedValue};`);
attrs.push(`${attName}: _${attID}`);
handleBooleanProps(attName, attID);
} }
const attID = ctx.generateID();
if (!attName.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
attName = '"' + attName + '"';
}
// we need to combine dynamic with non dynamic attributes:
// class="a" t-att-class="'yop'" should be rendered as class="a yop"
const attValue = (<Element>node).getAttribute(attName);
if (attValue) {
const attValueID = ctx.generateID();
ctx.addLine(`var _${attValueID} = ${formattedValue};`);
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
const attrIndex = attrs.findIndex(att =>
att.startsWith(attName + ":")
);
attrs.splice(attrIndex, 1);
}
ctx.addLine(`var _${attID} = ${formattedValue};`);
attrs.push(`${attName}: _${attID}`);
handleBooleanProps(attName, attID);
} }
if (name.startsWith("t-attf-")) { if (name.startsWith("t-attf-")) {
@@ -582,6 +638,9 @@ export class QWeb {
if (props.length > 0) { if (props.length > 0) {
parts.push(`props:{${props.join(",")}}`); parts.push(`props:{${props.join(",")}}`);
} }
if (classObj) {
parts.push(`class:${classObj}`);
}
if (withHandlers) { if (withHandlers) {
parts.push(`on:{}`); parts.push(`on:{}`);
} }
@@ -598,9 +657,7 @@ export class QWeb {
ctx.addLine(`}`); ctx.addLine(`}`);
ctx.closeIf(); ctx.closeIf();
} }
ctx.addLine( ctx.addLine(`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`);
`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`
);
if (ctx.parentNode) { if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`); ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`);
} }
@@ -633,12 +690,18 @@ export class Context {
rootContext: Context; rootContext: Context;
caller: Element | undefined; caller: Element | undefined;
shouldDefineOwner: boolean = false; shouldDefineOwner: boolean = false;
shouldDefineParent: boolean = false;
shouldDefineQWeb: boolean = false; shouldDefineQWeb: boolean = false;
shouldDefineUtils: boolean = false; shouldDefineUtils: boolean = false;
shouldDefineResult: boolean = false;
shouldProtectContext: boolean = false; shouldProtectContext: boolean = false;
shouldTrackScope: boolean = false;
inLoop: boolean = false; inLoop: boolean = false;
inPreTag: boolean = false; inPreTag: boolean = false;
templateName: string; templateName: string;
allowMultipleRoots: boolean = false;
hasParentWidget: boolean = false;
scopeVars: any[] = [];
constructor(name?: string) { constructor(name?: string) {
this.rootContext = this; this.rootContext = this;
@@ -651,8 +714,50 @@ export class Context {
return id; return id;
} }
generateCode(): string[] {
const shouldTrackScope = this.shouldTrackScope && this.scopeVars.length;
if (shouldTrackScope) {
// add some vars to scope if needed
for (let scopeVar of this.scopeVars.reverse()) {
let { index, key, indent } = scopeVar;
const prefix = new Array(indent + 2).join(" ");
this.code.splice(index + 1, 0, prefix + `scope.${key} = context.${key};`);
}
this.code.unshift(" const scope = Object.create(null);");
}
if (this.shouldProtectContext) {
this.code.unshift(" context = Object.create(context);");
}
if (this.shouldDefineResult) {
this.code.unshift(" let result;");
}
if (this.shouldDefineOwner) {
// this is necessary to prevent some directives (t-forach for ex) to
// pollute the rendering context by adding some keys in it.
this.code.unshift(" let owner = context;");
}
if (this.shouldDefineParent) {
if (this.hasParentWidget) {
this.code.unshift(" let parent = extra.parent;");
} else {
this.code.unshift(" let parent = context;");
}
}
if (this.shouldDefineQWeb) {
this.code.unshift(" let QWeb = this.constructor;");
}
if (this.shouldDefineUtils) {
this.code.unshift(" let utils = this.utils;");
}
return this.code;
}
withParent(node: number): Context { withParent(node: number): Context {
if (this === this.rootContext && (this.parentNode || this.parentTextNode)) { if (
!this.allowMultipleRoots &&
this === this.rootContext &&
(this.parentNode || this.parentTextNode)
) {
throw new Error("A template should not have more than one root node"); throw new Error("A template should not have more than one root node");
} }
if (!this.rootContext.rootNode) { if (!this.rootContext.rootNode) {
@@ -675,9 +780,15 @@ export class Context {
this.indentLevel--; this.indentLevel--;
} }
addLine(line: string) { addLine(line: string): number {
const prefix = new Array(this.indentLevel + 2).join(" "); const prefix = new Array(this.indentLevel + 2).join(" ");
this.code.push(prefix + line); this.code.push(prefix + line);
return this.code.length - 1;
}
addToScope(key: string, expr: string) {
const index = this.addLine(`context.${key} = ${expr};`);
this.rootContext.scopeVars.push({ index, key, indent: this.indentLevel });
} }
addIf(condition: string) { addIf(condition: string) {
@@ -724,10 +835,7 @@ export class Context {
return `(${this.formatExpression(s.slice(2, -2))})`; return `(${this.formatExpression(s.slice(2, -2))})`;
} }
let r = s.replace( let r = s.replace(/\{\{.*?\}\}/g, s => "${" + this.formatExpression(s.slice(2, -2)) + "}");
/\{\{.*?\}\}/g,
s => "${" + this.formatExpression(s.slice(2, -2)) + "}"
);
return "`" + r + "`"; return "`" + r + "`";
} }
} }
+13 -22
View File
@@ -60,9 +60,7 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Context) {
ctx.addLine(`var frag${fragID} = this.utils.getFragment(${exprID})`); ctx.addLine(`var frag${fragID} = this.utils.getFragment(${exprID})`);
let tempNodeID = ctx.generateID(); let tempNodeID = ctx.generateID();
ctx.addLine(`var p${tempNodeID} = {hook: {`); ctx.addLine(`var p${tempNodeID} = {hook: {`);
ctx.addLine( ctx.addLine(` insert: n => n.elm.parentNode.replaceChild(frag${fragID}, n.elm),`);
` insert: n => n.elm.parentNode.replaceChild(frag${fragID}, n.elm),`
);
ctx.addLine(`}};`); ctx.addLine(`}};`);
ctx.addLine(`var vn${tempNodeID} = h('div', p${tempNodeID})`); ctx.addLine(`var vn${tempNodeID} = h('div', p${tempNodeID})`);
ctx.addLine(`c${ctx.parentNode}.push(vn${tempNodeID});`); ctx.addLine(`c${ctx.parentNode}.push(vn${tempNodeID});`);
@@ -116,9 +114,7 @@ QWeb.addDirective({
if (value) { if (value) {
const formattedValue = ctx.formatExpression(value); const formattedValue = ctx.formatExpression(value);
if (ctx.variables.hasOwnProperty(variable)) { if (ctx.variables.hasOwnProperty(variable)) {
ctx.addLine( ctx.addLine(`${(<QWebExprVar>ctx.variables[variable]).id} = ${formattedValue}`);
`${(<QWebExprVar>ctx.variables[variable]).id} = ${formattedValue}`
);
} else { } else {
const varName = `_${ctx.generateID()}`; const varName = `_${ctx.generateID()}`;
ctx.addLine(`var ${varName} = ${formattedValue};`); ctx.addLine(`var ${varName} = ${formattedValue};`);
@@ -220,9 +216,7 @@ QWeb.addDirective({
} }
// compile sub template // compile sub template
const subCtx = ctx const subCtx = ctx.subContext("caller", nodeCopy).subContext("variables", Object.create(vars));
.subContext("caller", nodeCopy)
.subContext("variables", Object.create(vars));
qweb._compileNode(nodeTemplate.elem, subCtx); qweb._compileNode(nodeTemplate.elem, subCtx);
@@ -250,9 +244,7 @@ QWeb.addDirective({
const name = node.getAttribute("t-as")!; const name = node.getAttribute("t-as")!;
let arrayID = ctx.generateID(); let arrayID = ctx.generateID();
ctx.addLine(`var _${arrayID} = ${ctx.formatExpression(elems)};`); ctx.addLine(`var _${arrayID} = ${ctx.formatExpression(elems)};`);
ctx.addLine( ctx.addLine(`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`);
`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`
);
let keysID = ctx.generateID(); let keysID = ctx.generateID();
let valuesID = ctx.generateID(); let valuesID = ctx.generateID();
ctx.addLine(`var _${keysID} = _${valuesID} = _${arrayID};`); ctx.addLine(`var _${keysID} = _${valuesID} = _${arrayID};`);
@@ -263,16 +255,15 @@ QWeb.addDirective({
ctx.addLine(`var _length${keysID} = _${keysID}.length;`); ctx.addLine(`var _length${keysID} = _${keysID}.length;`);
ctx.addLine(`for (let i = 0; i < _length${keysID}; i++) {`); ctx.addLine(`for (let i = 0; i < _length${keysID}; i++) {`);
ctx.indent(); ctx.indent();
ctx.addLine(`context.${name}_first = i === 0;`); ctx.addToScope(name + '_first', 'i === 0');
ctx.addLine(`context.${name}_last = i === _length${keysID} - 1;`); ctx.addToScope(name + '_last', `i === _length${keysID} - 1`);
ctx.addLine(`context.${name}_index = i;`); ctx.addToScope(name + '_index', 'i');
ctx.addLine(`context.${name} = _${keysID}[i];`); ctx.addToScope(name, `_${keysID}[i]`);
ctx.addLine(`context.${name}_value = _${valuesID}[i];`); ctx.addToScope(name + '_value', `_${valuesID}[i]`);
const nodeCopy = <Element>node.cloneNode(true); const nodeCopy = <Element>node.cloneNode(true);
let shouldWarn = let shouldWarn = nodeCopy.tagName !== "t" && !nodeCopy.hasAttribute("t-key");
nodeCopy.tagName !== "t" && !nodeCopy.hasAttribute("t-key");
if (!shouldWarn && node.tagName === "t") { if (!shouldWarn && node.tagName === "t") {
if (node.hasAttribute("t-widget") && !node.hasAttribute("t-key")) { if (node.hasAttribute("t-component") && !node.hasAttribute("t-key")) {
shouldWarn = true; shouldWarn = true;
} }
if ( if (
@@ -304,7 +295,7 @@ QWeb.addDirective({
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
QWeb.addDirective({ QWeb.addDirective({
name: "debug", name: "debug",
priority: 99, priority: 1,
atNodeEncounter({ ctx }) { atNodeEncounter({ ctx }) {
ctx.addLine("debugger;"); ctx.addLine("debugger;");
} }
@@ -315,7 +306,7 @@ QWeb.addDirective({
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
QWeb.addDirective({ QWeb.addDirective({
name: "log", name: "log",
priority: 99, priority: 1,
atNodeEncounter({ ctx, value }) { atNodeEncounter({ ctx, value }) {
const expr = ctx.formatExpression(value); const expr = ctx.formatExpression(value);
ctx.addLine(`console.log(${expr})`); ctx.addLine(`console.log(${expr})`);
+3 -9
View File
@@ -82,7 +82,7 @@ const STATIC_TOKEN_MAP: { [key: string]: TKind } = {
")": "RIGHT_PAREN" ")": "RIGHT_PAREN"
}; };
const OPERATORS = ".,===,==,+,!,||,&&,>=,>,<=,<,?,-,*,/,%".split(','); const OPERATORS = ".,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%".split(",");
type Tokenizer = (expr: string) => Token | false; type Tokenizer = (expr: string) => Token | false;
@@ -231,10 +231,7 @@ export function tokenize(expr: string): Token[] {
* - or if the previous token is a left brace or a comma, and the next token is * - or if the previous token is a left brace or a comma, and the next token is
* a colon (in that case, this is an object key: `{a: b}`) * a colon (in that case, this is an object key: `{a: b}`)
*/ */
export function compileExpr( export function compileExpr(expr: string, vars: { [key: string]: QWebVar }): string {
expr: string,
vars: { [key: string]: QWebVar }
): string {
const tokens = tokenize(expr); const tokens = tokenize(expr);
let result = ""; let result = "";
for (let i = 0; i < tokens.length; i++) { for (let i = 0; i < tokens.length; i++) {
@@ -246,10 +243,7 @@ export function compileExpr(
if (prevToken) { if (prevToken) {
if (prevToken.type === "OPERATOR" && prevToken.value === ".") { if (prevToken.type === "OPERATOR" && prevToken.value === ".") {
isVar = false; isVar = false;
} else if ( } else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") {
prevToken.type === "LEFT_BRACE" ||
prevToken.type === "COMMA"
) {
let nextToken = tokens[i + 1]; let nextToken = tokens[i + 1];
if (nextToken && nextToken.type === "COLON") { if (nextToken && nextToken.type === "COLON") {
isVar = false; isVar = false;
+292 -144
View File
@@ -10,7 +10,7 @@ import { VNode } from "./vdom";
* - t-on * - t-on
* - t-ref * - t-ref
* - t-transition * - t-transition
* - t-widget/t-keepalive * - t-component/t-keepalive
* - t-mounted * - t-mounted
* - t-slot * - t-slot
* - t-model * - t-model
@@ -49,9 +49,7 @@ QWeb.addDirective({
)}'\`)` )}'\`)`
); );
ctx.closeIf(); ctx.closeIf();
let params = extraArgs let params = extraArgs ? `owner, ${ctx.formatExpression(extraArgs)}` : "owner";
? `owner, ${ctx.formatExpression(extraArgs)}`
: "owner";
let handler; let handler;
if (mods.length > 0) { if (mods.length > 0) {
handler = `function (e) {`; handler = `function (e) {`;
@@ -70,9 +68,7 @@ QWeb.addDirective({
ctx.addLine( ctx.addLine(
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};` `extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`
); );
ctx.addLine( ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`);
`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`
);
} }
} }
}); });
@@ -101,9 +97,7 @@ UTILS.transitionInsert = function(vn: VNode, name: string) {
const elm = <HTMLElement>vn.elm; const elm = <HTMLElement>vn.elm;
// remove potential duplicated vnode that is currently being removed, to // remove potential duplicated vnode that is currently being removed, to
// prevent from having twice the same node in the DOM during an animation // prevent from having twice the same node in the DOM during an animation
const dup = const dup = elm.parentElement && elm.parentElement!.querySelector(`*[data-owl-key='${vn.key}']`);
elm.parentElement &&
elm.parentElement!.querySelector(`*[data-owl-key='${vn.key}']`);
if (dup) { if (dup) {
dup.remove(); dup.remove();
} }
@@ -164,9 +158,7 @@ function toMs(s: string): number {
function whenTransitionEnd(elm: HTMLElement, cb) { function whenTransitionEnd(elm: HTMLElement, cb) {
const styles = window.getComputedStyle(elm); const styles = window.getComputedStyle(elm);
const delays: Array<string> = (styles.transitionDelay || "").split(", "); const delays: Array<string> = (styles.transitionDelay || "").split(", ");
const durations: Array<string> = (styles.transitionDuration || "").split( const durations: Array<string> = (styles.transitionDuration || "").split(", ");
", "
);
const timeout: number = getTimeout(delays, durations); const timeout: number = getTimeout(delays, durations);
if (timeout > 0) { if (timeout > 0) {
elm.addEventListener("transitionend", cb, { once: true }); elm.addEventListener("transitionend", cb, { once: true });
@@ -191,24 +183,37 @@ QWeb.addDirective({
}); });
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// t-widget // t-component
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const T_WIDGET_MODS_CODE = Object.assign({}, MODS_CODE, { const T_COMPONENT_MODS_CODE = Object.assign({}, MODS_CODE, {
self: "if (e.target !== vn.elm) {return}" self: "if (e.target !== vn.elm) {return}"
}); });
UTILS.defineProxy = function defineProxy(target, source) {
for (let k in source) {
Object.defineProperty(target, k, {
get() {
return source[k];
},
set(val) {
source[k] = val;
}
});
}
};
/** /**
* The t-widget directive is certainly a complicated and hard to maintain piece * The t-component 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 * of code. To help you, fellow developer, if you have to maintain it, I offer
* you this advice: Good luck... * you this advice: Good luck...
* *
* Since it is not 'direct' code, but rather code that generates other code, it * Since it is not 'direct' code, but rather code that generates other code, it
* is not easy to understand. To help you, here is a detailed and commented * is not easy to understand. To help you, here is a detailed and commented
* explanation of the code generated by the t-widget directive for the following * explanation of the code generated by the t-component directive for the following
* situation: * situation:
* ```xml * ```xml
* <t t-widget="child" * <Child
* t-key="'somestring'" * t-key="'somestring'"
* flag="state.flag" * flag="state.flag"
* t-transition="fade"/> * t-transition="fade"/>
@@ -216,23 +221,23 @@ const T_WIDGET_MODS_CODE = Object.assign({}, MODS_CODE, {
* *
* ```js * ```js
* // we assign utils on top of the function because it will be useful for * // we assign utils on top of the function because it will be useful for
* // each widgets * // each components
* let utils = this.utils; * let utils = this.utils;
* *
* // this is the virtual node representing the parent div * // this is the virtual node representing the parent div
* let c1 = [], p1 = { key: 1 }; * let c1 = [], p1 = { key: 1 };
* var vn1 = h("div", p1, c1); * var vn1 = h("div", p1, c1);
* *
* // t-widget directive: we start by evaluating the expression given by t-key: * // t-component directive: we start by evaluating the expression given by t-key:
* let key5 = "somestring"; * let key5 = "somestring";
* *
* // def3 is the promise that will contain later either the new widget * // def3 is the promise that will contain later either the new component
* // creation, or the props update... * // creation, or the props update...
* let def3; * let def3;
* *
* // this is kind of tricky: we need here to find if the widget was already * // this is kind of tricky: we need here to find if the component was already
* // created by a previous rendering. This is done by checking the internal * // created by a previous rendering. This is done by checking the internal
* // `cmap` (children map) of the parent widget: it maps keys to widget ids, * // `cmap` (children map) of the parent component: it maps keys to component ids,
* // and, then, if there is an id, we look into the children list to get the * // and, then, if there is an id, we look into the children list to get the
* // instance * // instance
* let w4 = * let w4 =
@@ -240,9 +245,9 @@ const T_WIDGET_MODS_CODE = Object.assign({}, MODS_CODE, {
* ? context.__owl__.children[context.__owl__.cmap[key5]] * ? context.__owl__.children[context.__owl__.cmap[key5]]
* : false; * : false;
* *
* // We keep the index of the position of the widget in the closure. We push * // We keep the index of the position of the component in the closure. We push
* // null to reserve the slot, and will replace it later by the widget vnode, * // null to reserve the slot, and will replace it later by the component vnode,
* // when it will be ready (do not forget that preparing/rendering a widget is * // when it will be ready (do not forget that preparing/rendering a component is
* // asynchronous) * // asynchronous)
* let _2_index = c1.length; * let _2_index = c1.length;
* c1.push(null); * c1.push(null);
@@ -252,7 +257,7 @@ const T_WIDGET_MODS_CODE = Object.assign({}, MODS_CODE, {
* // computation, so it is certainly better to do it only once * // computation, so it is certainly better to do it only once
* let props4 = { flag: context["state"].flag }; * let props4 = { flag: context["state"].flag };
* *
* // If we have a widget, currently rendering, but not ready yet, we do not want * // If we have a component, currently rendering, but not ready yet, we do not want
* // to wait for it to be ready if we can avoid it * // to wait for it to be ready if we can avoid it
* if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) { * if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
* // we check if the props are the same. In that case, we can simply reuse * // we check if the props are the same. In that case, we can simply reuse
@@ -260,7 +265,7 @@ const T_WIDGET_MODS_CODE = Object.assign({}, MODS_CODE, {
* if (utils.shallowEqual(props4, w4.__owl__.renderProps)) { * if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
* def3 = w4.__owl__.renderPromise; * def3 = w4.__owl__.renderPromise;
* } else { * } else {
* // if the props are not the same, we destroy the widget and starts anew. * // if the props are not the same, we destroy the component and starts anew.
* // this will be faster than waiting for its rendering, then updating it * // this will be faster than waiting for its rendering, then updating it
* w4.destroy(); * w4.destroy();
* w4 = false; * w4 = false;
@@ -268,29 +273,29 @@ const T_WIDGET_MODS_CODE = Object.assign({}, MODS_CODE, {
* } * }
* *
* if (!w4) { * if (!w4) {
* // in this situation, we need to create a new widget. First step is * // in this situation, we need to create a new component. First step is
* // to get a reference to the class, then create an instance with * // to get a reference to the class, then create an instance with
* // current context as parent, and the props. * // current context as parent, and the props.
* let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4]; * let W4 = context.component && context.components[componentKey4] || QWeb.component[componentKey4];
* if (!W4) { * if (!W4) {
* throw new Error("Cannot find the definition of widget 'child'"); * throw new Error("Cannot find the definition of component 'child'");
* } * }
* w4 = new W4(owner, props4); * w4 = new W4(owner, props4);
* *
* // Whenever we rerender the parent widget, we need to be sure that we * // Whenever we rerender the parent component, we need to be sure that we
* // are able to find the widget instance. To do that, we register it to * // are able to find the component instance. To do that, we register it to
* // the parent cmap (children map). Note that the 'template' key is * // the parent cmap (children map). Note that the 'template' key is
* // used here, since this is what identify the widget from the template * // used here, since this is what identify the component from the template
* // perspective. * // perspective.
* context.__owl__.cmap[key5] = w4.__owl__.id; * context.__owl__.cmap[key5] = w4.__owl__.id;
* *
* // _prepare is called, to basically call willStart, then render the * // __prepare is called, to basically call willStart, then render the
* // widget * // component
* def3 = w4._prepare(); * def3 = w4.__prepare();
* *
* def3 = def3.then(vnode => { * def3 = def3.then(vnode => {
* // we create here a virtual node for the parent (NOT the widget). This * // we create here a virtual node for the parent (NOT the component). This
* // means that the vdom of the parent will be stopped here, and from * // means that the vdom of the parent will be stopped here, and from
* // the parent's perspective, it simply is a vnode with no children. * // the parent's perspective, it simply is a vnode with no children.
* // However, it shares the same dom element with the component root * // However, it shares the same dom element with the component root
@@ -298,16 +303,16 @@ const T_WIDGET_MODS_CODE = Object.assign({}, MODS_CODE, {
* let pvnode = h(vnode.sel, { key: key5 }); * let pvnode = h(vnode.sel, { key: key5 });
* *
* // we add hooks to the parent vnode so we can interact with the new * // we add hooks to the parent vnode so we can interact with the new
* // widget at the proper time * // component at the proper time
* pvnode.data.hook = { * pvnode.data.hook = {
* insert(vn) { * insert(vn) {
* // the _mount method will patch the widget vdom into the elm vn.elm, * // the __mount method will patch the component vdom into the elm vn.elm,
* // then call the mounted hooks. However, suprisingly, the snabbdom * // then call the mounted hooks. However, suprisingly, the snabbdom
* // patch method actually replace the elm by a new elm, so we need * // patch method actually replace the elm by a new elm, so we need
* // to synchronise the pvnode elm with the resulting elm * // to synchronise the pvnode elm with the resulting elm
* let nvn = w4._mount(vnode, vn.elm); * let nvn = w4.__mount(vnode, vn.elm);
* pvnode.elm = nvn.elm; * pvnode.elm = nvn.elm;
* // what follows is only present if there are animations on the widget * // what follows is only present if there are animations on the component
* utils.transitionInsert(vn, "fade"); * utils.transitionInsert(vn, "fade");
* }, * },
* remove() { * remove() {
@@ -317,7 +322,7 @@ const T_WIDGET_MODS_CODE = Object.assign({}, MODS_CODE, {
* }, * },
* destroy() { * destroy() {
* // if there are animations, we delay the call to destroy on the * // if there are animations, we delay the call to destroy on the
* // widget, if not, we call it directly. * // component, if not, we call it directly.
* let finalize = () => { * let finalize = () => {
* w4.destroy(); * w4.destroy();
* }; * };
@@ -328,19 +333,19 @@ const T_WIDGET_MODS_CODE = Object.assign({}, MODS_CODE, {
* c1[_2_index] = pvnode; * c1[_2_index] = pvnode;
* *
* // we keep here a reference to the parent vnode (representing the * // we keep here a reference to the parent vnode (representing the
* // widget, so we can reuse it later whenever we update the widget * // component, so we can reuse it later whenever we update the component
* w4.__owl__.pvnode = pvnode; * w4.__owl__.pvnode = pvnode;
* }); * });
* } else { * } else {
* // this is the 'update' path of the directive. * // this is the 'update' path of the directive.
* // the call to _updateProps is the actual widget update * // the call to __updateProps is the actual component update
* // Note that we only update the props if we cannot reuse the previous * // 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) * // rendering work (in the case it was rendered with the same props)
* def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue); * def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
* def3 = def3.then(() => { * def3 = def3.then(() => {
* // if widget was destroyed in the meantime, we do nothing (so, this * // if component was destroyed in the meantime, we do nothing (so, this
* // means that the parent's element children list will have a null in * // means that the parent's element children list will have a null in
* // the widget's position, which will cause the pvnode to be removed * // the component's position, which will cause the pvnode to be removed
* // when it is patched. * // when it is patched.
* if (w4.__owl__.isDestroyed) { * if (w4.__owl__.isDestroyed) {
* return; * return;
@@ -360,13 +365,14 @@ const T_WIDGET_MODS_CODE = Object.assign({}, MODS_CODE, {
*/ */
QWeb.addDirective({ QWeb.addDirective({
name: "widget", name: "component",
extraNames: ["props", "keepalive", "asyncroot"], extraNames: ["props", "keepalive", "asyncroot"],
priority: 100, priority: 100,
atNodeEncounter({ ctx, value, node, qweb }): boolean { atNodeEncounter({ ctx, value, node, qweb }): boolean {
ctx.addLine("//WIDGET"); ctx.addLine("//COMPONENT");
ctx.rootContext.shouldDefineOwner = true; ctx.rootContext.shouldDefineOwner = true;
ctx.rootContext.shouldDefineQWeb = true; ctx.rootContext.shouldDefineQWeb = true;
ctx.rootContext.shouldDefineParent = true;
ctx.rootContext.shouldDefineUtils = true; ctx.rootContext.shouldDefineUtils = true;
let keepAlive = node.getAttribute("t-keepalive") ? true : false; let keepAlive = node.getAttribute("t-keepalive") ? true : false;
let async = node.getAttribute("t-asyncroot") ? true : false; let async = node.getAttribute("t-asyncroot") ? true : false;
@@ -408,7 +414,7 @@ QWeb.addDirective({
.join(","); .join(",");
let dummyID = ctx.generateID(); let dummyID = ctx.generateID();
let defID = ctx.generateID(); let defID = ctx.generateID();
let widgetID = ctx.generateID(); let componentID = ctx.generateID();
let keyID = key && ctx.generateID(); let keyID = key && ctx.generateID();
if (key) { if (key) {
// we bind a variable to the key (could be a complex expression, so we // we bind a variable to the key (could be a complex expression, so we
@@ -419,8 +425,18 @@ QWeb.addDirective({
let templateID = key let templateID = key
? `key${keyID}` ? `key${keyID}`
: ctx.inLoop : ctx.inLoop
? `String(-${widgetID} - i)` ? `String(-${componentID} - i)`
: String(widgetID); : String(componentID);
if (ctx.allowMultipleRoots) {
// necessary to prevent collisions
if (!key && ctx.inLoop) {
let id = ctx.generateID();
ctx.addLine(`let template${id} = "_slot_" + String(-${componentID} - i)`);
templateID = `template${id}`;
} else {
templateID = `"_slot_${templateID}"`;
}
}
let ref = node.getAttribute("t-ref"); let ref = node.getAttribute("t-ref");
let refExpr = ""; let refExpr = "";
@@ -428,21 +444,19 @@ QWeb.addDirective({
if (ref) { if (ref) {
refKey = `ref${ctx.generateID()}`; refKey = `ref${ctx.generateID()}`;
ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`); ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`);
refExpr = `context.refs[${refKey}] = w${widgetID};`; refExpr = `context.refs[${refKey}] = w${componentID};`;
} }
let transitionsInsertCode = ""; let transitionsInsertCode = "";
if (transition) { if (transition) {
transitionsInsertCode = `utils.transitionInsert(vn, '${transition}');`; transitionsInsertCode = `utils.transitionInsert(vn, '${transition}');`;
} }
let finalizeWidgetCode = `w${widgetID}.${ let finalizeComponentCode = `w${componentID}.${keepAlive ? "unmount" : "destroy"}();`;
keepAlive ? "unmount" : "destroy"
}();`;
if (ref && !keepAlive) { if (ref && !keepAlive) {
finalizeWidgetCode += `delete context.refs[${refKey}];`; finalizeComponentCode += `delete context.refs[${refKey}];`;
} }
if (transition) { if (transition) {
finalizeWidgetCode = `let finalize = () => { finalizeComponentCode = `let finalize = () => {
${finalizeWidgetCode} ${finalizeComponentCode}
}; };
utils.transitionRemove(vn, '${transition}', finalize);`; utils.transitionRemove(vn, '${transition}', finalize);`;
} }
@@ -457,37 +471,50 @@ QWeb.addDirective({
ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattStyle)};`); ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattStyle)};`);
tattStyle = attVar; tattStyle = attVar;
} }
let updateClassCode = ""; let classObj = "";
if (classAttr || tattClass || styleAttr || tattStyle || events.length) { if (classAttr || tattClass || styleAttr || tattStyle || events.length) {
let classCode = "";
if (classAttr) { if (classAttr) {
classCode = let classDef = classAttr
classAttr .trim()
.split(" ") .split(/\s+/)
.map(c => `vn.elm.classList.add('${c}')`) .map(a => `'${a}':true`)
.join(";") + ";"; .join(",");
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = {${classDef}};`);
} }
if (tattClass) { if (tattClass) {
const attVar = `_${ctx.generateID()}`; let tattExpr = ctx.formatExpression(tattClass);
ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattClass)};`); if (tattExpr[0] !== "{" || tattExpr[tattExpr.length - 1] !== "}") {
classCode = `for (let k in ${attVar}) { tattExpr = `utils.toObj(${tattExpr})`;
if (${attVar}[k]) { }
vn.elm.classList.add(k); if (classAttr) {
} ctx.addLine(`Object.assign(${classObj}, ${tattExpr})`);
}`; } else {
updateClassCode = `let cl=w${widgetID}.el.classList;for (let k in ${attVar}) {if (${attVar}[k]) {cl.add(k)} else {cl.remove(k)}}`; classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = ${tattExpr};`);
}
} }
let eventsCode = events let eventsCode = events
.map(function([eventName, mods, handlerName, extraArgs]) { .map(function([eventName, mods, handlerName, extraArgs]) {
let params = extraArgs let params = "owner";
? `owner, ${ctx.formatExpression(extraArgs)}` if (extraArgs) {
: "owner"; if (ctx.inLoop) {
let argId = ctx.generateID();
// we need to evaluate the arguments now, because the handler will
// be set asynchronously later when the widget is ready, and the
// context might be different.
ctx.addLine(`let arg${argId} = ${ctx.formatExpression(extraArgs)};`);
params = `owner, arg${argId}`;
} else {
params = `owner, ${ctx.formatExpression(extraArgs)}`;
}
}
let handler; let handler;
if (mods.length > 0) { if (mods.length > 0) {
handler = `function (e) {`; handler = `function (e) {`;
handler += mods handler += mods
.map(function(mod) { .map(function(mod) {
return T_WIDGET_MODS_CODE[mod]; return T_COMPONENT_MODS_CODE[mod];
}) })
.join(""); .join("");
handler += `owner['${handlerName}'].call(${params}, e);}`; handler += `owner['${handlerName}'].call(${params}, e);}`;
@@ -499,116 +526,147 @@ QWeb.addDirective({
.join(""); .join("");
const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false); const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false);
const styleCode = styleExpr ? `vn.elm.style = ${styleExpr};` : ""; const styleCode = styleExpr ? `vn.elm.style = ${styleExpr};` : "";
createHook = `vnode.data.hook = {create(_, vn){${classCode}${styleCode}${eventsCode}}};`; createHook = `vnode.data.hook = {create(_, vn){${styleCode}${eventsCode}}};`;
} }
ctx.addLine( ctx.addLine(
`let w${widgetID} = ${templateID} in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[${templateID}]] : false;` `let w${componentID} = ${templateID} in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[${templateID}]] : false;`
); );
ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`); if (ctx.parentNode) {
ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`);
}
let shouldProxy = false;
if (async) { if (async) {
ctx.addLine(`const patchQueue${widgetID} = [];`); ctx.addLine(`const patchQueue${componentID} = [];`);
ctx.addLine( ctx.addLine(
`c${ `c${ctx.parentNode}.push(w${componentID} && w${componentID}.__owl__.pvnode || null);`
ctx.parentNode
}.push(w${widgetID} && w${widgetID}.__owl__.pvnode || null);`
); );
} else { } else {
ctx.addLine(`c${ctx.parentNode}.push(null);`); if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push(null);`);
} else {
let id = ctx.generateID();
ctx.rootContext.rootNode = id;
shouldProxy = true;
ctx.rootContext.shouldDefineResult = true;
ctx.addLine(`let vn${id} = {};`);
ctx.addLine(`result = vn${id};`);
}
} }
ctx.addLine(`let props${widgetID} = {${propStr}};`); ctx.addLine(`let props${componentID} = {${propStr}};`);
ctx.addIf( ctx.addIf(
`w${widgetID} && w${widgetID}.__owl__.renderPromise && !w${widgetID}.__owl__.vnode` `w${componentID} && w${componentID}.__owl__.renderPromise && !w${componentID}.__owl__.vnode`
); );
ctx.addIf( ctx.addIf(`utils.shallowEqual(props${componentID}, w${componentID}.__owl__.renderProps)`);
`utils.shallowEqual(props${widgetID}, w${widgetID}.__owl__.renderProps)` ctx.addLine(`def${defID} = w${componentID}.__owl__.renderPromise;`);
);
ctx.addLine(`def${defID} = w${widgetID}.__owl__.renderPromise;`);
ctx.addElse(); ctx.addElse();
ctx.addLine(`w${widgetID}.destroy();`); ctx.addLine(`w${componentID}.destroy();`);
ctx.addLine(`w${widgetID} = false;`); ctx.addLine(`w${componentID} = false;`);
ctx.closeIf(); ctx.closeIf();
ctx.closeIf(); ctx.closeIf();
ctx.addIf(`!w${widgetID}`); ctx.addIf(`!w${componentID}`);
// new widget // new component
ctx.addLine(`let widgetKey${widgetID} = ${ctx.interpolate(value)};`); ctx.addLine(`let componentKey${componentID} = ${ctx.interpolate(value)};`);
ctx.addLine( ctx.addLine(
`let W${widgetID} = context.widgets && context.widgets[widgetKey${widgetID}] || QWeb.widgets[widgetKey${widgetID}];` `let W${componentID} = context.components && context.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}];`
); );
// maybe only do this in dev mode... // maybe only do this in dev mode...
ctx.addLine( ctx.addLine(
`if (!W${widgetID}) {throw new Error('Cannot find the definition of widget "' + widgetKey${widgetID} + '"')}` `if (!W${componentID}) {throw new Error('Cannot find the definition of component "' + componentKey${componentID} + '"')}`
);
ctx.addLine(`w${widgetID} = new W${widgetID}(owner, props${widgetID});`);
ctx.addLine(
`context.__owl__.cmap[${templateID}] = w${widgetID}.__owl__.id;`
); );
if (QWeb.dev) {
ctx.addLine(`utils.validateProps(W${componentID}, props${componentID})`);
}
ctx.addLine(`w${componentID} = new W${componentID}(parent, props${componentID});`);
ctx.addLine(`parent.__owl__.cmap[${templateID}] = w${componentID}.__owl__.id;`);
// SLOTS // SLOTS
if (node.childElementCount) { const varDefs: string[] = [];
const hasSlots = node.childNodes.length;
if (hasSlots) {
ctx.rootContext.shouldTrackScope = true;
for (let v of Object.values(ctx.variables)) {
if (v["id"]) {
varDefs.push(v["id"]);
}
}
const clone = <Element>node.cloneNode(true); const clone = <Element>node.cloneNode(true);
const slotNodes = clone.querySelectorAll("[t-set]"); const slotNodes = clone.querySelectorAll("[t-set]");
const slotId = qweb.nextSlotId++; const slotId = qweb.nextSlotId++;
ctx.addLine(`w${widgetID}.__owl__.slotId = ${slotId};`); ctx.addLine(`w${componentID}.__owl__.slotId = ${slotId};`);
if (slotNodes.length) { if (slotNodes.length) {
for (let i = 0, length = slotNodes.length; i < length; i++) { for (let i = 0, length = slotNodes.length; i < length; i++) {
const slotNode = slotNodes[i]; const slotNode = slotNodes[i];
slotNode.parentElement!.removeChild(slotNode); slotNode.parentElement!.removeChild(slotNode);
const key = slotNode.getAttribute("t-set")!; const key = slotNode.getAttribute("t-set")!;
slotNode.removeAttribute("t-set"); slotNode.removeAttribute("t-set");
const slotFn = qweb._compile(`slot_${key}_template`, slotNode); const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx);
qweb.slots[`${slotId}_${key}`] = slotFn.bind(qweb); qweb.slots[`${slotId}_${key}`] = slotFn.bind(qweb);
} }
} }
if (clone.childElementCount) { if (clone.childNodes.length) {
const content = clone.children[0]; const t = clone.ownerDocument!.createElement("t");
const slotFn = qweb._compile(`slot_default_template`, content); for (let child of Object.values(clone.childNodes)) {
t.appendChild(child);
}
const slotFn = qweb._compile(`slot_default_template`, t, ctx);
qweb.slots[`${slotId}_default`] = slotFn.bind(qweb); qweb.slots[`${slotId}_default`] = slotFn.bind(qweb);
} }
} }
ctx.addLine(`def${defID} = w${widgetID}._prepare();`); let scopeVars = "";
if (hasSlots) {
scopeVars += ctx.scopeVars.length ? `Object.assign({}, scope)` : varDefs.length ? `{}` : "";
if (varDefs.length) {
scopeVars += `, {${varDefs.join(",")}}`;
}
}
ctx.addLine(`def${defID} = w${componentID}.__prepare(${scopeVars});`);
// hack: specify empty remove hook to prevent the node from being removed from the DOM // hack: specify empty remove hook to prevent the node from being removed from the DOM
let registerCode = `c${ctx.parentNode}[_${dummyID}_index]=pvnode;`;
if (shouldProxy) {
registerCode = `utils.defineProxy(vn${ctx.rootNode}, pvnode);`;
}
ctx.addLine( ctx.addLine(
`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${widgetID}._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeWidgetCode}}}});c${ `def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${componentID}.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeComponentCode}}}});${registerCode}w${componentID}.__owl__.pvnode = pvnode;});`
ctx.parentNode
}[_${dummyID}_index]=pvnode;w${widgetID}.__owl__.pvnode = pvnode;});`
); );
ctx.addElse(); ctx.addElse();
// need to update widget // need to update component
const patchQueueCode = async ? `patchQueue${widgetID}` : "extra.patchQueue"; const patchQueueCode = async ? `patchQueue${componentID}` : "extra.patchQueue";
if (QWeb.dev) {
ctx.addLine(`utils.validateProps(w${componentID}.constructor, props${componentID})`);
}
ctx.addLine( ctx.addLine(
`def${defID} = def${defID} || w${widgetID}._updateProps(props${widgetID}, extra.forceUpdate, ${patchQueueCode});` `def${defID} = def${defID} || w${componentID}.__updateProps(props${componentID}, extra.forceUpdate, ${patchQueueCode}${scopeVars &&
", " + scopeVars});`
); );
let keepAliveCode = ""; let keepAliveCode = "";
if (keepAlive) { if (keepAlive) {
keepAliveCode = `pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w${widgetID}.el,vn.elm);vn.elm=w${widgetID}.el;w${widgetID}._remount();};`; keepAliveCode = `pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w${componentID}.el,vn.elm);vn.elm=w${componentID}.el;w${componentID}.__remount();};`;
} }
ctx.addLine( ctx.addLine(
`def${defID} = def${defID}.then(()=>{if (w${widgetID}.__owl__.isDestroyed) {return};${ `def${defID} = def${defID}.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};${
tattStyle ? `w${widgetID}.el.style=${tattStyle};` : "" tattStyle ? `w${componentID}.el.style=${tattStyle};` : ""
}${updateClassCode}let pvnode=w${widgetID}.__owl__.pvnode;${keepAliveCode}c${ }let pvnode=w${componentID}.__owl__.pvnode;${keepAliveCode}${registerCode}});`
ctx.parentNode
}[_${dummyID}_index]=pvnode;});`
); );
ctx.closeIf(); ctx.closeIf();
if (classObj) {
ctx.addLine(`w${componentID}.__owl__.classObj=${classObj};`);
}
if (async) { if (async) {
ctx.addLine( ctx.addLine(
`def${defID}.then(w${widgetID}._applyPatchQueue.bind(w${widgetID}, patchQueue${widgetID}));` `def${defID}.then(w${componentID}.__applyPatchQueue.bind(w${componentID}, patchQueue${componentID}));`
); );
} else { } else {
ctx.addLine(`extra.promises.push(def${defID});`); ctx.addLine(`extra.promises.push(def${defID});`);
} }
if ( if (node.hasAttribute("t-if") || node.hasAttribute("t-else") || node.hasAttribute("t-elif")) {
node.hasAttribute("t-if") ||
node.hasAttribute("t-else") ||
node.hasAttribute("t-elif")
) {
ctx.closeIf(); ctx.closeIf();
} }
@@ -616,6 +674,100 @@ QWeb.addDirective({
} }
}); });
//------------------------------------------------------------------------------
// Prop validation helper
//------------------------------------------------------------------------------
/**
* Validate the component props (or next props) against the (static) props
* description. This is potentially an expensive operation: it may needs to
* visit recursively the props and all the children to check if they are valid.
* This is why it is only done in 'dev' mode.
*/
UTILS.validateProps = function(Widget, props: Object) {
const propsDef = (<any>Widget).props;
if (propsDef instanceof Array) {
// list of strings (prop names)
for (let i = 0, l = propsDef.length; i < l; i++) {
const propName = propsDef[i];
if (propName[propName.length - 1] === "?") {
// optional prop
break;
}
if (!props[propName]) {
throw new Error(`Missing props '${propsDef[i]}' (component '${Widget.name}')`);
}
}
for (let key in props) {
if (!propsDef.includes(key) && !propsDef.includes(key + "?")) {
throw new Error(`Unknown prop '${key}' given to component '${Widget.name}'`);
}
}
} else if (propsDef) {
// propsDef is an object now
for (let propName in propsDef) {
if (props[propName] === undefined) {
if (propsDef[propName] && !propsDef[propName].optional) {
throw new Error(`Missing props '${propName}' (component '${Widget.name}')`);
} else {
break;
}
}
let isValid = isValidProp(props[propName], propsDef[propName]);
if (!isValid) {
throw new Error(`Props '${propName}' of invalid type in component '${Widget.name}'`);
}
}
for (let propName in props) {
if (!(propName in propsDef)) {
throw new Error(`Unknown prop '${propName}' given to component '${Widget.name}'`);
}
}
}
};
/**
* Check if an invidual prop value matches its (static) prop definition
*/
function isValidProp(prop, propDef): boolean {
if (propDef === true) {
return true;
}
if (typeof propDef === "function") {
// Check if a value is constructed by some Constructor. Note that there is a
// slight abuse of language: we want to consider primitive values as well.
//
// So, even though 1 is not an instance of Number, we want to consider that
// it is valid.
if (typeof prop === "object") {
return prop instanceof propDef;
}
return typeof prop === propDef.name.toLowerCase();
} else if (propDef instanceof Array) {
// If this code is executed, this means that we want to check if a prop
// matches at least one of its descriptor.
let result = false;
for (let i = 0, iLen = propDef.length; i < iLen; i++) {
result = result || isValidProp(prop, propDef[i]);
}
return result;
}
// propsDef is an object
let result = isValidProp(prop, propDef.type);
if (propDef.type === Array) {
for (let i = 0, iLen = prop.length; i < iLen; i++) {
result = result && isValidProp(prop[i], propDef.element);
}
}
if (propDef.type === Object) {
const shape = propDef.shape;
for (let key in shape) {
result = result && isValidProp(prop[key], shape[key]);
}
}
return result;
}
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// t-mounted // t-mounted
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -648,10 +800,7 @@ QWeb.addDirective({
`extra.mountedHandlers[${nodeID}] = extra.mountedHandlers[${nodeID}] || (context['${handler}'] || ${error}).bind(owner);` `extra.mountedHandlers[${nodeID}] = extra.mountedHandlers[${nodeID}] || (context['${handler}'] || ${error}).bind(owner);`
); );
} }
addNodeHook( addNodeHook("insert", `if (context.__owl__.isMounted) { extra.mountedHandlers[${nodeID}](); }`);
"insert",
`if (context.__owl__.isMounted) { extra.mountedHandlers[${nodeID}](); }`
);
} }
}); });
@@ -663,12 +812,15 @@ QWeb.addDirective({
priority: 80, priority: 80,
atNodeEncounter({ ctx, value }): boolean { atNodeEncounter({ ctx, value }): boolean {
const slotKey = ctx.generateID(); const slotKey = ctx.generateID();
ctx.rootContext.shouldDefineOwner = true;
ctx.addLine(`const slot${slotKey} = this.slots[context.__owl__.slotId + '_' + '${value}'];`);
ctx.addIf(`slot${slotKey}`);
ctx.addLine( ctx.addLine(
`const slot${slotKey} = this.slots[context.__owl__.slotId + '_' + '${value}'];` `slot${slotKey}(context.__owl__.parent, Object.assign({}, extra, {parentNode: c${
); ctx.parentNode
ctx.addLine( }, vars: extra.vars, parent: owner}));`
`c${ctx.parentNode}.push(slot${slotKey}(context.__owl__.parent, extra));`
); );
ctx.closeIf();
return true; return true;
} }
}); });
@@ -697,9 +849,7 @@ QWeb.addDirective({
handler = `(ev) => {context.state['${value}'] = ev.target.checked}`; handler = `(ev) => {context.state['${value}'] = ev.target.checked}`;
} else if (type === "radio") { } else if (type === "radio") {
const nodeValue = node.getAttribute("value")!; const nodeValue = node.getAttribute("value")!;
ctx.addLine( ctx.addLine(`p${nodeID}.props = {checked:context.state['${value}'] === '${nodeValue}'};`);
`p${nodeID}.props = {checked:context.state['${value}'] === '${nodeValue}'};`
);
handler = `(ev) => {context.state['${value}'] = ev.target.value}`; handler = `(ev) => {context.state['${value}'] = ev.target.value}`;
event = "click"; event = "click";
} else { } else {
@@ -715,8 +865,6 @@ QWeb.addDirective({
ctx.addLine( ctx.addLine(
`extra.handlers['${event}' + ${nodeID}] = extra.handlers['${event}' + ${nodeID}] || (${handler});` `extra.handlers['${event}' + ${nodeID}] = extra.handlers['${event}' + ${nodeID}] || (${handler});`
); );
ctx.addLine( ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers['${event}' + ${nodeID}];`);
`p${nodeID}.on['${event}'] = extra.handlers['${event}' + ${nodeID}];`
);
} }
}); });
+147 -139
View File
@@ -7,7 +7,7 @@ import { Observer } from "./observer";
* *
* We have here: * We have here:
* - a Store class * - a Store class
* - a connect function * - the ConnectedComponent class
* *
* The Owl store is our answer to the problem of managing complex state across * The Owl store is our answer to the problem of managing complex state across
* components. The main idea is that the store owns some state, allow external * components. The main idea is that the store owns some state, allow external
@@ -21,8 +21,8 @@ import { Observer } from "./observer";
// Store Definition // Store Definition
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
type Mutation = ({ state, commit, getters }, payload: any) => void; type Mutation = ({ state, commit, getters }, ...payload: any) => void;
type Action = ({ commit, state, dispatch, env, getters }, payload: any) => void; type Action = ({ commit, state, dispatch, env, getters }, ...payload: any) => void;
type Getter = ({ state, getters }, payload) => any; type Getter = ({ state, getters }, payload) => any;
interface StoreConfig { interface StoreConfig {
@@ -48,6 +48,7 @@ export class Store extends EventBus {
observer: Observer; observer: Observer;
getters: { [name: string]: (payload?) => any }; getters: { [name: string]: (payload?) => any };
_gettersCache: { [name: string]: {} }; _gettersCache: { [name: string]: {} };
_updateId: number = 1;
constructor(config: StoreConfig, options: StoreOption = {}) { constructor(config: StoreConfig, options: StoreOption = {}) {
super(); super();
@@ -57,10 +58,7 @@ export class Store extends EventBus {
this.mutations = config.mutations; this.mutations = config.mutations;
this.env = config.env; this.env = config.env;
this.observer = new Observer(); this.observer = new Observer();
this.observer.notifyCB = () => { this.observer.notifyCB = this.__notifyComponents.bind(this);
this._gettersCache = {};
this.trigger("update");
};
this.observer.allowMutations = false; this.observer.allowMutations = false;
this.observer.observe(this.state); this.observer.observe(this.state);
this.getters = {}; this.getters = {};
@@ -87,7 +85,7 @@ export class Store extends EventBus {
} }
} }
dispatch(action: string, payload?: any): Promise<void> | void { dispatch(action: string, ...payload: any): Promise<void> | void {
if (!this.actions[action]) { if (!this.actions[action]) {
throw new Error(`[Error] action ${action} is undefined`); throw new Error(`[Error] action ${action} is undefined`);
} }
@@ -99,7 +97,7 @@ export class Store extends EventBus {
state: this.state, state: this.state,
getters: this.getters getters: this.getters
}, },
payload ...payload
); );
if (result instanceof Promise) { if (result instanceof Promise) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -109,7 +107,7 @@ export class Store extends EventBus {
} }
} }
commit(type: string, payload?: any): any { commit(type: string, ...payload: any): any {
if (!this.mutations[type]) { if (!this.mutations[type]) {
throw new Error(`[Error] mutation ${type} is undefined`); throw new Error(`[Error] mutation ${type} is undefined`);
} }
@@ -123,7 +121,7 @@ export class Store extends EventBus {
state: this.state, state: this.state,
getters: this.getters getters: this.getters
}, },
payload ...payload
); );
if (this._commitLevel === 1) { if (this._commitLevel === 1) {
@@ -132,13 +130,41 @@ export class Store extends EventBus {
this.history.push({ this.history.push({
state: this.state, state: this.state,
mutation: type, mutation: type,
payload: payload payload: [...payload]
}); });
} }
} }
this._commitLevel--; this._commitLevel--;
return res; return res;
} }
/**
* Instead of using trigger to emit an update event, we actually implement
* our own function to do that. The reason is that we need to be smarter than
* a simple trigger function: we need to wait for parent components to be
* done before doing children components. The reason is that if an update
* as an effect of destroying a children, we do not want to call the
* mapStoreToProps function of the child, nor rendering it.
*
* This method is not optimal if we have a bunch of asynchronous components:
* we wait sequentially for each component to be completed before updating the
* next. However, the only things that matters is that children are updated
* after their parents. So, this could be optimized by being smarter, and
* updating all widgets concurrently, except for parents/children.
*/
async __notifyComponents() {
this._updateId++;
const current = this._updateId;
this._gettersCache = {};
const subs = this.subscriptions.update || [];
for (let i = 0, iLen = subs.length; i < iLen; i++) {
const sub = subs[i];
const shouldCallback = sub.owner ? sub.owner.__owl__.isMounted : true;
if (shouldCallback) {
await sub.callback.call(sub.owner, current);
}
}
}
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -159,140 +185,122 @@ function deepRevNumber<T extends Object>(o: T): number {
return 0; return 0;
} }
type Constructor<T> = new (...args: any[]) => T;
interface EnvWithStore extends Env {
store: Store;
}
type HashFunction = (a: any, b: any) => number; type HashFunction = (a: any, b: any) => number;
interface StoreOptions {
getStore?(Env): Store;
hashFunction?: HashFunction;
deep?: boolean;
}
let nextID = 1; export class ConnectedComponent<T extends Env, P, S> extends Component<T, P, S> {
deep: boolean = true;
export function connect<E extends EnvWithStore, P, S>( getStore(env) {
Comp: Constructor<Component<E, P, S>>, return env.store;
mapStoreToProps,
options: StoreOptions = <StoreOptions>{}
) {
let hashFunction = options.hashFunction || null;
const getStore = options.getStore || (env => env.store);
if (!hashFunction) {
let deep = "deep" in options ? options.deep : true;
let defaultRevFunction = deep ? deepRevNumber : revNumber;
hashFunction = function({ storeProps }, options) {
const { currentStoreProps } = options;
if ("__owl__" in storeProps) {
return defaultRevFunction(storeProps);
}
let hash = 0;
for (let key in storeProps) {
const val = storeProps[key];
const hashVal = defaultRevFunction(val);
if (hashVal === 0) {
if (val !== currentStoreProps[key]) {
options.didChange = true;
}
} else {
hash += hashVal;
}
}
return hash;
};
} }
const Result = class extends Comp { hashFunction: HashFunction = ({ storeProps }, options) => {
constructor(parent, props?: any) { let refFunction = this.deep ? deepRevNumber : revNumber;
const env = parent instanceof Component ? parent.env : parent; if ("__owl__" in storeProps) {
const store = getStore(env); return refFunction(storeProps);
const ownProps = Object.assign({}, props || {}); }
const storeProps = mapStoreToProps(store.state, ownProps, store.getters); const { currentStoreProps } = options;
const mergedProps = Object.assign({}, props || {}, storeProps); let hash = 0;
super(parent, mergedProps); for (let key in storeProps) {
(<any>this.__owl__).ownProps = ownProps; const val = storeProps[key];
(<any>this.__owl__).currentStoreProps = storeProps; const hashVal = refFunction(val);
(<any>this.__owl__).store = store; if (hashVal === 0) {
(<any>this.__owl__).storeHash = (<HashFunction>hashFunction)( if (val !== currentStoreProps[key]) {
{ options.didChange = true;
state: store.state,
storeProps: storeProps,
revNumber,
deepRevNumber
},
{
currentStoreProps: storeProps
} }
); } else {
} hash += hashVal;
/**
* We do not use the mounted hook here for a subtle reason: we want the
* updates to be called for the parents before the children. However,
* if we use the mounted hook, this will be done in the reverse order.
*/
_callMounted() {
(<any>this.__owl__).store.on("update", this, this._checkUpdate);
super._callMounted();
}
willUnmount() {
(<any>this.__owl__).store.off("update", this);
super.willUnmount();
}
_checkUpdate() {
const ownProps = (<any>this.__owl__).ownProps;
const storeProps = mapStoreToProps(
(<any>this.__owl__).store.state,
ownProps,
(<any>this.__owl__).store.getters
);
const options: any = {
currentStoreProps: (<any>this.__owl__).currentStoreProps
};
const storeHash = (<HashFunction>hashFunction)(
{
state: (<any>this.__owl__).store.state,
storeProps: storeProps,
revNumber,
deepRevNumber
},
options
);
let didChange = options.didChange;
if (storeHash !== (<any>this.__owl__).storeHash) {
didChange = true;
(<any>this.__owl__).storeHash = storeHash;
}
if (didChange) {
(<any>this.__owl__).currentStoreProps = storeProps;
this._updateProps(ownProps, false);
} }
} }
_updateProps(nextProps, forceUpdate, patchQueue?: any[]) { return hash;
if ((<any>this.__owl__).ownProps !== nextProps) {
(<any>this.__owl__).currentStoreProps = mapStoreToProps(
(<any>this.__owl__).store.state,
nextProps,
(<any>this.__owl__).store.getters
);
}
(<any>this.__owl__).ownProps = nextProps;
const mergedProps = Object.assign(
{},
nextProps,
(<any>this.__owl__).currentStoreProps
);
return super._updateProps(mergedProps, forceUpdate, patchQueue);
}
}; };
// we assign here a unique name to the resulting anonymous class. static mapStoreToProps(storeState, ownProps, getters) {
// this is necessary for Owl to be able to properly deduce templates. return {};
// Otherwise, all connected components would have the same name, and then }
// each component after the first will necessarily have the same template. constructor(parent, props?: any) {
let name = `ConnectedComponent${nextID++}`; super(parent, props);
Object.defineProperty(Result, "name", { value: name }); const store = this.getStore(this.env);
return Result; const ownProps = Object.assign({}, props || {});
const storeProps = (<any>this.constructor).mapStoreToProps(
store.state,
ownProps,
store.getters
);
const mergedProps = Object.assign({}, props || {}, storeProps);
this.props = mergedProps;
(<any>this.__owl__).ownProps = ownProps;
(<any>this.__owl__).currentStoreProps = storeProps;
(<any>this.__owl__).store = store;
(<any>this.__owl__).storeHash = this.hashFunction(
{
state: store.state,
storeProps: storeProps,
revNumber,
deepRevNumber
},
{
currentStoreProps: storeProps
}
);
}
/**
* We do not use the mounted hook here for a subtle reason: we want the
* updates to be called for the parents before the children. However,
* if we use the mounted hook, this will be done in the reverse order.
*/
__callMounted() {
(<any>this.__owl__).store.on("update", this, this.__checkUpdate);
super.__callMounted();
}
willUnmount() {
(<any>this.__owl__).store.off("update", this);
super.willUnmount();
}
async __checkUpdate(updateId) {
if (updateId === (<any>this.__owl__).currentUpdateId) {
return;
}
const ownProps = (<any>this.__owl__).ownProps;
const storeProps = (<any>this.constructor).mapStoreToProps(
(<any>this.__owl__).store.state,
ownProps,
(<any>this.__owl__).store.getters
);
const options: any = {
currentStoreProps: (<any>this.__owl__).currentStoreProps
};
const storeHash = this.hashFunction(
{
state: (<any>this.__owl__).store.state,
storeProps: storeProps,
revNumber,
deepRevNumber
},
options
);
let didChange = options.didChange;
if (storeHash !== (<any>this.__owl__).storeHash) {
didChange = true;
(<any>this.__owl__).storeHash = storeHash;
}
if (didChange) {
(<any>this.__owl__).currentStoreProps = storeProps;
await this.__updateProps(ownProps, false);
}
}
__updateProps(nextProps, forceUpdate, patchQueue?: any[]) {
const __owl__ = <any>this.__owl__;
__owl__.currentUpdateId = __owl__.store._updateId;
if (__owl__.ownProps !== nextProps) {
__owl__.currentStoreProps = (<any>this.constructor).mapStoreToProps(
__owl__.store.state,
nextProps,
__owl__.store.getters
);
}
__owl__.ownProps = nextProps;
const mergedProps = Object.assign({}, nextProps, __owl__.currentStoreProps);
return super.__updateProps(mergedProps, forceUpdate, patchQueue);
}
} }
+2 -6
View File
@@ -17,7 +17,7 @@ export function whenReady(fn) {
} else { } else {
document.addEventListener("DOMContentLoaded", resolve, false); document.addEventListener("DOMContentLoaded", resolve, false);
} }
}).then(fn || function () {}); }).then(fn || function() {});
} }
const loadedScripts: { [key: string]: Promise<void> } = {}; const loadedScripts: { [key: string]: Promise<void> } = {};
@@ -76,11 +76,7 @@ export function escape(str: string | number | undefined): string {
* *
* Inspired by https://davidwalsh.name/javascript-debounce-function * Inspired by https://davidwalsh.name/javascript-debounce-function
*/ */
export function debounce( export function debounce(func: Function, wait: number, immediate?: boolean): Function {
func: Function,
wait: number,
immediate?: boolean
): Function {
let timeout; let timeout;
return function(this: any) { return function(this: any) {
const context = this; const context = this;
+61 -114
View File
@@ -59,7 +59,7 @@ function vnode(
elm: Element | Text | undefined elm: Element | Text | undefined
): VNode { ): VNode {
let key = data === undefined ? undefined : data.key; let key = data === undefined ? undefined : data.key;
return {sel, data, children, text, elm, key}; return { sel, data, children, text, elm, key };
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -109,14 +109,7 @@ function createKeyToOldIdx(
return map; return map;
} }
const hooks: (keyof Module)[] = [ const hooks: (keyof Module)[] = ["create", "update", "remove", "destroy", "pre", "post"];
"create",
"update",
"remove",
"destroy",
"pre",
"post"
];
export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) { export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
let i: number, let i: number,
@@ -138,13 +131,7 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
function emptyNodeAt(elm: Element) { function emptyNodeAt(elm: Element) {
const id = elm.id ? "#" + elm.id : ""; const id = elm.id ? "#" + elm.id : "";
const c = elm.className ? "." + elm.className.split(" ").join(".") : ""; const c = elm.className ? "." + elm.className.split(" ").join(".") : "";
return vnode( return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm);
api.tagName(elm).toLowerCase() + id + c,
{},
[],
undefined,
elm
);
} }
function createRmCb(childElm: Node, listeners: number) { function createRmCb(childElm: Node, listeners: number) {
@@ -179,19 +166,14 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
const dotIdx = sel.indexOf(".", hashIdx); const dotIdx = sel.indexOf(".", hashIdx);
const hash = hashIdx > 0 ? hashIdx : sel.length; const hash = hashIdx > 0 ? hashIdx : sel.length;
const dot = dotIdx > 0 ? dotIdx : sel.length; const dot = dotIdx > 0 ? dotIdx : sel.length;
const tag = const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel;
hashIdx !== -1 || dotIdx !== -1
? sel.slice(0, Math.min(hash, dot))
: sel;
const elm = (vnode.elm = const elm = (vnode.elm =
isDef(data) && isDef((i = (data as VNodeData).ns)) isDef(data) && isDef((i = (data as VNodeData).ns))
? api.createElementNS(i, tag) ? api.createElementNS(i, tag)
: api.createElement(tag)); : api.createElement(tag));
if (hash < dot) elm.setAttribute("id", sel.slice(hash + 1, dot)); if (hash < dot) elm.setAttribute("id", sel.slice(hash + 1, dot));
if (dotIdx > 0) if (dotIdx > 0) elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " "));
elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " ")); for (i = 0, iLen = cbs.create.length; i < iLen; ++i) cbs.create[i](emptyNode, vnode);
for (i = 0, iLen = cbs.create.length; i < iLen; ++i)
cbs.create[i](emptyNode, vnode);
if (array(children)) { if (array(children)) {
for (i = 0, iLen = children.length; i < iLen; ++i) { for (i = 0, iLen = children.length; i < iLen; ++i) {
const ch = children[i]; const ch = children[i];
@@ -237,8 +219,7 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
data = vnode.data; data = vnode.data;
if (data !== undefined) { if (data !== undefined) {
if (isDef((i = data.hook)) && isDef((i = i.destroy))) i(vnode); if (isDef((i = data.hook)) && isDef((i = i.destroy))) i(vnode);
for (i = 0, iLen = cbs.destroy.length; i < iLen; ++i) for (i = 0, iLen = cbs.destroy.length; i < iLen; ++i) cbs.destroy[i](vnode);
cbs.destroy[i](vnode);
if (vnode.children !== undefined) { if (vnode.children !== undefined) {
for (j = 0, jLen = vnode.children.length; j < jLen; ++j) { for (j = 0, jLen = vnode.children.length; j < jLen; ++j) {
i = vnode.children[j]; i = vnode.children[j];
@@ -267,13 +248,8 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
invokeDestroyHook(ch); invokeDestroyHook(ch);
listeners = cbs.remove.length + 1; listeners = cbs.remove.length + 1;
rm = createRmCb(ch.elm as Node, listeners); rm = createRmCb(ch.elm as Node, listeners);
for (i = 0, iLen = cbs.remove.length; i < iLen; ++i) for (i = 0, iLen = cbs.remove.length; i < iLen; ++i) cbs.remove[i](ch, rm);
cbs.remove[i](ch, rm); if (isDef((i = ch.data)) && isDef((i = i.hook)) && isDef((i = i.remove))) {
if (
isDef((i = ch.data)) &&
isDef((i = i.hook)) &&
isDef((i = i.remove))
) {
i(ch, rm); i(ch, rm);
} else { } else {
rm(); rm();
@@ -335,11 +311,7 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
} else if (sameVnode(oldEndVnode, newStartVnode)) { } else if (sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
api.insertBefore( api.insertBefore(parentElm, oldEndVnode.elm as Node, oldStartVnode.elm as Node);
parentElm,
oldEndVnode.elm as Node,
oldStartVnode.elm as Node
);
oldEndVnode = oldCh[--oldEndIdx]; oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx]; newStartVnode = newCh[++newStartIdx];
} else { } else {
@@ -366,11 +338,7 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
} else { } else {
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue); patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
oldCh[idxInOld] = undefined as any; oldCh[idxInOld] = undefined as any;
api.insertBefore( api.insertBefore(parentElm, elmToMove.elm as Node, oldStartVnode.elm as Node);
parentElm,
elmToMove.elm as Node,
oldStartVnode.elm as Node
);
} }
newStartVnode = newCh[++newStartIdx]; newStartVnode = newCh[++newStartIdx];
} }
@@ -379,31 +347,16 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) { if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
if (oldStartIdx > oldEndIdx) { if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm; before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
addVnodes( addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
parentElm,
before,
newCh,
newStartIdx,
newEndIdx,
insertedVnodeQueue
);
} else { } else {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
} }
} }
} }
function patchVnode( function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
oldVnode: VNode,
vnode: VNode,
insertedVnodeQueue: VNodeQueue
) {
let i: any, iLen: number, hook: any; let i: any, iLen: number, hook: any;
if ( if (isDef((i = vnode.data)) && isDef((hook = i.hook)) && isDef((i = hook.prepatch))) {
isDef((i = vnode.data)) &&
isDef((hook = i.hook)) &&
isDef((i = hook.prepatch))
) {
i(oldVnode, vnode); i(oldVnode, vnode);
} }
const elm = (vnode.elm = oldVnode.elm as Node); const elm = (vnode.elm = oldVnode.elm as Node);
@@ -411,20 +364,14 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
let ch = vnode.children; let ch = vnode.children;
if (oldVnode === vnode) return; if (oldVnode === vnode) return;
if (vnode.data !== undefined) { if (vnode.data !== undefined) {
for (i = 0, iLen = cbs.update.length; i < iLen; ++i) for (i = 0, iLen = cbs.update.length; i < iLen; ++i) cbs.update[i](oldVnode, vnode);
cbs.update[i](oldVnode, vnode);
i = vnode.data.hook; i = vnode.data.hook;
if (isDef(i) && isDef((i = i.update))) i(oldVnode, vnode); if (isDef(i) && isDef((i = i.update))) i(oldVnode, vnode);
} }
if (isUndef(vnode.text)) { if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) { if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) if (oldCh !== ch)
updateChildren( updateChildren(elm, oldCh as Array<VNode>, ch as Array<VNode>, insertedVnodeQueue);
elm,
oldCh as Array<VNode>,
ch as Array<VNode>,
insertedVnodeQueue
);
} else if (isDef(ch)) { } else if (isDef(ch)) {
if (isDef(oldVnode.text)) api.setTextContent(elm, ""); if (isDef(oldVnode.text)) api.setTextContent(elm, "");
addVnodes( addVnodes(
@@ -436,23 +383,13 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
insertedVnodeQueue insertedVnodeQueue
); );
} else if (isDef(oldCh)) { } else if (isDef(oldCh)) {
removeVnodes( removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
elm,
oldCh as Array<VNode>,
0,
(oldCh as Array<VNode>).length - 1
);
} else if (isDef(oldVnode.text)) { } else if (isDef(oldVnode.text)) {
api.setTextContent(elm, ""); api.setTextContent(elm, "");
} }
} else if (oldVnode.text !== vnode.text) { } else if (oldVnode.text !== vnode.text) {
if (isDef(oldCh)) { if (isDef(oldCh)) {
removeVnodes( removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
elm,
oldCh as Array<VNode>,
0,
(oldCh as Array<VNode>).length - 1
);
} }
api.setTextContent(elm, vnode.text as string); api.setTextContent(elm, vnode.text as string);
} }
@@ -510,11 +447,7 @@ interface DOMAPI {
createElementNS: (namespaceURI: string, qualifiedName: string) => Element; createElementNS: (namespaceURI: string, qualifiedName: string) => Element;
createTextNode: (text: string) => Text; createTextNode: (text: string) => Text;
createComment: (text: string) => Comment; createComment: (text: string) => Comment;
insertBefore: ( insertBefore: (parentNode: Node, newNode: Node, referenceNode: Node | null) => void;
parentNode: Node,
newNode: Node,
referenceNode: Node | null
) => void;
removeChild: (node: Node, child: Node) => void; removeChild: (node: Node, child: Node) => void;
appendChild: (node: Node, child: Node) => void; appendChild: (node: Node, child: Node) => void;
parentNode: (node: Node) => Node; parentNode: (node: Node) => Node;
@@ -543,11 +476,7 @@ function createComment(text: string): Comment {
return document.createComment(text); return document.createComment(text);
} }
function insertBefore( function insertBefore(parentNode: Node, newNode: Node, referenceNode: Node | null): void {
parentNode: Node,
newNode: Node,
referenceNode: Node | null
): void {
parentNode.insertBefore(newNode, referenceNode); parentNode.insertBefore(newNode, referenceNode);
} }
@@ -644,21 +573,13 @@ type VNodeChildElement = VNode | string | number | undefined | null;
type ArrayOrElement<T> = T | T[]; type ArrayOrElement<T> = T | T[];
type VNodeChildren = ArrayOrElement<VNodeChildElement>; type VNodeChildren = ArrayOrElement<VNodeChildElement>;
function addNS( function addNS(data: any, children: VNodes | undefined, sel: string | undefined): void {
data: any,
children: VNodes | undefined,
sel: string | undefined
): void {
data.ns = "http://www.w3.org/2000/svg"; data.ns = "http://www.w3.org/2000/svg";
if (sel !== "foreignObject" && children !== undefined) { if (sel !== "foreignObject" && children !== undefined) {
for (let i = 0, iLen = children.length; i < iLen; ++i) { for (let i = 0, iLen = children.length; i < iLen; ++i) {
let childData = children[i].data; let childData = children[i].data;
if (childData !== undefined) { if (childData !== undefined) {
addNS( addNS(childData, (children[i] as VNode).children as VNodes, children[i].sel);
childData,
(children[i] as VNode).children as VNodes,
children[i].sel
);
} }
} }
} }
@@ -697,13 +618,7 @@ export function h(sel: any, b?: any, c?: any): VNode {
if (children !== undefined) { if (children !== undefined) {
for (i = 0, iLen = children.length; i < iLen; ++i) { for (i = 0, iLen = children.length; i < iLen; ++i) {
if (primitive(children[i])) if (primitive(children[i]))
children[i] = vnode( children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
undefined,
undefined,
undefined,
children[i],
undefined
);
} }
} }
if ( if (
@@ -769,9 +684,7 @@ interface Module {
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// module/eventlisteners.ts // module/eventlisteners.ts
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
type On = { type On = { [N in keyof HTMLElementEventMap]?: (ev: HTMLElementEventMap[N]) => void } & {
[N in keyof HTMLElementEventMap]?: (ev: HTMLElementEventMap[N]) => void
} & {
[event: string]: EventListener; [event: string]: EventListener;
}; };
@@ -850,8 +763,7 @@ function updateEventListeners(oldVnode: VNode, vnode?: VNode): void {
// add new listeners which has not already attached // add new listeners which has not already attached
if (on) { if (on) {
// reuse existing listener or create new // reuse existing listener or create new
var listener = ((vnode as any).listener = var listener = ((vnode as any).listener = (oldVnode as any).listener || createListener());
(oldVnode as any).listener || createListener());
// update vnode for listener // update vnode for listener
listener.vnode = vnode; listener.vnode = vnode;
@@ -938,4 +850,39 @@ export const attrsModule = {
update: updateAttrs update: updateAttrs
} as Module; } as Module;
export const patch = init([eventListenersModule, attrsModule, propsModule]); //------------------------------------------------------------------------------
// class.ts
//------------------------------------------------------------------------------
function updateClass(oldVnode: VNode, vnode: VNode): void {
var cur: any,
name: string,
elm: Element,
oldClass = (oldVnode.data as VNodeData).class,
klass = (vnode.data as VNodeData).class;
if (!oldClass && !klass) return;
if (oldClass === klass) return;
oldClass = oldClass || {};
klass = klass || {};
elm = vnode.elm as Element;
for (name in oldClass) {
if (!klass[name]) {
elm.classList.remove(name);
}
}
for (name in klass) {
cur = klass[name];
if (cur !== oldClass[name]) {
(elm.classList as any)[cur ? "add" : "remove"](name);
}
}
}
const classModule = { create: updateClass, update: updateClass } as Module;
//------------------------------------------------------------------------------
// patch
//------------------------------------------------------------------------------
export const patch = init([eventListenersModule, attrsModule, propsModule, classModule]);
+24 -22
View File
@@ -1,17 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`animations t-transition combined with t-widget 1`] = ` exports[`animations t-transition combined with component 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
let utils = this.utils; let utils = this.utils;
let QWeb = this.constructor; let QWeb = this.constructor;
let parent = context;
let owner = context; let owner = context;
var h = this.utils.h; var h = this.utils.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
//WIDGET //COMPONENT
let def3; let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false; let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
let _2_index = c1.length; let _2_index = c1.length;
c1.push(null); c1.push(null);
let props4 = {}; let props4 = {};
@@ -24,18 +25,18 @@ exports[`animations t-transition combined with t-widget 1`] = `
} }
} }
if (!w4) { if (!w4) {
let widgetKey4 = \`Child\`; let componentKey4 = \`Child\`;
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4]; let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')} if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(owner, props4); w4 = new W4(parent, props4);
context.__owl__.cmap[4] = w4.__owl__.id; parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4._prepare(); def3 = w4.__prepare();
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => { def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
w4.destroy(); w4.destroy();
}; };
utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;}); utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else { } else {
def3 = 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;}); def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
} }
extra.promises.push(def3); extra.promises.push(def3);
@@ -43,19 +44,20 @@ exports[`animations t-transition combined with t-widget 1`] = `
}" }"
`; `;
exports[`animations t-transition combined with t-widget and t-if 1`] = ` exports[`animations t-transition combined with t-component and t-if 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
let utils = this.utils; let utils = this.utils;
let QWeb = this.constructor; let QWeb = this.constructor;
let parent = context;
let owner = context; let owner = context;
var h = this.utils.h; var h = this.utils.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
if (context['state'].display) { if (context['state'].display) {
//WIDGET //COMPONENT
let def3; let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false; let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
let _2_index = c1.length; let _2_index = c1.length;
c1.push(null); c1.push(null);
let props4 = {}; let props4 = {};
@@ -68,18 +70,18 @@ exports[`animations t-transition combined with t-widget and t-if 1`] = `
} }
} }
if (!w4) { if (!w4) {
let widgetKey4 = \`Child\`; let componentKey4 = \`Child\`;
let W4 = context.widgets && context.widgets[widgetKey4] || QWeb.widgets[widgetKey4]; let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
if (!W4) {throw new Error('Cannot find the definition of widget \\"' + widgetKey4 + '\\"')} if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(owner, props4); w4 = new W4(parent, props4);
context.__owl__.cmap[4] = w4.__owl__.id; parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4._prepare(); def3 = w4.__prepare();
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => { def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
w4.destroy(); w4.destroy();
}; };
utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;}); utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else { } else {
def3 = 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;}); def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
} }
extra.promises.push(def3); extra.promises.push(def3);
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,44 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`props validation props are validated in dev mode (code snapshot) 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
//COMPONENT
let def3;
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {message:1};
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 componentKey4 = \`Child\`;
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
utils.validateProps(W4, props4)
w4 = new W4(parent, props4);
parent.__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 {
utils.validateProps(w4.constructor, props4)
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;
}"
`;
+14 -16
View File
@@ -128,8 +128,8 @@ exports[`attributes from object variables set previously 1`] = `
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = {a:'b'}; var _2 = {a:'b'};
var _3 = _2.a; let _3 = this.utils.toObj(_2.a);
let c4 = [], p4 = {key:4,attrs:{class: _3}}; let c4 = [], p4 = {key:4,class:_3};
var vn4 = h('span', p4, c4); var vn4 = h('span', p4, c4);
c1.push(vn4); c1.push(vn4);
return vn1; return vn1;
@@ -143,8 +143,8 @@ exports[`attributes from variables set previously 1`] = `
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = 'def'; var _2 = 'def';
var _3 = _2; let _3 = this.utils.toObj(_2);
let c4 = [], p4 = {key:4,attrs:{class: _3}}; let c4 = [], p4 = {key:4,class:_3};
var vn4 = h('span', p4, c4); var vn4 = h('span', p4, c4);
c1.push(vn4); c1.push(vn4);
return vn1; return vn1;
@@ -209,12 +209,11 @@ exports[`attributes t-att-class and class should combine together 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.utils.h;
var _1 = 'hello'; let _2 = {'hello':true};
var _3 = context['value']; Object.assign(_2, this.utils.toObj(context['value']))
var _2 = 'hello' + (_3 ? ' ' + _3 : ''); let c3 = [], p3 = {key:3,class:_2};
let c4 = [], p4 = {key:4,attrs:{class: _2}}; var vn3 = h('div', p3, c3);
var vn4 = h('div', p4, c4); return vn3;
return vn4;
}" }"
`; `;
@@ -222,12 +221,11 @@ exports[`attributes t-att-class with object 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.utils.h;
var _1 = 'static'; let _2 = {'static':true};
var _3 = this.utils.objectToAttrString({a:context['b'],c:context['d'],e:context['f']}); Object.assign(_2, this.utils.toObj({a:context['b'],c:context['d'],e:context['f']}))
var _2 = 'static' + (_3 ? ' ' + _3 : ''); let c3 = [], p3 = {key:3,class:_2};
let c4 = [], p4 = {key:4,attrs:{class: _2}}; var vn3 = h('div', p3, c3);
var vn4 = h('div', p4, c4); return vn3;
return vn4;
}" }"
`; `;
-33
View File
@@ -1,33 +0,0 @@
function anonymous(context, extra) {
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['state'].display) {
//WIDGET
let _2_index = c1.length;
c1.push(null);
let def3;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
w4.destroy();
w4 = false
}
if (!w4) {
let W4 = context.widgets['Child'];
if (!W4) {throw new Error(`Cannot find the definition of widget "Child"`)}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4._prepare();
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert: (vn) => {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;this.utils.transitionInsert(vn.elm, 'chimay');},remove: () => {},destroy: (vn) => {let finalize = () => {
w4.destroy();
};
this.utils.transitionRemove(vn.elm, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
}
}
+16 -31
View File
@@ -18,7 +18,7 @@ import {
// - fixture: a div, appended to the DOM, intended to be the target of dom // - fixture: a div, appended to the DOM, intended to be the target of dom
// manipulations. Note that it is removed after each test. // manipulations. Note that it is removed after each test.
// - qweb: a new QWeb instance // - qweb: a new QWeb instance
// - env: a WEnv, necessary to create new widgets // - env: a WEnv, necessary to create new components
// - cssEl: a stylesheet injected into the dom // - cssEl: a stylesheet injected into the dom
let fixture: HTMLElement; let fixture: HTMLElement;
@@ -170,16 +170,13 @@ describe("animations", () => {
expect(spanNode.className).toBe(""); expect(spanNode.className).toBe("");
}); });
test("t-transition combined with t-widget", async () => { test("t-transition combined with component", async () => {
expect.assertions(5); expect.assertions(5);
env.qweb.addTemplate( env.qweb.addTemplate("Parent", `<div><Child t-transition="chimay"/></div>`);
"Parent",
`<div><t t-widget="Child" t-transition="chimay"/></div>`
);
env.qweb.addTemplate("Child", `<span>blue</span>`); env.qweb.addTemplate("Child", `<span>blue</span>`);
class Parent extends Widget { class Parent extends Widget {
widgets = { Child: Child }; components = { Child: Child };
} }
class Child extends Widget {} class Child extends Widget {}
const widget = new Parent(env); const widget = new Parent(env);
@@ -209,16 +206,16 @@ describe("animations", () => {
expect(fixture.innerHTML).toBe('<div><span class="">blue</span></div>'); expect(fixture.innerHTML).toBe('<div><span class="">blue</span></div>');
}); });
test("t-transition combined with t-widget and t-if", async () => { test("t-transition combined with t-component and t-if", async () => {
expect.assertions(8); expect.assertions(8);
env.qweb.addTemplate( env.qweb.addTemplate(
"Parent", "Parent",
`<div><t t-if="state.display" t-widget="Child" t-transition="chimay"/></div>` `<div><t t-if="state.display" t-component="Child" t-transition="chimay"/></div>`
); );
env.qweb.addTemplate("Child", `<span>blue</span>`); env.qweb.addTemplate("Child", `<span>blue</span>`);
class Parent extends Widget { class Parent extends Widget {
widgets = { Child: Child }; components = { Child: Child };
state = { display: true }; state = { display: true };
} }
class Child extends Widget {} class Child extends Widget {}
@@ -320,9 +317,7 @@ describe("animations", () => {
expect(spans.length).toBe(1); expect(spans.length).toBe(1);
expect(spans[0].className).toBe(`chimay-${phase} chimay-${phase}-active`); expect(spans[0].className).toBe(`chimay-${phase} chimay-${phase}-active`);
cb(); cb();
expect(spans[0].className).toBe( expect(spans[0].className).toBe(`chimay-${phase}-active chimay-${phase}-to`);
`chimay-${phase}-active chimay-${phase}-to`
);
def.resolve(); def.resolve();
}); });
@@ -330,9 +325,7 @@ describe("animations", () => {
button!.click(); button!.click();
await def; // wait for the mocked repaint to be done await def; // wait for the mocked repaint to be done
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe('<div><button>Toggle</button><span class="">blue</span></div>');
'<div><button>Toggle</button><span class="">blue</span></div>'
);
// click to remove the span, and click again to re-add it before transitionend // click to remove the span, and click again to re-add it before transitionend
def = makeDeferred(); def = makeDeferred();
@@ -346,26 +339,24 @@ describe("animations", () => {
await def; // wait for the mocked repaint to be done await def; // wait for the mocked repaint to be done
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe('<div><button>Toggle</button><span class="">blue</span></div>');
'<div><button>Toggle</button><span class="">blue</span></div>'
);
}); });
test("t-transition combined with t-widget, remove and re-add before transitionend", async () => { test("t-transition combined with t-component, remove and re-add before transitionend", async () => {
expect.assertions(11); expect.assertions(11);
env.qweb.addTemplates( env.qweb.addTemplates(
`<templates> `<templates>
<div t-name="Parent"> <div t-name="Parent">
<button t-on-click="toggle">Toggle</button> <button t-on-click="toggle">Toggle</button>
<t t-if="state.flag" t-widget="Child" t-transition="chimay"/> <t t-if="state.flag" t-component="Child" t-transition="chimay"/>
</div> </div>
<span t-name="Child">blue</span> <span t-name="Child">blue</span>
</templates>` </templates>`
); );
class Child extends Widget {} class Child extends Widget {}
class Parent extends Widget { class Parent extends Widget {
widgets = { Child }; components = { Child };
constructor(parent) { constructor(parent) {
super(parent); super(parent);
this.state = { flag: false }; this.state = { flag: false };
@@ -386,9 +377,7 @@ describe("animations", () => {
expect(spans.length).toBe(1); expect(spans.length).toBe(1);
expect(spans[0].className).toBe(`chimay-${phase} chimay-${phase}-active`); expect(spans[0].className).toBe(`chimay-${phase} chimay-${phase}-active`);
cb(); cb();
expect(spans[0].className).toBe( expect(spans[0].className).toBe(`chimay-${phase}-active chimay-${phase}-to`);
`chimay-${phase}-active chimay-${phase}-to`
);
def.resolve(); def.resolve();
}); });
@@ -396,9 +385,7 @@ describe("animations", () => {
button!.click(); button!.click();
await def; // wait for the mocked repaint to be done await def; // wait for the mocked repaint to be done
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe('<div><button>Toggle</button><span class="">blue</span></div>');
'<div><button>Toggle</button><span class="">blue</span></div>'
);
// click to remove the span, and click again to re-add it before transitionend // click to remove the span, and click again to re-add it before transitionend
def = makeDeferred(); def = makeDeferred();
@@ -412,8 +399,6 @@ describe("animations", () => {
await def; // wait for the mocked repaint to be done await def; // wait for the mocked repaint to be done
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe('<div><button>Toggle</button><span class="">blue</span></div>');
'<div><button>Toggle</button><span class="">blue</span></div>'
);
}); });
}); });
+1185 -437
View File
File diff suppressed because it is too large Load Diff
+4 -9
View File
@@ -52,15 +52,11 @@ interface FileData {
sections: MarkDownSection[]; sections: MarkDownSection[];
} }
function isLinkValid( function isLinkValid(link: MarkDownLink, current: FileData, files: FileData[]): boolean {
link: MarkDownLink,
current: FileData,
files: FileData[]
): boolean {
const parts = link.link.split("#"); const parts = link.link.split("#");
const currentParts = current.name.split("/"); const currentParts = current.name.split("/");
const path = currentParts.length > 1 ? currentParts[0] + '/' : ""; const path = currentParts.length > 1 ? currentParts[0] + "/" : "";
const fullName = path + parts[0]; const fullName = path + parts[0];
if (parts.length === 1) { if (parts.length === 1) {
// no # in url // no # in url
if (parts[0].endsWith(".md")) { if (parts[0].endsWith(".md")) {
@@ -70,8 +66,7 @@ function isLinkValid(
} }
} }
} else { } else {
const file = const file = parts[0] === "" ? current : files.find(f => f.name === fullName);
parts[0] === "" ? current : files.find(f => f.name === fullName);
if (!file) { if (!file) {
return false; return false;
} }
+1 -3
View File
@@ -23,9 +23,7 @@ describe("event bus behaviour", () => {
test("throw error if callback is undefined", () => { test("throw error if callback is undefined", () => {
expect.assertions(1); expect.assertions(1);
const bus = new EventBus(); const bus = new EventBus();
expect(() => bus.on("event", {}, <any>undefined)).toThrow( expect(() => bus.on("event", {}, <any>undefined)).toThrow(`Missing callback`);
`Missing callback`
);
}); });
test("can unsubscribe", () => { test("can unsubscribe", () => {
+16 -10
View File
@@ -68,13 +68,22 @@ export function renderToDOM(
return result.elm as HTMLElement; return result.elm as HTMLElement;
} }
export function renderToString( /**
qweb: QWeb, * Render a template to an html string. The big difference with the
t: string, * renderToString method in QWeb is that we use the renderToDom method, which
context: EvalContext = {} * snapshots the resulting template function. Doing so gives us a large body
): string { * of reference to evaluate changes in the generated QWeb code.
*
* Note that the result of renderToString is guaranteed to be the same as the
* one from QWeb.
*/
export function renderToString(qweb: QWeb, t: string, context: EvalContext = {}): string {
const node = renderToDOM(qweb, t, context); const node = renderToDOM(qweb, t, context);
return node instanceof Text ? node.textContent! : node.outerHTML; const result = node instanceof Text ? node.textContent! : node.outerHTML;
if (result !== qweb.renderToString(t, context)) {
throw new Error("HTML string returned by renderToString helper does not match QWeb render");
}
return result;
} }
// hereafter, we define two helpers to patch/unpatch the nextFrame utils. This // hereafter, we define two helpers to patch/unpatch the nextFrame utils. This
@@ -92,10 +101,7 @@ export function unpatchNextFrame() {
UTILS.nextFrame = nextFrame; UTILS.nextFrame = nextFrame;
} }
export async function editInput( export async function editInput(input: HTMLInputElement | HTMLTextAreaElement, value: string) {
input: HTMLInputElement | HTMLTextAreaElement,
value: string
) {
input.value = value; input.value = value;
input.dispatchEvent(new Event("input")); input.dispatchEvent(new Event("input"));
input.dispatchEvent(new Event("change")); input.dispatchEvent(new Event("change"));
+103 -31
View File
@@ -1,6 +1,6 @@
import { Component, Env } from "../src/component"; import { Component, Env } from "../src/component";
import { makeTestFixture, makeTestEnv } from "./helpers"; import { makeTestFixture, makeTestEnv } from "./helpers";
import { QWeb } from "../src"; import { QWeb, UTILS } from "../src/qweb_core";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Setup and helpers // Setup and helpers
@@ -45,19 +45,6 @@ describe("props validation", () => {
}).not.toThrow(); }).not.toThrow();
}); });
test("props validation is also done on update props", async () => {
expect.assertions(1);
class TestWidget extends Widget {
static props = ["message"];
}
const w = new TestWidget(env, { message: "bottle" });
try {
await w._updateProps({});
} catch (e) {
expect(e.message).toBe("Missing props 'message' (widget 'TestWidget')");
}
});
test("props: list of strings", async () => { test("props: list of strings", async () => {
class TestWidget extends Widget { class TestWidget extends Widget {
static props = ["message"]; static props = ["message"];
@@ -65,7 +52,7 @@ describe("props validation", () => {
expect(() => { expect(() => {
new TestWidget(env); new TestWidget(env);
}).toThrow("Missing props 'message' (widget 'TestWidget')"); }).toThrow("Missing props 'message' (component 'TestWidget')");
}); });
test("validate simple types", async () => { test("validate simple types", async () => {
@@ -93,7 +80,7 @@ describe("props validation", () => {
expect(() => { expect(() => {
new TestWidget(env, { p: test.ko }); new TestWidget(env, { p: test.ko });
}).toThrow("Props 'p' of invalid type in widget"); }).toThrow("Props 'p' of invalid type in component");
} }
}); });
@@ -122,11 +109,10 @@ describe("props validation", () => {
expect(() => { expect(() => {
new TestWidget(env, { p: test.ko }); new TestWidget(env, { p: test.ko });
}).toThrow("Props 'p' of invalid type in widget"); }).toThrow("Props 'p' of invalid type in component");
} }
}); });
test("can validate a prop with multiple types", async () => { test("can validate a prop with multiple types", async () => {
let TestWidget = class extends Widget { let TestWidget = class extends Widget {
static props = { p: [String, Boolean] }; static props = { p: [String, Boolean] };
@@ -139,7 +125,7 @@ describe("props validation", () => {
expect(() => { expect(() => {
new TestWidget(env, { p: 1 }); new TestWidget(env, { p: 1 });
}).toThrow("Props 'p' of invalid type in widget"); }).toThrow("Props 'p' of invalid type in component");
}); });
test("can validate an optional props", async () => { test("can validate an optional props", async () => {
@@ -149,16 +135,14 @@ describe("props validation", () => {
expect(() => { expect(() => {
new TestWidget(env, { p: "hey" }); new TestWidget(env, { p: "hey" });
new TestWidget(env, { }); new TestWidget(env, {});
}).not.toThrow(); }).not.toThrow();
expect(() => { expect(() => {
new TestWidget(env, { p: 1 }); new TestWidget(env, { p: 1 });
}).toThrow(); }).toThrow();
}); });
test("can validate an array with given primitive type", async () => { test("can validate an array with given primitive type", async () => {
let TestWidget = class extends Widget { let TestWidget = class extends Widget {
static props = { p: { type: Array, element: String } }; static props = { p: { type: Array, element: String } };
@@ -222,7 +206,7 @@ describe("props validation", () => {
type: Object, type: Object,
shape: { shape: {
id: Number, id: Number,
url: [Boolean, {type: Array, element: Number}], url: [Boolean, { type: Array, element: Number }]
} }
} }
}; };
@@ -230,21 +214,109 @@ describe("props validation", () => {
expect(() => { expect(() => {
new TestWidget(env, { p: { id: 1, url: true } }); new TestWidget(env, { p: { id: 1, url: true } });
new TestWidget(env, { p: { id: 1, url: [12]} }); new TestWidget(env, { p: { id: 1, url: [12] } });
}).not.toThrow(); }).not.toThrow();
expect(() => { expect(() => {
new TestWidget(env, { p: { id: 1, url: [12, true]} }); new TestWidget(env, { p: { id: 1, url: [12, true] } });
}).toThrow(); }).toThrow();
}); });
});
test("props are validated in dev mode (code snapshot)", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="App">
<Child message="1"/>
</div>
<div t-name="Child"><t t-esc="props.message"/></div>
</templates>`);
class Child extends Widget {
static props = ["message"];
}
class App extends Widget {
components = { Child };
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
// need to make sure there are 2 call to update props. one at component
// creation, and one at update time.
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
});
test("props: list of strings with optional props", async () => {
class TestWidget extends Widget {
static props = ["message", "someProp?"];
}
expect(() => {
UTILS.validateProps(TestWidget, { someProp: 1 });
}).toThrow();
expect(() => {
UTILS.validateProps(TestWidget, { message: 1 });
}).not.toThrow();
});
test("props: can be defined with a boolean", async () => {
class TestWidget extends Widget {
static props = { message: true };
}
expect(() => {
UTILS.validateProps(TestWidget, {});
}).toThrow();
});
test("props: extra props cause an error", async () => {
class TestWidget extends Widget {
static props = ["message"];
}
expect(() => {
UTILS.validateProps(TestWidget, { message: 1, flag: true });
}).toThrow();
});
test("props: extra props cause an error, part 2", async () => {
class TestWidget extends Widget {
static props = {message: true};
}
expect(() => {
UTILS.validateProps(TestWidget, { message: 1, flag: true });
}).toThrow();
});
test("props: optional prop do not cause an error", async () => {
class TestWidget extends Widget {
static props = ["message?"];
}
expect(() => {
UTILS.validateProps(TestWidget, { message: 1});
}).not.toThrow();
});
test("optional prop do not cause an error if value is undefined", async () => {
class TestWidget extends Widget {
static props = {message: {type: String, optional: true}};
}
expect(() => {
UTILS.validateProps(TestWidget, { message: undefined});
}).not.toThrow();
expect(() => {
UTILS.validateProps(TestWidget, { message: null});
}).toThrow();
});
});
describe("default props", () => { describe("default props", () => {
test("can set default values", async () => { test("can set default values", async () => {
class TestWidget extends Widget { class TestWidget extends Widget {
static defaultProps = {p: 4} static defaultProps = { p: 4 };
} }
const w = new TestWidget(env, {}); const w = new TestWidget(env, {});
@@ -253,11 +325,11 @@ describe("default props", () => {
test("default values are also set whenever component is updated", async () => { test("default values are also set whenever component is updated", async () => {
class TestWidget extends Widget { class TestWidget extends Widget {
static defaultProps = {p: 4} static defaultProps = { p: 4 };
} }
const w = new TestWidget(env, {p: 1}); const w = new TestWidget(env, { p: 1 });
await w._updateProps({}); await w.__updateProps({});
expect(w.props.p).toBe(4); expect(w.props.p).toBe(4);
}); });
}); });
+38 -109
View File
@@ -25,13 +25,13 @@ describe("static templates", () => {
}); });
test("simple dynamic value", () => { test("simple dynamic value", () => {
qweb.addTemplate("test", "<t><t t-esc=\"text\"/></t>"); qweb.addTemplate("test", '<t><t t-esc="text"/></t>');
expect(renderToString(qweb, "test", {text: "hello vdom"})).toBe("hello vdom"); expect(renderToString(qweb, "test", { text: "hello vdom" })).toBe("hello vdom");
}); });
test("simple string, with some dynamic value", () => { test("simple string, with some dynamic value", () => {
qweb.addTemplate("test", "<t>hello <t t-esc=\"text\"/></t>"); qweb.addTemplate("test", '<t>hello <t t-esc="text"/></t>');
expect(renderToString(qweb, "test", {text: "vdom"})).toBe("hello vdom"); expect(renderToString(qweb, "test", { text: "vdom" })).toBe("hello vdom");
}); });
test("empty div", () => { test("empty div", () => {
@@ -52,9 +52,7 @@ describe("static templates", () => {
describe("error handling", () => { describe("error handling", () => {
test("invalid xml", () => { test("invalid xml", () => {
expect(() => qweb.addTemplate("test", "<div>")).toThrow( expect(() => qweb.addTemplate("test", "<div>")).toThrow("Invalid XML in template");
"Invalid XML in template"
);
}); });
test("template with text node and tag", () => { test("template with text node and tag", () => {
@@ -71,6 +69,7 @@ describe("error handling", () => {
test("cannot add twice the same template", () => { test("cannot add twice the same template", () => {
qweb.addTemplate("test", `<t></t>`); qweb.addTemplate("test", `<t></t>`);
expect(() => qweb.addTemplate("test", "<div/>", true)).not.toThrow("already defined");
expect(() => qweb.addTemplate("test", "<div/>")).toThrow("already defined"); expect(() => qweb.addTemplate("test", "<div/>")).toThrow("already defined");
}); });
@@ -89,19 +88,14 @@ describe("error handling", () => {
test("nice error when t-on is evaluated with a missing event", () => { test("nice error when t-on is evaluated with a missing event", () => {
qweb.addTemplate("templatename", `<div t-on="somemethod"></div>`); qweb.addTemplate("templatename", `<div t-on="somemethod"></div>`);
expect(() => expect(() => qweb.render("templatename", { someMethod() {} }, { handlers: [] })).toThrow(
qweb.render("templatename", { someMethod() {} }, { handlers: [] }) "Missing event name with t-on directive"
).toThrow("Missing event name with t-on directive"); );
}); });
test("error when unknown directive", () => { test("error when unknown directive", () => {
qweb.addTemplate( qweb.addTemplate("templatename", `<div t-best-beer="rochefort 10">test</div>`);
"templatename", expect(() => qweb.render("templatename")).toThrow("Unknown QWeb directive: 't-best-beer'");
`<div t-best-beer="rochefort 10">test</div>`
);
expect(() => qweb.render("templatename")).toThrow(
"Unknown QWeb directive: 't-best-beer'"
);
}); });
}); });
@@ -118,9 +112,7 @@ describe("t-esc", () => {
test.skip("escaping", () => { test.skip("escaping", () => {
qweb.addTemplate("test", `<span><t t-esc="var"/></span>`); qweb.addTemplate("test", `<span><t t-esc="var"/></span>`);
expect(renderToString(qweb, "test", { var: "<ok>" })).toBe( expect(renderToString(qweb, "test", { var: "<ok>" })).toBe("<span>&lt;ok&gt;</span>");
"<span>&lt;ok&gt;</span>"
);
}); });
test("escaping on a node", () => { test("escaping on a node", () => {
@@ -152,9 +144,7 @@ describe("t-raw", () => {
test("not escaping", () => { test("not escaping", () => {
qweb.addTemplate("test", `<div><t t-raw="var"/></div>`); qweb.addTemplate("test", `<div><t t-raw="var"/></div>`);
expect(renderToString(qweb, "test", { var: "<ok></ok>" })).toBe( expect(renderToString(qweb, "test", { var: "<ok></ok>" })).toBe("<div><ok></ok></div>");
"<div><ok></ok></div>"
);
}); });
test("t-raw and another sibling node", () => { test("t-raw and another sibling node", () => {
@@ -167,10 +157,7 @@ describe("t-raw", () => {
describe("t-set", () => { describe("t-set", () => {
test("set from attribute literal", () => { test("set from attribute literal", () => {
qweb.addTemplate( qweb.addTemplate("test", `<div><t t-set="value" t-value="'ok'"/><t t-esc="value"/></div>`);
"test",
`<div><t t-set="value" t-value="'ok'"/><t t-esc="value"/></div>`
);
expect(renderToString(qweb, "test")).toBe("<div>ok</div>"); expect(renderToString(qweb, "test")).toBe("<div>ok</div>");
}); });
@@ -182,24 +169,16 @@ describe("t-set", () => {
<t t-if="v === 'ok'">grimbergen</t> <t t-if="v === 'ok'">grimbergen</t>
</div>` </div>`
); );
expect(renderToString(qweb, "test", { value: "ok" })).toBe( expect(renderToString(qweb, "test", { value: "ok" })).toBe("<div>grimbergen</div>");
"<div>grimbergen</div>"
);
}); });
test("set from body literal", () => { test("set from body literal", () => {
qweb.addTemplate( qweb.addTemplate("test", `<t><t t-set="value">ok</t><t t-esc="value"/></t>`);
"test",
`<t><t t-set="value">ok</t><t t-esc="value"/></t>`
);
expect(renderToString(qweb, "test")).toBe("ok"); expect(renderToString(qweb, "test")).toBe("ok");
}); });
test("set from attribute lookup", () => { test("set from attribute lookup", () => {
qweb.addTemplate( qweb.addTemplate("test", `<div><t t-set="stuff" t-value="value"/><t t-esc="stuff"/></div>`);
"test",
`<div><t t-set="stuff" t-value="value"/><t t-esc="stuff"/></div>`
);
expect(renderToString(qweb, "test", { value: "ok" })).toBe("<div>ok</div>"); expect(renderToString(qweb, "test", { value: "ok" })).toBe("<div>ok</div>");
}); });
@@ -231,18 +210,12 @@ describe("t-set", () => {
}); });
test("value priority", () => { test("value priority", () => {
qweb.addTemplate( qweb.addTemplate("test", `<div><t t-set="value" t-value="1">2</t><t t-esc="value"/></div>`);
"test",
`<div><t t-set="value" t-value="1">2</t><t t-esc="value"/></div>`
);
expect(renderToString(qweb, "test")).toBe("<div>1</div>"); expect(renderToString(qweb, "test")).toBe("<div>1</div>");
}); });
test("evaluate value expression", () => { test("evaluate value expression", () => {
qweb.addTemplate( qweb.addTemplate("test", `<div><t t-set="value" t-value="1 + 2"/><t t-esc="value"/></div>`);
"test",
`<div><t t-set="value" t-value="1 + 2"/><t t-esc="value"/></div>`
);
expect(renderToString(qweb, "test")).toBe("<div>3</div>"); expect(renderToString(qweb, "test")).toBe("<div>3</div>");
}); });
@@ -267,25 +240,19 @@ describe("t-set", () => {
"test", "test",
`<div><t t-set="value" t-value="somevariable + 2"/><t t-esc="value"/></div>` `<div><t t-set="value" t-value="somevariable + 2"/><t t-esc="value"/></div>`
); );
expect(renderToString(qweb, "test", { somevariable: 43 })).toBe( expect(renderToString(qweb, "test", { somevariable: 43 })).toBe("<div>45</div>");
"<div>45</div>"
);
}); });
}); });
describe("t-if", () => { describe("t-if", () => {
test("boolean value true condition", () => { test("boolean value true condition", () => {
qweb.addTemplate("test", `<div><t t-if="condition">ok</t></div>`); qweb.addTemplate("test", `<div><t t-if="condition">ok</t></div>`);
expect(renderToString(qweb, "test", { condition: true })).toBe( expect(renderToString(qweb, "test", { condition: true })).toBe("<div>ok</div>");
"<div>ok</div>"
);
}); });
test("boolean value false condition", () => { test("boolean value false condition", () => {
qweb.addTemplate("test", `<div><t t-if="condition">ok</t></div>`); qweb.addTemplate("test", `<div><t t-if="condition">ok</t></div>`);
expect(renderToString(qweb, "test", { condition: false })).toBe( expect(renderToString(qweb, "test", { condition: false })).toBe("<div></div>");
"<div></div>"
);
}); });
test("boolean value condition missing", () => { test("boolean value condition missing", () => {
@@ -302,9 +269,7 @@ describe("t-if", () => {
<t t-else="">beer</t></div> <t t-else="">beer</t></div>
` `
); );
expect(renderToString(qweb, "test", { color: "red" })).toBe( expect(renderToString(qweb, "test", { color: "red" })).toBe("<div>red is dead</div>");
"<div>red is dead</div>"
);
}); });
test("boolean value condition else", () => { test("boolean value condition else", () => {
@@ -330,9 +295,7 @@ describe("t-if", () => {
` `
); );
const result = trim(renderToString(qweb, "test", { condition: false })); const result = trim(renderToString(qweb, "test", { condition: false }));
expect(result).toBe( expect(result).toBe("<div><span>begin</span>fail-else<span>end</span></div>");
"<div><span>begin</span>fail-else<span>end</span></div>"
);
}); });
test("can use some boolean operators in expressions", () => { test("can use some boolean operators in expressions", () => {
@@ -357,9 +320,7 @@ describe("t-if", () => {
m: 5, m: 5,
n: 2 n: 2
}; };
expect(normalize(renderToString(qweb, "test", context))).toBe( expect(normalize(renderToString(qweb, "test", context))).toBe("<div>andormgtnlt</div>");
"<div>andormgtnlt</div>"
);
}); });
}); });
@@ -484,10 +445,7 @@ describe("attributes", () => {
}); });
test("format multiple", () => { test("format multiple", () => {
qweb.addTemplate( qweb.addTemplate("test", `<div t-attf-foo="a {{value1}} is {{value2}} of {{value3}} ]"/>`);
"test",
`<div t-attf-foo="a {{value1}} is {{value2}} of {{value3}} ]"/>`
);
const result = renderToString(qweb, "test", { const result = renderToString(qweb, "test", {
value1: 0, value1: 0,
value2: 1, value2: 1,
@@ -523,19 +481,13 @@ describe("attributes", () => {
}); });
test("class and t-attf-class with ternary operation", () => { test("class and t-attf-class with ternary operation", () => {
qweb.addTemplate( qweb.addTemplate("test", `<div class="hello" t-attf-class="{{value ? 'world' : ''}}"/>`);
"test",
`<div class="hello" t-attf-class="{{value ? 'world' : ''}}"/>`
);
const result = renderToString(qweb, "test", { value: true }); const result = renderToString(qweb, "test", { value: true });
expect(result).toBe(`<div class="hello world"></div>`); expect(result).toBe(`<div class="hello world"></div>`);
}); });
test("t-att-class with object", () => { test("t-att-class with object", () => {
qweb.addTemplate( qweb.addTemplate("test", `<div class="static" t-att-class="{a: b, c: d, e: f}"/>`);
"test",
`<div class="static" t-att-class="{a: b, c: d, e: f}"/>`
);
const result = renderToString(qweb, "test", { b: true, d: false, f: true }); const result = renderToString(qweb, "test", { b: true, d: false, f: true });
expect(result).toBe(`<div class="static a e"></div>`); expect(result).toBe(`<div class="static a e"></div>`);
}); });
@@ -564,10 +516,7 @@ describe("t-call (template calling", () => {
test("with unused setbody", () => { test("with unused setbody", () => {
qweb.addTemplate("_basic-callee", "<div>ok</div>"); qweb.addTemplate("_basic-callee", "<div>ok</div>");
qweb.addTemplate( qweb.addTemplate("caller", '<t t-call="_basic-callee"><t t-set="qux" t-value="3"/></t>');
"caller",
'<t t-call="_basic-callee"><t t-set="qux" t-value="3"/></t>'
);
const expected = "<div>ok</div>"; const expected = "<div>ok</div>";
expect(renderToString(qweb, "caller")).toBe(expected); expect(renderToString(qweb, "caller")).toBe(expected);
}); });
@@ -673,9 +622,7 @@ describe("foreach", () => {
</t> </t>
</div>` </div>`
); );
const result = trim( const result = trim(renderToString(qweb, "test", { value: { a: 1, b: 2, c: 3 } }));
renderToString(qweb, "test", { value: { a: 1, b: 2, c: 3 } })
);
const expected = `<div>[0:a1][1:b2][2:c3]</div>`; const expected = `<div>[0:a1][1:b2][2:c3]</div>`;
expect(result).toBe(expected); expect(result).toBe(expected);
}); });
@@ -722,14 +669,8 @@ describe("foreach", () => {
describe("misc", () => { describe("misc", () => {
test("global", () => { test("global", () => {
qweb.addTemplate("_callee-asc", `<Año t-att-falló="'agüero'" t-raw="0"/>`); qweb.addTemplate("_callee-asc", `<Año t-att-falló="'agüero'" t-raw="0"/>`);
qweb.addTemplate( qweb.addTemplate("_callee-uses-foo", `<span t-esc="foo">foo default</span>`);
"_callee-uses-foo", qweb.addTemplate("_callee-asc-toto", `<div t-raw="toto">toto default</div>`);
`<span t-esc="foo">foo default</span>`
);
qweb.addTemplate(
"_callee-asc-toto",
`<div t-raw="toto">toto default</div>`
);
qweb.addTemplate( qweb.addTemplate(
"caller", "caller",
` `
@@ -842,10 +783,7 @@ describe("t-on", () => {
}); });
test("can bind handlers with object arguments", () => { test("can bind handlers with object arguments", () => {
qweb.addTemplate( qweb.addTemplate("test", `<button t-on-click="add({val: 5})">Click</button>`);
"test",
`<button t-on-click="add({val: 5})">Click</button>`
);
let a = 1; let a = 1;
const node = renderToDOM( const node = renderToDOM(
qweb, qweb,
@@ -863,10 +801,7 @@ describe("t-on", () => {
test("can bind handlers with empty object", () => { test("can bind handlers with empty object", () => {
expect.assertions(2); expect.assertions(2);
qweb.addTemplate( qweb.addTemplate("test", `<button t-on-click="doSomething({})">Click</button>`);
"test",
`<button t-on-click="doSomething({})">Click</button>`
);
const node = renderToDOM( const node = renderToDOM(
qweb, qweb,
"test", "test",
@@ -882,10 +817,7 @@ describe("t-on", () => {
test("can bind handlers with empty object (with non empty inner string)", () => { test("can bind handlers with empty object (with non empty inner string)", () => {
expect.assertions(2); expect.assertions(2);
qweb.addTemplate( qweb.addTemplate("test", `<button t-on-click="doSomething({ })">Click</button>`);
"test",
`<button t-on-click="doSomething({ })">Click</button>`
);
const node = renderToDOM( const node = renderToDOM(
qweb, qweb,
"test", "test",
@@ -1185,13 +1117,10 @@ describe("whitespace handling", () => {
describe("t-key", () => { describe("t-key", () => {
test("can use t-key directive on a node", () => { test("can use t-key directive on a node", () => {
qweb.addTemplate( qweb.addTemplate("test", `<div t-key="beer.id"><t t-esc="beer.name"/></div>`);
"test", expect(renderToString(qweb, "test", { beer: { id: 12, name: "Chimay Rouge" } })).toBe(
`<div t-key="beer.id"><t t-esc="beer.name"/></div>` "<div>Chimay Rouge</div>"
); );
expect(
renderToString(qweb, "test", { beer: { id: 12, name: "Chimay Rouge" } })
).toBe("<div>Chimay Rouge</div>");
}); });
test("t-key directive in a list", () => { test("t-key directive in a list", () => {
+19 -40
View File
@@ -2,9 +2,7 @@ import { compileExpr, tokenize } from "../src/qweb_expressions";
describe("tokenizer", () => { describe("tokenizer", () => {
test("simple tokens", () => { test("simple tokens", () => {
expect(tokenize("1.3")).toEqual([ expect(tokenize("1.3")).toEqual([{ type: "VALUE", value: "1.3" }]);
{ type: "VALUE", value: "1.3" }
]);
expect(tokenize("{}")).toEqual([ expect(tokenize("{}")).toEqual([
{ type: "LEFT_BRACE", value: "{" }, { type: "LEFT_BRACE", value: "{" },
@@ -29,10 +27,7 @@ describe("tokenizer", () => {
{ type: "VALUE", value: "2" }, { type: "VALUE", value: "2" },
{ type: "RIGHT_BRACE", value: "}" } { type: "RIGHT_BRACE", value: "}" }
]); ]);
expect(tokenize("a,")).toEqual([ expect(tokenize("a,")).toEqual([{ type: "SYMBOL", value: "a" }, { type: "COMMA", value: "," }]);
{ type: "SYMBOL", value: "a" },
{ type: "COMMA", value: "," }
]);
expect(tokenize("][")).toEqual([ expect(tokenize("][")).toEqual([
{ type: "RIGHT_BRACKET", value: "]" }, { type: "RIGHT_BRACKET", value: "]" },
{ type: "LEFT_BRACKET", value: "[" } { type: "LEFT_BRACKET", value: "[" }
@@ -40,11 +35,13 @@ describe("tokenizer", () => {
}); });
test("various operators", () => { test("various operators", () => {
expect(tokenize(">= <= < >")).toEqual([ expect(tokenize(">= <= < > !== !=")).toEqual([
{ type: "OPERATOR", value: ">=" }, { type: "OPERATOR", value: ">=" },
{ type: "OPERATOR", value: "<=" }, { type: "OPERATOR", value: "<=" },
{ type: "OPERATOR", value: "<" }, { type: "OPERATOR", value: "<" },
{ type: "OPERATOR", value: ">" }, { type: "OPERATOR", value: ">" },
{ type: "OPERATOR", value: "!==" },
{ type: "OPERATOR", value: "!=" }
]); ]);
}); });
@@ -52,25 +49,17 @@ describe("tokenizer", () => {
expect(() => tokenize("'")).toThrow("Invalid expression"); expect(() => tokenize("'")).toThrow("Invalid expression");
expect(() => tokenize("'\\")).toThrow("Invalid expression"); expect(() => tokenize("'\\")).toThrow("Invalid expression");
expect(() => tokenize("'\\'")).toThrow("Invalid expression"); expect(() => tokenize("'\\'")).toThrow("Invalid expression");
expect(tokenize("'hello ged'")).toEqual([ expect(tokenize("'hello ged'")).toEqual([{ type: "VALUE", value: "'hello ged'" }]);
{ type: "VALUE", value: "'hello ged'" } expect(tokenize("'hello \\'ged\\''")).toEqual([{ type: "VALUE", value: "'hello \\'ged\\''" }]);
]);
expect(tokenize("'hello \\'ged\\''")).toEqual([
{ type: "VALUE", value: "'hello \\'ged\\''" }
]);
expect(() => tokenize('"')).toThrow("Invalid expression"); expect(() => tokenize('"')).toThrow("Invalid expression");
expect(() => tokenize('"\\"')).toThrow("Invalid expression"); expect(() => tokenize('"\\"')).toThrow("Invalid expression");
expect(tokenize('"hello ged"')).toEqual([ expect(tokenize('"hello ged"')).toEqual([{ type: "VALUE", value: '"hello ged"' }]);
{ type: "VALUE", value: '"hello ged"' }
]);
expect(tokenize('"hello ged"}')).toEqual([ expect(tokenize('"hello ged"}')).toEqual([
{ type: "VALUE", value: '"hello ged"' }, { type: "VALUE", value: '"hello ged"' },
{ type: "RIGHT_BRACE", value: "}" } { type: "RIGHT_BRACE", value: "}" }
]); ]);
expect(tokenize('"hello \\"ged\\""')).toEqual([ expect(tokenize('"hello \\"ged\\""')).toEqual([{ type: "VALUE", value: '"hello \\"ged\\""' }]);
{ type: "VALUE", value: '"hello \\"ged\\""' }
]);
}); });
}); });
@@ -111,9 +100,7 @@ describe("expression evaluation", () => {
test("arrays and objects", () => { test("arrays and objects", () => {
expect(compileExpr("[{b:1}] ", {})).toBe("[{b:1}]"); expect(compileExpr("[{b:1}] ", {})).toBe("[{b:1}]");
expect(compileExpr("{a: []} ", {})).toBe("{a:[]}"); expect(compileExpr("{a: []} ", {})).toBe("{a:[]}");
expect(compileExpr("[{b:1, c: [1, {d: {e: 3}} ]}] ", {})).toBe( expect(compileExpr("[{b:1, c: [1, {d: {e: 3}} ]}] ", {})).toBe("[{b:1,c:[1,{d:{e:3}}]}]");
"[{b:1,c:[1,{d:{e:3}}]}]"
);
}); });
test("dot operator", () => { test("dot operator", () => {
@@ -128,13 +115,9 @@ describe("expression evaluation", () => {
}); });
test("various binary operators", () => { test("various binary operators", () => {
expect(compileExpr("color == 'black'", {})).toBe( expect(compileExpr("color == 'black'", {})).toBe("context['color']=='black'");
"context['color']=='black'"
);
expect(compileExpr("a || b", {})).toBe("context['a']||context['b']"); expect(compileExpr("a || b", {})).toBe("context['a']||context['b']");
expect(compileExpr("color === 'black'", {})).toBe( expect(compileExpr("color === 'black'", {})).toBe("context['color']==='black'");
"context['color']==='black'"
);
expect(compileExpr("'li_'+item", {})).toBe("'li_'+context['item']"); expect(compileExpr("'li_'+item", {})).toBe("'li_'+context['item']");
expect(compileExpr("state.val > 1", {})).toBe("context['state'].val>1"); expect(compileExpr("state.val > 1", {})).toBe("context['state'].val>1");
}); });
@@ -162,18 +145,14 @@ describe("expression evaluation", () => {
expect(compileExpr("a()", {})).toBe("context['a']()"); expect(compileExpr("a()", {})).toBe("context['a']()");
expect(compileExpr("a(1)", {})).toBe("context['a'](1)"); expect(compileExpr("a(1)", {})).toBe("context['a'](1)");
expect(compileExpr("a(1,2)", {})).toBe("context['a'](1,2)"); expect(compileExpr("a(1,2)", {})).toBe("context['a'](1,2)");
expect(compileExpr("a(1,2,{a:[a]})", {})).toBe( expect(compileExpr("a(1,2,{a:[a]})", {})).toBe("context['a'](1,2,{a:[context['a']]})");
"context['a'](1,2,{a:[context['a']]})"
);
expect(compileExpr("'x'.toUpperCase()", {})).toBe("'x'.toUpperCase()"); expect(compileExpr("'x'.toUpperCase()", {})).toBe("'x'.toUpperCase()");
expect(compileExpr("'x'.toUpperCase({a: 3})", {})).toBe( expect(compileExpr("'x'.toUpperCase({a: 3})", {})).toBe("'x'.toUpperCase({a:3})");
"'x'.toUpperCase({a:3})" expect(compileExpr("'x'.toUpperCase(a)", { a: { id: "_v5", expr: "" } })).toBe(
"'x'.toUpperCase(_v5)"
);
expect(compileExpr("'x'.toUpperCase({b: a})", { a: { id: "_v5", expr: "" } })).toBe(
"'x'.toUpperCase({b:_v5})"
); );
expect(
compileExpr("'x'.toUpperCase(a)", { a: { id: "_v5", expr: "" } })
).toBe("'x'.toUpperCase(_v5)");
expect(
compileExpr("'x'.toUpperCase({b: a})", { a: { id: "_v5", expr: "" } })
).toBe("'x'.toUpperCase({b:_v5})");
}); });
}); });
+408 -265
View File
@@ -1,11 +1,7 @@
import { Component, Env } from "../src/component"; import { Component, Env } from "../src/component";
import { connect, Store } from "../src/store"; import { Store, ConnectedComponent } from "../src/store";
import { import { makeTestFixture, makeTestEnv, nextMicroTick, nextTick } from "./helpers";
makeTestFixture, import { Observer } from "../src";
makeTestEnv,
nextMicroTick,
nextTick
} from "./helpers";
describe("basic use", () => { describe("basic use", () => {
test("commit a mutation", () => { test("commit a mutation", () => {
@@ -41,6 +37,31 @@ describe("basic use", () => {
expect(store.state.n).toBe(15); expect(store.state.n).toBe(15);
}); });
test("dispatch an action + commit a mutation with positional arguments", () => {
const state = { n1: 1, n2: 1, n3: 1 };
const mutations = {
batchInc({ state }, delta1, delta2, delta3) {
state.n1 += delta1;
state.n2 += delta2;
state.n3 += delta3;
}
};
const actions = {
batchInc({ commit }, delta1, delta2, delta3) {
commit("batchInc", delta1, delta2, delta3);
}
};
const store = new Store({ state, mutations, actions });
expect(store.state.n1).toBe(1);
expect(store.state.n2).toBe(1);
expect(store.state.n3).toBe(1);
store.dispatch("batchInc", 14, 30, 88);
expect(store.state.n1).toBe(15);
expect(store.state.n2).toBe(31);
expect(store.state.n3).toBe(89);
});
test("modifying state outside of mutations trigger error", () => { test("modifying state outside of mutations trigger error", () => {
const state = { n: 1 }; const state = { n: 1 };
const actions = { const actions = {
@@ -207,7 +228,7 @@ describe("basic use", () => {
bestBeerName({ state }) { bestBeerName({ state }) {
n++; n++;
return state.beers[1].name; return state.beers[1].name;
}, }
}; };
const store = new Store({ state, mutations: {}, actions: {}, getters }); const store = new Store({ state, mutations: {}, actions: {}, getters });
expect((<any>store.getters).bestBeerName()).toBe("bertinchamps"); expect((<any>store.getters).bestBeerName()).toBe("bertinchamps");
@@ -226,7 +247,7 @@ describe("basic use", () => {
name: "bertinchamps", name: "bertinchamps",
tasterID: 1 tasterID: 1
} }
}, }
}; };
let n = 0; let n = 0;
const getters = { const getters = {
@@ -265,7 +286,7 @@ describe("basic use", () => {
} }
}; };
const mutations = { const mutations = {
changeTaster({ state }, {beerID, tasterID}) { changeTaster({ state }, { beerID, tasterID }) {
state.beers[beerID].tasterID = tasterID; state.beers[beerID].tasterID = tasterID;
} }
}; };
@@ -274,15 +295,20 @@ describe("basic use", () => {
beerTasterName({ state }, beerID) { beerTasterName({ state }, beerID) {
n++; n++;
return state.tasters[state.beers[beerID].tasterID].name; return state.tasters[state.beers[beerID].tasterID].name;
}, }
}; };
const store = new Store({ state, mutations: mutations, actions: {}, getters }); const store = new Store({
state,
mutations: mutations,
actions: {},
getters
});
expect((<any>store.getters).beerTasterName(1)).toBe("aaron"); expect((<any>store.getters).beerTasterName(1)).toBe("aaron");
expect(n).toBe(1); expect(n).toBe(1);
expect((<any>store.getters).beerTasterName(1)).toBe("aaron"); expect((<any>store.getters).beerTasterName(1)).toBe("aaron");
expect(n).toBe(1); expect(n).toBe(1);
store.commit('changeTaster', {beerID: 1, tasterID: 2}); store.commit("changeTaster", { beerID: 1, tasterID: 2 });
await nextTick(); await nextTick();
expect((<any>store.getters).beerTasterName(1)).toBe("gery"); expect((<any>store.getters).beerTasterName(1)).toBe("gery");
@@ -295,14 +321,14 @@ describe("basic use", () => {
1: { 1: {
id: 1, id: 1,
name: "bertinchamps" name: "bertinchamps"
}, }
} }
}; };
const mutations = { const mutations = {
renameBeer({ state, getters }, beerID) { renameBeer({ state, getters }, beerID) {
expect(getters.beerName(beerID)).toBe('bertinchamps'); expect(getters.beerName(beerID)).toBe("bertinchamps");
state.beers[1].name = 'chouffe'; state.beers[1].name = "chouffe";
expect(getters.beerName(beerID)).toBe('chouffe'); expect(getters.beerName(beerID)).toBe("chouffe");
} }
}; };
let n = 0; let n = 0;
@@ -310,12 +336,17 @@ describe("basic use", () => {
beerName({ state }, beerID) { beerName({ state }, beerID) {
n++; n++;
return state.beers[beerID].name; return state.beers[beerID].name;
}, }
}; };
const store = new Store({ state, mutations: mutations, actions: {}, getters }); const store = new Store({
state,
mutations: mutations,
actions: {},
getters
});
store.commit('renameBeer', 1); store.commit("renameBeer", 1);
expect((<any>store.getters).beerName(1)).toBe('chouffe'); expect((<any>store.getters).beerName(1)).toBe("chouffe");
await nextTick(); await nextTick();
expect(n).toBe(3); expect(n).toBe(3);
@@ -530,18 +561,21 @@ describe("connecting a component to store", () => {
}); });
test("connecting a component works", async () => { test("connecting a component works", async () => {
env.qweb.addTemplate( env.qweb.addTemplates(`
"App", <templates>
` <div t-name="App">
<div> <t t-foreach="props.todos" t-as="todo" >
<t t-foreach="props.todos" t-as="todo" t-key="todo"> <Todo msg="todo.msg" t-key="todo"/>
<t t-widget="Todo" msg="todo.msg"/>
</t> </t>
</div>` </div>
); <span t-name="Todo"><t t-esc="props.msg"/></span>
env.qweb.addTemplate("Todo", `<span><t t-esc="props.msg"/></span>`); </templates>
class App extends Component<any, any, any> { `);
widgets = { Todo }; class App extends ConnectedComponent<any, any, any> {
components = { Todo };
static mapStoreToProps(s) {
return { todos: s.todos };
}
} }
class Todo extends Component<any, any, any> {} class Todo extends Component<any, any, any> {}
const state = { todos: [] }; const state = { todos: [] };
@@ -550,16 +584,9 @@ describe("connecting a component to store", () => {
state.todos.push({ msg }); state.todos.push({ msg });
} }
}; };
function mapStoreToProps(s) {
return { todos: s.todos };
}
const TodoApp = connect(
App,
mapStoreToProps
);
const store = new Store({ state, mutations }); const store = new Store({ state, mutations });
(<any>env).store = store; (<any>env).store = store;
const app = new TodoApp(env); const app = new App(env);
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toMatchSnapshot(); expect(fixture.innerHTML).toMatchSnapshot();
@@ -570,38 +597,35 @@ describe("connecting a component to store", () => {
}); });
test("deep and shallow connecting a component", async () => { test("deep and shallow connecting a component", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="App">
<span t-foreach="props.todos" t-as="todo" t-key="todo">
<t t-esc="todo.title"/>
</span>
</div>
</templates>
`);
const state = { todos: [{ title: "Kasteel" }] }; const state = { todos: [{ title: "Kasteel" }] };
const mutations = { const mutations = {
edit({ state }, title) { edit({ state }, title) {
state.todos[0].title = title; state.todos[0].title = title;
} }
}; };
function mapStoreToProps(s) {
return { todos: s.todos };
}
const store = new Store({ state, mutations }); const store = new Store({ state, mutations });
env.qweb.addTemplate( class App extends ConnectedComponent<any, any, any> {
"App", static mapStoreToProps(s) {
` return { todos: s.todos };
<div> }
<span t-foreach="props.todos" t-as="todo" t-key="todo"> }
<t t-esc="todo.title"/> class DeepTodoApp extends App {
</span> deep = true;
</div>` }
); class ShallowTodoApp extends App {
class App extends Component<any, any, any> {} deep = false;
}
const DeepTodoApp = connect(
App,
mapStoreToProps,
{ deep: true }
);
const ShallowTodoApp = connect(
App,
mapStoreToProps,
{ deep: false }
);
(<any>env).store = store; (<any>env).store = store;
const deepTodoApp = new DeepTodoApp(env); const deepTodoApp = new DeepTodoApp(env);
const shallowTodoApp = new ShallowTodoApp(env); const shallowTodoApp = new ShallowTodoApp(env);
@@ -624,16 +648,13 @@ describe("connecting a component to store", () => {
env.qweb.addTemplates(` env.qweb.addTemplates(`
<templates> <templates>
<div t-name="App"> <div t-name="App">
<t t-foreach="props.todos" t-as="todo" t-key="todo"> <t t-foreach="props.todos" t-as="todo">
<t t-widget="Todo" msg="todo.msg"/> <Todo msg="todo.msg" t-key="todo" />
</t> </t>
</div> </div>
<span t-name="Todo"><t t-esc="props.msg"/></span> <span t-name="Todo"><t t-esc="props.msg"/></span>
</templates> </templates>
`); `);
class App extends Component<any, any, any> {
widgets = { Todo };
}
class Todo extends Component<any, any, any> {} class Todo extends Component<any, any, any> {}
(<any>env).store = new Store({}); (<any>env).store = new Store({});
@@ -645,17 +666,16 @@ describe("connecting a component to store", () => {
} }
} }
}); });
function mapStoreToProps(s) { class App extends ConnectedComponent<any, any, any> {
return { todos: s.todos }; components = { Todo };
} static mapStoreToProps(s) {
const TodoApp = connect( return { todos: s.todos };
App,
mapStoreToProps,
{
getStore: () => store
} }
); getStore() {
const app = new TodoApp(env); return store;
}
}
const app = new App(env);
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toMatchSnapshot(); expect(fixture.innerHTML).toMatchSnapshot();
@@ -667,8 +687,18 @@ describe("connecting a component to store", () => {
test("connected child components with custom hooks", async () => { test("connected child components with custom hooks", async () => {
let steps: any = []; let steps: any = [];
env.qweb.addTemplate("Child", `<div/>`); env.qweb.addTemplates(`
class Child extends Component<any, any, any> { <templates>
<div t-name="Parent">
<Child t-if="state.child" />
</div>
<div t-name="Child"/>
</templates>
`);
class Child extends ConnectedComponent<any, any, any> {
static mapStoreToProps(s) {
return s;
}
mounted() { mounted() {
steps.push("child:mounted"); steps.push("child:mounted");
} }
@@ -677,20 +707,8 @@ describe("connecting a component to store", () => {
} }
} }
const ConnectedChild = connect(
Child,
s => s
);
env.qweb.addTemplate(
"Parent",
`
<div>
<t t-if="state.child" t-widget="ConnectedChild"/>
</div>`
);
class Parent extends Component<any, any, any> { class Parent extends Component<any, any, any> {
widgets = { ConnectedChild }; components = { Child };
constructor(env: Env) { constructor(env: Env) {
super(env); super(env);
@@ -710,7 +728,7 @@ describe("connecting a component to store", () => {
expect(steps).toEqual(["child:mounted", "child:willUnmount"]); expect(steps).toEqual(["child:mounted", "child:willUnmount"]);
}); });
test("connect receives ownprops as second argument", async () => { test("mapStoreToProps receives ownprops as second argument", async () => {
const state = { todos: [{ id: 1, text: "jupiler" }] }; const state = { todos: [{ id: 1, text: "jupiler" }] };
let nextId = 2; let nextId = 2;
const mutations = { const mutations = {
@@ -720,50 +738,42 @@ describe("connecting a component to store", () => {
}; };
const store = new Store({ state, mutations }); const store = new Store({ state, mutations });
env.qweb.addTemplate("TodoItem", `<span><t t-esc="props.text"/></span>`); env.qweb.addTemplates(`
class TodoItem extends Component<any, any, any> {} <templates>
const ConnectedTodo = connect( <span t-name="TodoItem"><t t-esc="props.text"/></span>
TodoItem, <div t-name="TodoList">
(state, props) => { <t t-foreach="props.todos" t-as="todo">
<TodoItem id="todo.id" t-key="todo.id"/>
</t>
</div>
</templates>
`);
class TodoItem extends ConnectedComponent<any, any, any> {
static mapStoreToProps(state, props) {
const todo = state.todos.find(t => t.id === props.id); const todo = state.todos.find(t => t.id === props.id);
return todo; return todo;
} }
);
env.qweb.addTemplate(
"TodoList",
`<div>
<t t-foreach="props.todos" t-as="todo">
<t t-widget="ConnectedTodo" id="todo.id"/>
</t>
</div>`
);
class TodoList extends Component<any, any, any> {
widgets = { ConnectedTodo };
} }
function mapStoreToProps(state) { class TodoList extends ConnectedComponent<any, any, any> {
return { todos: state.todos }; components = { TodoItem };
static mapStoreToProps(state) {
return { todos: state.todos };
}
} }
const ConnectedTodoList = connect(
TodoList,
mapStoreToProps
);
(<any>env).store = store; (<any>env).store = store;
const app = new ConnectedTodoList(env); const app = new TodoList(env);
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>"); expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
store.commit("addTodo", "hoegaarden"); store.commit("addTodo", "hoegaarden");
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe("<div><span>jupiler</span><span>hoegaarden</span></div>");
"<div><span>jupiler</span><span>hoegaarden</span></div>"
);
}); });
test("connect receives store getters as third argument", async () => { test("mapStoreToProps receives store getters as third argument", async () => {
const state = { const state = {
importantID: 1, importantID: 1,
todos: [{ id: 1, text: "jupiler" }, { id: 2, text: "bertinchamps" }] todos: [{ id: 1, text: "jupiler" }, { id: 2, text: "bertinchamps" }]
@@ -778,47 +788,39 @@ describe("connecting a component to store", () => {
}; };
const store = new Store({ state, getters }); const store = new Store({ state, getters });
env.qweb.addTemplate( env.qweb.addTemplates(`
"TodoItem", <templates>
`<div> <div t-name="TodoItem">
<span><t t-esc="props.activeTodoText"/></span> <span><t t-esc="props.activeTodoText"/></span>
<span><t t-esc="props.importantTodoText"/></span> <span><t t-esc="props.importantTodoText"/></span>
</div>` </div>
); <div t-name="TodoList">
class TodoItem extends Component<any, any, any> {} <t t-foreach="props.todos" t-as="todo">
const ConnectedTodo = connect( <TodoItem id="todo.id" t-key="todo.id"/>
TodoItem, </t>
(state, props, getters) => { </div>
</templates>
`);
class TodoItem extends ConnectedComponent<any, any, any> {
static mapStoreToProps(state, props, getters) {
const todo = state.todos.find(t => t.id === props.id); const todo = state.todos.find(t => t.id === props.id);
return { return {
activeTodoText: getters.text(todo.id), activeTodoText: getters.text(todo.id),
importantTodoText: getters.importantTodoText() importantTodoText: getters.importantTodoText()
}; };
} }
);
env.qweb.addTemplate(
"TodoList",
`<div>
<t t-foreach="props.todos" t-as="todo">
<t t-widget="ConnectedTodo" id="todo.id"/>
</t>
</div>`
);
class TodoList extends Component<any, any, any> {
widgets = { ConnectedTodo };
} }
function mapStoreToProps(state) { class TodoList extends ConnectedComponent<any, any, any> {
return { todos: state.todos }; components = { TodoItem };
static mapStoreToProps(state) {
return { todos: state.todos };
}
} }
const ConnectedTodoList = connect(
TodoList,
mapStoreToProps
);
(<any>env).store = store; (<any>env).store = store;
const app = new ConnectedTodoList(env); const app = new TodoList(env);
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe(
@@ -827,23 +829,23 @@ describe("connecting a component to store", () => {
}); });
test("connected component is updated when props are updated", async () => { test("connected component is updated when props are updated", async () => {
env.qweb.addTemplate("Beer", `<span><t t-esc="props.name"/></span>`); env.qweb.addTemplates(`
class Beer extends Component<any, any, any> {} <templates>
const ConnectedBeer = connect( <span t-name="Beer"><t t-esc="props.name"/></span>
Beer, <div t-name="App">
(state, props) => { <Beer id="state.beerId"/>
</div>
</templates>
`);
class Beer extends ConnectedComponent<any, any, any> {
static mapStoreToProps(state, props) {
return state.beers[props.id]; return state.beers[props.id];
} }
); }
env.qweb.addTemplate(
"App",
`<div>
<t t-widget="ConnectedBeer" id="state.beerId"/>
</div>`
);
class App extends Component<any, any, any> { class App extends Component<any, any, any> {
widgets = { ConnectedBeer }; components = { Beer };
state = { beerId: 1 }; state = { beerId: 1 };
} }
@@ -861,14 +863,19 @@ describe("connecting a component to store", () => {
}); });
test("connected component is updated when store is changed", async () => { test("connected component is updated when store is changed", async () => {
env.qweb.addTemplate( env.qweb.addTemplates(`
"App", <templates>
` <div t-name="App">
<div>
<span t-foreach="props.beers" t-as="beer" t-key="beer.name"><t t-esc="beer.name"/></span> <span t-foreach="props.beers" t-as="beer" t-key="beer.name"><t t-esc="beer.name"/></span>
</div>` </div>
); </templates>
class App extends Component<any, any, any> {} `);
class App extends ConnectedComponent<any, any, any> {
static mapStoreToProps(state) {
return { beers: state.beers, otherKey: 1 };
}
}
const mutations = { const mutations = {
addBeer({ state }, name) { addBeer({ state }, name) {
@@ -880,54 +887,42 @@ describe("connecting a component to store", () => {
const store = new Store({ state, mutations }); const store = new Store({ state, mutations });
(<any>env).store = store; (<any>env).store = store;
function mapStoreToProps(state) { const app = new App(env);
return { beers: state.beers, otherKey: 1 };
}
const ConnectedApp = connect(
App,
mapStoreToProps
);
const app = new ConnectedApp(env);
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>"); expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
store.commit("addBeer", "kwak"); store.commit("addBeer", "kwak");
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe("<div><span>jupiler</span><span>kwak</span></div>");
"<div><span>jupiler</span><span>kwak</span></div>"
);
}); });
test("connected component with undefined, null and string props", async () => { test("connected component with undefined, null and string props", async () => {
env.qweb.addTemplate( env.qweb.addTemplates(`
"Beer", <templates>
`<div> <div t-name="Beer">
<span>taster:<t t-esc="props.taster"/></span> <span>taster:<t t-esc="props.taster"/></span>
<span t-if="props.selected">selected:<t t-esc="props.selected.name"/></span> <span t-if="props.selected">selected:<t t-esc="props.selected.name"/></span>
<span t-if="props.consumed">consumed:<t t-esc="props.consumed.name"/></span> <span t-if="props.consumed">consumed:<t t-esc="props.consumed.name"/></span>
</div>` </div>
); <div t-name="App">
class Beer extends Component<any, any, any> {} <Beer id="state.beerId"/>
const ConnectedBeer = connect( </div>
Beer, </templates>
(state, props) => { `);
class Beer extends ConnectedComponent<any, any, any> {
static mapStoreToProps(state, props) {
return { return {
selected: state.beers[props.id], selected: state.beers[props.id],
consumed: state.beers[state.consumedID] || null, consumed: state.beers[state.consumedID] || null,
taster: state.taster taster: state.taster
}; };
} }
); }
env.qweb.addTemplate(
"App",
`<div>
<t t-widget="ConnectedBeer" id="state.beerId"/>
</div>`
);
class App extends Component<any, any, any> { class App extends Component<any, any, any> {
widgets = { ConnectedBeer }; components = { Beer };
state = { beerId: 0 }; state = { beerId: 0 };
} }
@@ -948,9 +943,7 @@ describe("connecting a component to store", () => {
const app = new App(env); const app = new App(env);
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe("<div><div><span>taster:aaron</span></div></div>");
"<div><div><span>taster:aaron</span></div></div>"
);
app.state.beerId = 1; app.state.beerId = 1;
await nextTick(); await nextTick();
@@ -972,34 +965,31 @@ describe("connecting a component to store", () => {
}); });
test("connected component deeply reactive with undefined, null and string props", async () => { test("connected component deeply reactive with undefined, null and string props", async () => {
env.qweb.addTemplate( env.qweb.addTemplates(`
"Beer", <templates>
`<div> <div t-name="Beer">
<span>taster:<t t-esc="props.taster"/></span> <span>taster:<t t-esc="props.taster"/></span>
<span t-if="props.selected">selected:<t t-esc="props.selected.name"/></span> <span t-if="props.selected">selected:<t t-esc="props.selected.name"/></span>
<span t-if="props.consumed">consumed:<t t-esc="props.consumed.name"/></span> <span t-if="props.consumed">consumed:<t t-esc="props.consumed.name"/></span>
</div>` </div>
); <div t-name="App">
class Beer extends Component<any, any, any> {} <Beer id="state.beerId"/>
const ConnectedBeer = connect( </div>
Beer, </templates>
(state, props) => { `);
class Beer extends ConnectedComponent<any, any, any> {
static mapStoreToProps(storeState, props) {
return { return {
selected: state.beers[props.id], selected: storeState.beers[props.id],
consumed: state.beers[state.consumedID] || null, consumed: storeState.beers[storeState.consumedID] || null,
taster: state.taster taster: storeState.taster
}; };
} }
); }
env.qweb.addTemplate(
"App",
`<div>
<t t-widget="ConnectedBeer" id="state.beerId"/>
</div>`
);
class App extends Component<any, any, any> { class App extends Component<any, any, any> {
widgets = { ConnectedBeer }; components = { Beer };
state = { beerId: 0 }; state = { beerId: 0 };
} }
@@ -1026,9 +1016,7 @@ describe("connecting a component to store", () => {
const app = new App(env); const app = new App(env);
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe("<div><div><span>taster:aaron</span></div></div>");
"<div><div><span>taster:aaron</span></div></div>"
);
app.state.beerId = 1; app.state.beerId = 1;
await nextTick(); await nextTick();
@@ -1070,35 +1058,29 @@ describe("connecting a component to store", () => {
test("correct update order when parent/children are connected", async () => { test("correct update order when parent/children are connected", async () => {
const steps: string[] = []; const steps: string[] = [];
env.qweb.addTemplate( env.qweb.addTemplates(`
"Parent", <templates>
` <div t-name="Parent">
<div> <Child key="props.current"/>
<t t-widget="Child" key="props.current"/> </div>
</div> <span t-name="Child"><t t-esc="props.msg"/></span>
` </templates>
); `);
class Parent extends Component<any, any, any> {
widgets = { Child: ConnectedChild }; class Parent extends ConnectedComponent<any, any, any> {
} components = { Child };
const ConnectedParent = connect( static mapStoreToProps(s) {
Parent,
function(s) {
steps.push("parent"); steps.push("parent");
return { current: s.current, isvisible: s.isvisible }; return { current: s.current, isvisible: s.isvisible };
} }
); }
env.qweb.addTemplate("Child", `<span><t t-esc="props.msg"/></span>`); class Child extends ConnectedComponent<any, any, any> {
class Child extends Component<any, any, any> {} static mapStoreToProps(s, props) {
const ConnectedChild = connect(
Child,
function(s, props) {
steps.push("child"); steps.push("child");
return { msg: s.msg[props.key] }; return { msg: s.msg[props.key] };
} }
); }
const state = { current: "a", msg: { a: "a", b: "b" } }; const state = { current: "a", msg: { a: "a", b: "b" } };
const mutations = { const mutations = {
@@ -1109,7 +1091,7 @@ describe("connecting a component to store", () => {
const store = new Store({ state, mutations }); const store = new Store({ state, mutations });
(<any>env).store = store; (<any>env).store = store;
const app = new ConnectedParent(env); const app = new Parent(env);
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>a</span></div>"); expect(fixture.innerHTML).toBe("<div><span>a</span></div>");
@@ -1117,13 +1099,179 @@ describe("connecting a component to store", () => {
store.commit("setCurrent", "b"); store.commit("setCurrent", "b");
await nextTick(); await nextTick();
expect(steps).toEqual(["parent", "child", "parent", "child", "child"]); expect(fixture.innerHTML).toBe("<div><span>b</span></div>");
expect(steps).toEqual(["parent", "child", "parent", "child"]);
});
test("connected parent/children: no double rendering", async () => {
const mutations = {
editTodo({ state }) {
state.todos[1].title = "abc";
}
};
const todos = { 1: { id: 1, title: "kikoou" } };
const state = {
todos
};
const store = new Store({
state,
mutations
});
env.qweb.addTemplates(`
<templates>
<div t-name="TodoApp" class="todoapp">
<t t-foreach="Object.values(props.todos)" t-as="todo">
<TodoItem t-key="todo.id" id="todo.id"/>
</t>
</div>
<div t-name="TodoItem" class="todo">
<t t-esc="props.todo.title"/>
<button class="destroy" t-on-click="editTodo">x</button>
</div>
</templates>
`);
class TodoApp extends ConnectedComponent<any, any, any> {
components = { TodoItem };
static mapStoreToProps(state) {
return {
todos: state.todos
};
}
}
let renderCount = 0;
let fCount = 0;
class TodoItem extends ConnectedComponent<any, any, any> {
state = { isEditing: false };
static mapStoreToProps(state, ownProps) {
fCount++;
return {
todo: state.todos[ownProps.id]
};
}
editTodo() {
this.env.store.commit("editTodo");
}
__render(...args) {
renderCount++;
return super.__render(...args);
}
}
(<any>env).store = store;
const app = new TodoApp(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
'<div class="todoapp"><div class="todo">kikoou<button class="destroy">x</button></div></div>'
);
expect(renderCount).toBe(1);
expect(fCount).toBe(1);
fixture.querySelector("button")!.click();
await nextTick();
expect(renderCount).toBe(2);
expect(fCount).toBe(2);
expect(fixture.innerHTML).toBe(
'<div class="todoapp"><div class="todo">abc<button class="destroy">x</button></div></div>'
);
});
test("connected parent/children: no rendering if child is destroyed", async () => {
const mutations = {
removeTodo({ state }) {
Observer.delete(state.todos, 1);
}
};
const todos = { 1: { id: 1, title: "kikoou" } };
const state = {
todos
};
const store = new Store({
state,
mutations
});
env.qweb.addTemplates(`
<templates>
<div t-name="TodoApp" class="todoapp">
<t t-foreach="Object.values(props.todos)" t-as="todo">
<TodoItem t-key="todo.id" id="todo.id"/>
</t>
</div>
<div t-name="TodoItem" class="todo">
<t t-esc="props.todo.title"/>
<button class="destroy" t-on-click="removeTodo">x</button>
</div>
</templates>
`);
class TodoApp extends ConnectedComponent<any, any, any> {
components = { TodoItem };
static mapStoreToProps(state) {
return {
todos: state.todos
};
}
}
let renderCount = 0;
let fCount = 0;
class TodoItem extends ConnectedComponent<any, any, any> {
state = { isEditing: false };
static mapStoreToProps(state, ownProps) {
fCount++;
return {
todo: state.todos[ownProps.id]
};
}
removeTodo() {
this.env.store.commit("removeTodo");
}
__render(...args) {
renderCount++;
return super.__render(...args);
}
}
(<any>env).store = store;
const app = new TodoApp(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
'<div class="todoapp"><div class="todo">kikoou<button class="destroy">x</button></div></div>'
);
expect(renderCount).toBe(1);
expect(fCount).toBe(1);
fixture.querySelector("button")!.click();
await nextTick();
expect(renderCount).toBe(1);
expect(fCount).toBe(1);
expect(fixture.innerHTML).toBe('<div class="todoapp"></div>');
}); });
test("connected component willpatch/patch hooks are called on store updates", async () => { test("connected component willpatch/patch hooks are called on store updates", async () => {
const steps: string[] = []; const steps: string[] = [];
env.qweb.addTemplate("App", `<div><t t-esc="props.msg"/></div>`);
class App extends Component<any, any, any> { env.qweb.addTemplates(`
<templates>
<div t-name="App"><t t-esc="props.msg"/></div>
</templates>
`);
class App extends ConnectedComponent<any, any, any> {
static mapStoreToProps(s) {
return { msg: s.msg };
}
willPatch() { willPatch() {
steps.push("willpatch"); steps.push("willpatch");
} }
@@ -1131,12 +1279,6 @@ describe("connecting a component to store", () => {
steps.push("patched"); steps.push("patched");
} }
} }
const ConnectedApp = connect(
App,
function(s) {
return { msg: s.msg };
}
);
const state = { msg: "a" }; const state = { msg: "a" };
const mutations = { const mutations = {
@@ -1147,7 +1289,7 @@ describe("connecting a component to store", () => {
const store = new Store({ state, mutations }); const store = new Store({ state, mutations });
(<any>env).store = store; (<any>env).store = store;
const app = new ConnectedApp(env); const app = new App(env);
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>a</div>"); expect(fixture.innerHTML).toBe("<div>a</div>");
@@ -1157,4 +1299,5 @@ describe("connecting a component to store", () => {
expect(fixture.innerHTML).toBe("<div>b</div>"); expect(fixture.innerHTML).toBe("<div>b</div>");
expect(steps).toEqual(["willpatch", "patched"]); expect(steps).toEqual(["willpatch", "patched"]);
}); });
}); });
+28 -143
View File
@@ -59,9 +59,7 @@ describe("attributes", function() {
test("are set correctly when namespaced", function() { test("are set correctly when namespaced", function() {
const vnode1 = h("div", { attrs: { "xlink:href": "#foo" } }); const vnode1 = h("div", { attrs: { "xlink:href": "#foo" } });
elm = patch(vnode0, vnode1).elm; elm = patch(vnode0, vnode1).elm;
expect(elm.getAttributeNS("http://www.w3.org/1999/xlink", "href")).toBe( expect(elm.getAttributeNS("http://www.w3.org/1999/xlink", "href")).toBe("#foo");
"#foo"
);
}); });
test("should not touch class nor id fields", function() { test("should not touch class nor id fields", function() {
@@ -207,12 +205,8 @@ describe("snabbdom", function() {
expect(elm.firstChild.namespaceURI).toBe(SVGNamespace); expect(elm.firstChild.namespaceURI).toBe(SVGNamespace);
// verify that svg tag automatically gets svg namespace // verify that svg tag automatically gets svg namespace
elm = patch( elm = patch(vnode0, h("svg", [h("foreignObject", [h("div", ["I am HTML embedded in SVG"])])]))
vnode0, .elm;
h("svg", [
h("foreignObject", [h("div", ["I am HTML embedded in SVG"])])
])
).elm;
expect(elm.namespaceURI).toBe(SVGNamespace); expect(elm.namespaceURI).toBe(SVGNamespace);
expect(elm.firstChild.namespaceURI).toBe(SVGNamespace); expect(elm.firstChild.namespaceURI).toBe(SVGNamespace);
expect(elm.firstChild.firstChild.namespaceURI).toBe(XHTMLNamespace); expect(elm.firstChild.firstChild.namespaceURI).toBe(XHTMLNamespace);
@@ -352,13 +346,7 @@ describe("snabbdom", function() {
elm = patch(vnode0, vnode1).elm; elm = patch(vnode0, vnode1).elm;
expect(elm.children.length).toBe(2); expect(elm.children.length).toBe(2);
elm = patch(vnode1, vnode2).elm; elm = patch(vnode1, vnode2).elm;
expect(map(elm.children, c => c.innerHTML)).toEqual([ expect(map(elm.children, c => c.innerHTML)).toEqual(["1", "2", "3", "4", "5"]);
"1",
"2",
"3",
"4",
"5"
]);
}); });
test("add elements in the middle", function() { test("add elements in the middle", function() {
@@ -368,13 +356,7 @@ describe("snabbdom", function() {
expect(elm.children.length).toBe(4); expect(elm.children.length).toBe(4);
expect(elm.children.length).toBe(4); expect(elm.children.length).toBe(4);
elm = patch(vnode1, vnode2).elm; elm = patch(vnode1, vnode2).elm;
expect(map(elm.children, c => c.innerHTML)).toEqual([ expect(map(elm.children, c => c.innerHTML)).toEqual(["1", "2", "3", "4", "5"]);
"1",
"2",
"3",
"4",
"5"
]);
}); });
test("add elements at beginning and end", function() { test("add elements at beginning and end", function() {
@@ -383,13 +365,7 @@ describe("snabbdom", function() {
elm = patch(vnode0, vnode1).elm; elm = patch(vnode0, vnode1).elm;
expect(elm.children.length).toBe(3); expect(elm.children.length).toBe(3);
elm = patch(vnode1, vnode2).elm; elm = patch(vnode1, vnode2).elm;
expect(map(elm.children, c => c.innerHTML)).toEqual([ expect(map(elm.children, c => c.innerHTML)).toEqual(["1", "2", "3", "4", "5"]);
"1",
"2",
"3",
"4",
"5"
]);
}); });
test("adds children to parent with no children", function() { test("adds children to parent with no children", function() {
@@ -588,35 +564,19 @@ describe("snabbdom", function() {
elm = patch(vnode0, vnode1).elm; elm = patch(vnode0, vnode1).elm;
expect(elm.children.length).toBe(6); expect(elm.children.length).toBe(6);
elm = patch(vnode1, vnode2).elm; elm = patch(vnode1, vnode2).elm;
expect(map(elm.children, c => c.innerHTML)).toEqual([ expect(map(elm.children, c => c.innerHTML)).toEqual(["4", "3", "2", "1", "5", "0"]);
"4",
"3",
"2",
"1",
"5",
"0"
]);
}); });
test("supports null/undefined children", function() { test("supports null/undefined children", function() {
const vnode1 = h("i", [0, 1, 2, 3, 4, 5].map(spanNum)); const vnode1 = h("i", [0, 1, 2, 3, 4, 5].map(spanNum));
const vnode2 = h( const vnode2 = h(
"i", "i",
[null, 2, undefined, null, 1, 0, null, 5, 4, null, 3, undefined].map( [null, 2, undefined, null, 1, 0, null, 5, 4, null, 3, undefined].map(spanNum)
spanNum
)
); );
elm = patch(vnode0, vnode1).elm; elm = patch(vnode0, vnode1).elm;
expect(elm.children.length).toBe(6); expect(elm.children.length).toBe(6);
elm = patch(vnode1, vnode2).elm; elm = patch(vnode1, vnode2).elm;
expect(map(elm.children, c => c.innerHTML)).toEqual([ expect(map(elm.children, c => c.innerHTML)).toEqual(["2", "1", "0", "5", "4", "3"]);
"2",
"1",
"0",
"5",
"4",
"3"
]);
}); });
test("supports all null/undefined children", function() { test("supports all null/undefined children", function() {
@@ -627,14 +587,7 @@ describe("snabbdom", function() {
elm = patch(vnode1, vnode2).elm; elm = patch(vnode1, vnode2).elm;
expect(elm.children.length).toBe(0); expect(elm.children.length).toBe(0);
elm = patch(vnode2, vnode3).elm; elm = patch(vnode2, vnode3).elm;
expect(map(elm.children, c => c.innerHTML)).toEqual([ expect(map(elm.children, c => c.innerHTML)).toEqual(["5", "4", "3", "2", "1", "0"]);
"5",
"4",
"3",
"2",
"1",
"0"
]);
}); });
}); });
}); });
@@ -714,18 +667,10 @@ describe("snabbdom", function() {
}); });
test("removes elements", function() { test("removes elements", function() {
const vnode1 = h("div", [ const vnode1 = h("div", [h("span", "One"), h("span", "Two"), h("span", "Three")]);
h("span", "One"),
h("span", "Two"),
h("span", "Three")
]);
const vnode2 = h("div", [h("span", "One"), h("span", "Three")]); const vnode2 = h("div", [h("span", "One"), h("span", "Three")]);
elm = patch(vnode0, vnode1).elm; elm = patch(vnode0, vnode1).elm;
expect(map(elm.children, c => c.innerHTML)).toEqual([ expect(map(elm.children, c => c.innerHTML)).toEqual(["One", "Two", "Three"]);
"One",
"Two",
"Three"
]);
elm = patch(vnode1, vnode2).elm; elm = patch(vnode1, vnode2).elm;
expect(map(elm.children, c => c.innerHTML)).toEqual(["One", "Three"]); expect(map(elm.children, c => c.innerHTML)).toEqual(["One", "Three"]);
}); });
@@ -760,49 +705,19 @@ describe("snabbdom", function() {
}); });
test("reorders elements", function() { test("reorders elements", function() {
const vnode1 = h("div", [ const vnode1 = h("div", [h("span", "One"), h("div", "Two"), h("b", "Three")]);
h("span", "One"), const vnode2 = h("div", [h("b", "Three"), h("span", "One"), h("div", "Two")]);
h("div", "Two"),
h("b", "Three")
]);
const vnode2 = h("div", [
h("b", "Three"),
h("span", "One"),
h("div", "Two")
]);
elm = patch(vnode0, vnode1).elm; elm = patch(vnode0, vnode1).elm;
expect(map(elm.children, c => c.innerHTML)).toEqual([ expect(map(elm.children, c => c.innerHTML)).toEqual(["One", "Two", "Three"]);
"One",
"Two",
"Three"
]);
elm = patch(vnode1, vnode2).elm; elm = patch(vnode1, vnode2).elm;
expect(map(elm.children, c => c.tagName)).toEqual(["B", "SPAN", "DIV"]); expect(map(elm.children, c => c.tagName)).toEqual(["B", "SPAN", "DIV"]);
expect(map(elm.children, c => c.innerHTML)).toEqual([ expect(map(elm.children, c => c.innerHTML)).toEqual(["Three", "One", "Two"]);
"Three",
"One",
"Two"
]);
}); });
test("supports null/undefined children", function() { test("supports null/undefined children", function() {
const vnode1 = h("i", [null, h("i", "1"), h("i", "2"), null]); const vnode1 = h("i", [null, h("i", "1"), h("i", "2"), null]);
const vnode2 = h("i", [ const vnode2 = h("i", [h("i", "2"), undefined, undefined, h("i", "1"), undefined]);
h("i", "2"), const vnode3 = h("i", [null, h("i", "1"), undefined, null, h("i", "2"), undefined, null]);
undefined,
undefined,
h("i", "1"),
undefined
]);
const vnode3 = h("i", [
null,
h("i", "1"),
undefined,
null,
h("i", "2"),
undefined,
null
]);
elm = patch(vnode0, vnode1).elm; elm = patch(vnode0, vnode1).elm;
expect(map(elm.children, c => c.innerHTML)).toEqual(["1", "2"]); expect(map(elm.children, c => c.innerHTML)).toEqual(["1", "2"]);
elm = patch(vnode1, vnode2).elm; elm = patch(vnode1, vnode2).elm;
@@ -835,10 +750,7 @@ describe("snabbdom", function() {
} }
const vnode1 = h("div", [ const vnode1 = h("div", [
h("span", "First sibling"), h("span", "First sibling"),
h("div", { hook: { create: cb } }, [ h("div", { hook: { create: cb } }, [h("span", "Child 1"), h("span", "Child 2")]),
h("span", "Child 1"),
h("span", "Child 2")
]),
h("span", "Can't touch me") h("span", "Can't touch me")
]); ]);
patch(vnode0, vnode1); patch(vnode0, vnode1);
@@ -855,10 +767,7 @@ describe("snabbdom", function() {
} }
const vnode1 = h("div", [ const vnode1 = h("div", [
h("span", "First sibling"), h("span", "First sibling"),
h("div", { hook: { insert: cb } }, [ h("div", { hook: { insert: cb } }, [h("span", "Child 1"), h("span", "Child 2")]),
h("span", "Child 1"),
h("span", "Child 2")
]),
h("span", "Can touch me") h("span", "Can touch me")
]); ]);
patch(vnode0, vnode1); patch(vnode0, vnode1);
@@ -874,17 +783,11 @@ describe("snabbdom", function() {
} }
const vnode1 = h("div", [ const vnode1 = h("div", [
h("span", "First sibling"), h("span", "First sibling"),
h("div", { hook: { prepatch: cb } }, [ h("div", { hook: { prepatch: cb } }, [h("span", "Child 1"), h("span", "Child 2")])
h("span", "Child 1"),
h("span", "Child 2")
])
]); ]);
const vnode2 = h("div", [ const vnode2 = h("div", [
h("span", "First sibling"), h("span", "First sibling"),
h("div", { hook: { prepatch: cb } }, [ h("div", { hook: { prepatch: cb } }, [h("span", "Child 1"), h("span", "Child 2")])
h("span", "Child 1"),
h("span", "Child 2")
])
]); ]);
patch(vnode0, vnode1); patch(vnode0, vnode1);
patch(vnode1, vnode2); patch(vnode1, vnode2);
@@ -965,10 +868,7 @@ describe("snabbdom", function() {
} }
const vnode1 = h("div", [ const vnode1 = h("div", [
h("span", "First sibling"), h("span", "First sibling"),
h("div", { hook: { remove: cb } }, [ h("div", { hook: { remove: cb } }, [h("span", "Child 1"), h("span", "Child 2")])
h("span", "Child 1"),
h("span", "Child 2")
])
]); ]);
const vnode2 = h("div", [h("span", "First sibling")]); const vnode2 = h("div", [h("span", "First sibling")]);
patch(vnode0, vnode1); patch(vnode0, vnode1);
@@ -981,9 +881,7 @@ describe("snabbdom", function() {
function cb(vnode) { function cb(vnode) {
calls++; calls++;
} }
const vnode1 = h("div", [ const vnode1 = h("div", [h("div", { hook: { destroy: cb } }, [h("span", "Child 1")])]);
h("div", { hook: { destroy: cb } }, [h("span", "Child 1")])
]);
const vnode2 = h("div", "Text node"); const vnode2 = h("div", "Text node");
patch(vnode0, vnode1); patch(vnode0, vnode1);
patch(vnode1, vnode2); patch(vnode1, vnode2);
@@ -1053,10 +951,7 @@ describe("snabbdom", function() {
result.push(vnode); result.push(vnode);
rm(); rm();
} }
const vnode1 = h("div", { hook: { remove: cb } }, [ const vnode1 = h("div", { hook: { remove: cb } }, [h("b", "Child 1"), h("i", "Child 2")]);
h("b", "Child 1"),
h("i", "Child 2")
]);
const vnode2 = h("span", [h("b", "Child 1"), h("i", "Child 2")]); const vnode2 = h("span", [h("b", "Child 1"), h("i", "Child 2")]);
patch(vnode0, vnode1); patch(vnode0, vnode1);
patch(vnode1, vnode2); patch(vnode1, vnode2);
@@ -1091,10 +986,7 @@ describe("snabbdom", function() {
} }
const vnode1 = h("div", [ const vnode1 = h("div", [
h("span", "First sibling"), h("span", "First sibling"),
h("div", [ h("div", [h("span", { hook: { destroy: cb } }, "Child 1"), h("span", "Child 2")])
h("span", { hook: { destroy: cb } }, "Child 1"),
h("span", "Child 2")
])
]); ]);
const vnode2 = h("div"); const vnode2 = h("div");
patch(vnode0, vnode1); patch(vnode0, vnode1);
@@ -1150,11 +1042,7 @@ describe("snabbdom", function() {
} }
} }
]); ]);
const vnode1 = h("div", [ const vnode1 = h("div", [h("span", "First child"), "", h("span", "Third child")]);
h("span", "First child"),
"",
h("span", "Third child")
]);
const vnode2 = h("div"); const vnode2 = h("div");
patch(vnode0, vnode1); patch(vnode0, vnode1);
patch(vnode1, vnode2); patch(vnode1, vnode2);
@@ -1196,10 +1084,7 @@ describe("snabbdom", function() {
function cb(vnode) { function cb(vnode) {
result.push(vnode); result.push(vnode);
} }
const vnode1 = h("div", [ const vnode1 = h("div", [h("span", { hook: { update: cb } }, "Hello"), h("span", "there")]);
h("span", { hook: { update: cb } }, "Hello"),
h("span", "there")
]);
patch(vnode0, vnode1); patch(vnode0, vnode1);
patch(vnode1, vnode1); patch(vnode1, vnode1);
expect(result).toHaveLength(0); expect(result).toHaveLength(0);
+2 -11
View File
@@ -1,9 +1,4 @@
import { import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
buildData,
startMeasure,
stopMeasure,
formatNumber
} from "../shared/utils.js";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Likes Counter Widget // Likes Counter Widget
@@ -40,11 +35,7 @@ class App extends owl.Component {
state = { messages: [], multipleFlag: false, clearAfterFlag: false }; state = { messages: [], multipleFlag: false, clearAfterFlag: false };
mounted() { mounted() {
this.log( this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
`Benchmarking Owl v${owl.__info__.version} (build date: ${
owl.__info__.date
})`
);
} }
benchmark(message, fn, callback) { benchmark(message, fn, callback) {
+2 -11
View File
@@ -1,9 +1,4 @@
import { import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
buildData,
startMeasure,
stopMeasure,
formatNumber
} from "../shared/utils.js";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Likes Counter Widget // Likes Counter Widget
@@ -40,11 +35,7 @@ class App extends owl.Component {
state = { messages: [], multipleFlag: false, clearAfterFlag: false }; state = { messages: [], multipleFlag: false, clearAfterFlag: false };
mounted() { mounted() {
this.log( this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
`Benchmarking Owl v${owl.__info__.version} (build date: ${
owl.__info__.date
})`
);
} }
benchmark(message, fn, callback) { benchmark(message, fn, callback) {
+161
View File
@@ -0,0 +1,161 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
widgets = { Counter };
shouldUpdate(nextProps) {
return nextProps.message !== this.props.message;
}
removeMessage() {
this.trigger("remove-message", {
id: this.props.message.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
widgets = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
mounted() {
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
}
benchmark(message, fn, callback) {
if (this.state.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.state.clearAfterFlag) {
this._benchmark(
"clear",
() => {
this.state.messages = [];
},
finalize,
false
);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
}
_benchmark(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
fn();
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
}
addMessages(n) {
this.benchmark("add " + n, () => {
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
});
}
clear() {
this._benchmark("clear", () => {
this.state.messages = [];
});
}
updateSomeMessages() {
this.benchmark("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);
}
});
}
removeMessage(event) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === event.detail.id);
this.state.messages.splice(index, 1);
});
}
log(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
}
}
//------------------------------------------------------------------------------
// 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.15.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
+50
View File
@@ -0,0 +1,50 @@
<templates>
<div t-name="App" class="main">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1k messages</button>
<button t-on-click="addMessages(10000)">Add 10k messages</button>
<button t-on-click="addMessages(30000)">Add 30k messages</button>
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
<hr/>
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
<div class="log">
<div class="log-content" t-ref="log"/>
</div>
</div>
<div class="right-thing">
<div class="content" t-on-remove-message="removeMessage">
<t t-foreach="state.messages" t-as="message">
<t t-widget="Message" t-key="message.id" message="message"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.message.author"/></span>
<span class="msg"><t t-esc="props.message.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>
+161
View File
@@ -0,0 +1,161 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
components = { Counter };
shouldUpdate(nextProps) {
return nextProps.message !== this.props.message;
}
removeMessage() {
this.trigger("remove-message", {
id: this.props.message.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
components = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
mounted() {
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
}
benchmark(message, fn, callback) {
if (this.state.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.state.clearAfterFlag) {
this._benchmark(
"clear",
() => {
this.state.messages = [];
},
finalize,
false
);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
}
_benchmark(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
fn();
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
}
addMessages(n) {
this.benchmark("add " + n, () => {
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
});
}
clear() {
this._benchmark("clear", () => {
this.state.messages = [];
});
}
updateSomeMessages() {
this.benchmark("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);
}
});
}
removeMessage(event) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === event.detail.id);
this.state.messages.splice(index, 1);
});
}
log(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
}
}
//------------------------------------------------------------------------------
// 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.16.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
+50
View File
@@ -0,0 +1,50 @@
<templates>
<div t-name="App" class="main">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1k messages</button>
<button t-on-click="addMessages(10000)">Add 10k messages</button>
<button t-on-click="addMessages(30000)">Add 30k messages</button>
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
<hr/>
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
<div class="log">
<div class="log-content" t-ref="log"/>
</div>
</div>
<div class="right-thing">
<div class="content" t-on-remove-message="removeMessage">
<t t-foreach="state.messages" t-as="message">
<Message t-key="message.id" message="message"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.message.author"/></span>
<span class="msg"><t t-esc="props.message.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<Counter/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
+5 -16
View File
@@ -1,9 +1,4 @@
import { import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
buildData,
startMeasure,
stopMeasure,
formatNumber
} from "../shared/utils.js";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Likes Counter Widget // Likes Counter Widget
@@ -20,7 +15,7 @@ class Counter extends owl.Component {
// Message Widget // Message Widget
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class Message extends owl.Component { class Message extends owl.Component {
widgets = { Counter }; components = { Counter };
shouldUpdate(nextProps) { shouldUpdate(nextProps) {
return nextProps.message !== this.props.message; return nextProps.message !== this.props.message;
@@ -36,15 +31,11 @@ class Message extends owl.Component {
// Root Widget // Root Widget
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class App extends owl.Component { class App extends owl.Component {
widgets = { Message }; components = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false }; state = { messages: [], multipleFlag: false, clearAfterFlag: false };
mounted() { mounted() {
this.log( this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
`Benchmarking Owl v${owl.__info__.version} (build date: ${
owl.__info__.date
})`
);
} }
benchmark(message, fn, callback) { benchmark(message, fn, callback) {
@@ -128,9 +119,7 @@ class App extends owl.Component {
removeMessage(event) { removeMessage(event) {
this.benchmark("remove message", () => { this.benchmark("remove message", () => {
const index = this.state.messages.findIndex( const index = this.state.messages.findIndex(m => m.id === event.detail.id);
m => m.id === event.detail.id
);
this.state.messages.splice(index, 1); this.state.messages.splice(index, 1);
}); });
} }
+2 -2
View File
@@ -30,7 +30,7 @@
<div class="right-thing"> <div class="right-thing">
<div class="content" t-on-remove-message="removeMessage"> <div class="content" t-on-remove-message="removeMessage">
<t t-foreach="state.messages" t-as="message"> <t t-foreach="state.messages" t-as="message">
<t t-widget="Message" t-key="message.id" message="message"/> <Message t-key="message.id" message="message"/>
</t> </t>
</div> </div>
</div> </div>
@@ -40,7 +40,7 @@
<span class="author"><t t-esc="props.message.author"/></span> <span class="author"><t t-esc="props.message.author"/></span>
<span class="msg"><t t-esc="props.message.msg"/></span> <span class="msg"><t t-esc="props.message.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button> <button class="remove" t-on-click="removeMessage">Remove</button>
<t t-widget="Counter"/> <Counter/>
</div> </div>
<div t-name="Counter"> <div t-name="Counter">
+1 -3
View File
@@ -1,9 +1,7 @@
html, html,
body { body {
height: 100%; height: 100%;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, font-family: Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial,
sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
} }
body { body {
+3 -3
View File
@@ -17,7 +17,7 @@ let nextId = 1;
export function buildData(n = 1000) { export function buildData(n = 1000) {
const data = []; const data = [];
for (let i = 0; i < n; i++) { for (let i = 0; i < n; i++) {
let id = nextId++; let id = nextId++;
data.push({ data.push({
id: id, id: id,
author: chooseRandomly(AUTHORS), author: chooseRandomly(AUTHORS),
@@ -32,7 +32,7 @@ export function buildData(n = 1000) {
// Measuring helpers // Measuring helpers
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
export function formatNumber(n) { export function formatNumber(n) {
return Number(n).toFixed(); return Number(n).toFixed();
} }
let startTime; let startTime;
@@ -51,7 +51,7 @@ export function stopMeasure(cb) {
const msg = `[${last}] took ${formatNumber(delta)}ms`; const msg = `[${last}] took ${formatNumber(delta)}ms`;
console.log(msg); console.log(msg);
if (cb) { if (cb) {
cb({msg, delta}); cb({ msg, delta });
} }
}, 0); }, 0);
} }
+2
View File
@@ -35,6 +35,8 @@
<li><a href="benchmarks/owl-0.12.0">OWL 0.12.0</a></li> <li><a href="benchmarks/owl-0.12.0">OWL 0.12.0</a></li>
<li><a href="benchmarks/owl-0.13.0">OWL 0.13.0</a></li> <li><a href="benchmarks/owl-0.13.0">OWL 0.13.0</a></li>
<li><a href="benchmarks/owl-0.14.0">OWL 0.14.0</a></li> <li><a href="benchmarks/owl-0.14.0">OWL 0.14.0</a></li>
<li><a href="benchmarks/owl-0.15.0">OWL 0.15.0</a></li>
<li><a href="benchmarks/owl-0.16.0">OWL 0.16.0</a></li>
<li><a href="benchmarks/owl-master">OWL Master</a></li> <li><a href="benchmarks/owl-master">OWL Master</a></li>
</ul> </ul>
<ul> <ul>
+1 -3
View File
@@ -1,9 +1,7 @@
html, html,
body { body {
height: 100%; height: 100%;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, font-family: Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial,
sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
} }
.title, .title,
+28 -15
View File
@@ -97,10 +97,7 @@ function makeCodeIframe(js, css, xml, errorHandler) {
setTimeout(function() { setTimeout(function() {
if (iframe.contentWindow) { if (iframe.contentWindow) {
iframe.contentWindow.removeEventListener("error", errorHandler); iframe.contentWindow.removeEventListener("error", errorHandler);
iframe.contentWindow.removeEventListener( iframe.contentWindow.removeEventListener("unhandledrejection", errorHandler);
"unhandledrejection",
errorHandler
);
} }
}, 200); }, 200);
doc.body.appendChild(script); doc.body.appendChild(script);
@@ -161,7 +158,7 @@ class App extends owl.Component {
super(...args); super(...args);
this.version = owl.__info__.version; this.version = owl.__info__.version;
this.SAMPLES = SAMPLES; this.SAMPLES = SAMPLES;
this.widgets = { TabbedEditor }; this.components = { TabbedEditor };
this.state = { this.state = {
js: SAMPLES[0].code, js: SAMPLES[0].code,
@@ -200,6 +197,8 @@ class App extends owl.Component {
} catch (e) { } catch (e) {
//probably problem with the templates //probably problem with the templates
error = e; error = e;
// we still log the error, always useful to have it available
console.error(e);
} }
if (error) { if (error) {
this.displayError(error.message); this.displayError(error.message);
@@ -284,15 +283,7 @@ class TabbedEditor extends owl.Component {
this.setTab = owl.utils.debounce(this.setTab, 250, true); this.setTab = owl.utils.debounce(this.setTab, 250, true);
this.sessions = {}; this.sessions = {};
for (let tab of ["js", "xml", "css"]) { this._setupSessions(props);
if (props[tab]) {
this.sessions[tab] = new ace.EditSession(props[tab], MODES[tab]);
this.sessions[tab].setOption("useWorker", false);
const tabSize = tab === "xml" ? 2 : 4;
this.sessions[tab].setOption("tabSize", tabSize);
this.sessions[tab].setUndoManager(new ace.UndoManager());
}
}
this.editor = null; this.editor = null;
} }
@@ -317,9 +308,19 @@ class TabbedEditor extends owl.Component {
}); });
} }
willUpdateProps(nextProps) {
this._setupSessions(nextProps);
}
patched() { patched() {
const session = this.sessions[this.state.currentTab]; const session = this.sessions[this.state.currentTab];
session.setValue(this.props[this.state.currentTab], -1); let content = this.props[this.state.currentTab];
if (content === false) {
const tab = this.props.js ? "js" : this.props.xml ? "xml" : "css";
content = this.props[tab];
this.state.currentTab = tab;
}
session.setValue(content, -1);
this.editor.setSession(session); this.editor.setSession(session);
this.editor.resize(); this.editor.resize();
} }
@@ -347,6 +348,18 @@ class TabbedEditor extends owl.Component {
}); });
} }
} }
_setupSessions(props) {
for (let tab of ["js", "xml", "css"]) {
if (props[tab] && !this.sessions[tab]) {
this.sessions[tab] = new ace.EditSession(props[tab], MODES[tab]);
this.sessions[tab].setOption("useWorker", false);
const tabSize = tab === "xml" ? 2 : 4;
this.sessions[tab].setOption("tabSize", tabSize);
this.sessions[tab].setUndoManager(new ace.UndoManager());
}
}
}
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
+10 -8
View File
@@ -2,9 +2,7 @@
html, html,
body { body {
height: 100%; height: 100%;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, font-family: Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial,
sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
} }
body { body {
@@ -181,10 +179,14 @@ body {
.right-pane .error { .right-pane .error {
height: 100%; height: 100%;
padding-top: 40%; width: 90%;
font-size: 30px; padding-top: 30%;
font-size: 18px;
color: darkred; color: darkred;
text-align: center; margin-left: 5%;
padding-left: 30px; }
padding-right: 30px;
.right-pane .error pre {
overflow: auto;
width: 100%;
} }
+100 -94
View File
@@ -1,16 +1,17 @@
const COMPONENTS = `// In this example, we show how components can be defined and created. const COMPONENTS = `// In this example, we show how components can be defined and created.
class Counter extends owl.Component { class Greeter extends owl.Component {
state = { value: 0 }; state = { word: 'Hello' };
increment() { toggle() {
this.state.value++; this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi'
} }
} }
// Main root widget // Main root component
class App extends owl.Component { class App extends owl.Component {
widgets = { Counter }; components = { Greeter };
state = { name: 'World'};
} }
// Application setup // Application setup
@@ -21,20 +22,25 @@ app.mount(document.body);
`; `;
const COMPONENTS_XML = `<templates> const COMPONENTS_XML = `<templates>
<button t-name="Counter" t-on-click="increment"> <div t-name="Greeter" class="greeter" t-on-click="toggle">
Click Me! [<t t-esc="state.value"/>] <t t-esc="state.word"/>, <t t-esc="props.name"/>
</button> </div>
<div t-name="App"> <div t-name="App">
<t t-widget="Counter"/> <Greeter name="state.name"/>
<t t-widget="Counter"/>
</div> </div>
</templates>`; </templates>
`;
const COMPONENTS_CSS = `button { const COMPONENTS_CSS = `.greeter {
font-size: 20px; font-size: 20px;
width: 220px; width: 300px;
height: 100px;
margin: 5px; margin: 5px;
text-align: center;
line-height: 100px;
background-color: #eeeeee;
user-select: none;
}`; }`;
const ANIMATION = `// The goal of this component is to see how the t-transition directive can be const ANIMATION = `// The goal of this component is to see how the t-transition directive can be
@@ -49,8 +55,8 @@ class Counter extends owl.Component {
} }
class App extends owl.Component { class App extends owl.Component {
state = { flag: false, widgetFlag: false, numbers: [] }; state = { flag: false, componentFlag: false, numbers: [] };
widgets = { Counter }; components = { Counter };
toggle(key) { toggle(key) {
this.state[key] = !this.state[key]; this.state[key] = !this.state[key];
@@ -83,12 +89,12 @@ const ANIMATION_XML = `<templates>
</div> </div>
</div> </div>
<h2>Transition on sub widgets</h2> <h2>Transition on sub components</h2>
<div class="demo"> <div class="demo">
<button t-on-click="toggle('widgetFlag')">Toggle widget</button> <button t-on-click="toggle('componentFlag')">Toggle component</button>
<div> <div>
<t t-widget="Counter" t-if="state.widgetFlag" t-transition="fade"/> <Counter t-if="state.componentFlag" t-transition="fade"/>
</div> </div>
</div> </div>
@@ -98,7 +104,7 @@ const ANIMATION_XML = `<templates>
<button t-on-click="addNumber">Add a number</button> <button t-on-click="addNumber">Add a number</button>
<div> <div>
<t t-foreach="state.numbers" t-as="n"> <t t-foreach="state.numbers" t-as="n">
<span t-transition="fade" class="numberspan"><t t-esc="n"/></span> <span t-transition="fade" class="numberspan" t-key="n"><t t-esc="n"/></span>
</t> </t>
</div> </div>
</div> </div>
@@ -173,12 +179,12 @@ const ANIMATION_CSS = `button {
const LIFECYCLE_DEMO = `// This example shows all the possible lifecycle hooks const LIFECYCLE_DEMO = `// This example shows all the possible lifecycle hooks
// //
// The root widget controls a sub widget (DemoWidget). It logs all its lifecycle // The root component controls a sub component (DemoComponent). It logs all its lifecycle
// methods in the console. Try modifying its state by clicking on it, or by // methods in the console. Try modifying its state by clicking on it, or by
// clicking on the two main buttons, and look into the console to see what // clicking on the two main buttons, and look into the console to see what
// happens. // happens.
class DemoWidget extends owl.Component { class DemoComponent extends owl.Component {
constructor() { constructor() {
super(...arguments); super(...arguments);
this.state = { n: 0 }; this.state = { n: 0 };
@@ -208,14 +214,14 @@ class DemoWidget extends owl.Component {
} }
class App extends owl.Component { class App extends owl.Component {
widgets = { DemoWidget }; components = { DemoComponent };
state = { n: 0, flag: true }; state = { n: 0, flag: true };
increment() { increment() {
this.state.n++; this.state.n++;
} }
toggleSubWidget() { toggleSubComponent() {
this.state.flag = !this.state.flag; this.state.flag = !this.state.flag;
} }
} }
@@ -226,17 +232,17 @@ app.mount(document.body);
`; `;
const LIFECYCLE_DEMO_XML = `<templates> const LIFECYCLE_DEMO_XML = `<templates>
<div t-name="DemoWidget" t-on-click="increment" class="demo"> <div t-name="DemoComponent" t-on-click="increment" class="demo">
<div>Demo Sub Widget</div> <div>Demo Sub Component</div>
<div>(click on me to update me)</div> <div>(click on me to update me)</div>
<div>Props: <t t-esc="props.n"/>, State: <t t-esc="state.n"/>. </div> <div>Props: <t t-esc="props.n"/>, State: <t t-esc="state.n"/>. </div>
</div> </div>
<div t-name="App"> <div t-name="App">
<button t-on-click="increment">Increment Parent State</button> <button t-on-click="increment">Increment Parent State</button>
<button t-on-click="toggleSubWidget">Toggle SubWidget</button> <button t-on-click="toggleSubComponent">Toggle SubComponent</button>
<div t-if="state.flag"> <div t-if="state.flag">
<t t-widget="DemoWidget" n="state.n"/> <DemoComponent n="state.n"/>
</div> </div>
</div> </div>
</templates>`; </templates>`;
@@ -399,16 +405,15 @@ class TodoItem extends owl.Component {
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// TodoApp // TodoApp
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
function mapStoreToProps(state) { class TodoApp extends owl.ConnectedComponent {
return { components = { TodoItem };
todos: state.todos
};
}
class TodoApp extends owl.Component {
widgets = { TodoItem };
state = { filter: "all" }; state = { filter: "all" };
static mapStoreToProps(state) {
return {
todos: state.todos
};
}
get visibleTodos() { get visibleTodos() {
let todos = this.props.todos; let todos = this.props.todos;
if (this.state.filter === "active") { if (this.state.filter === "active") {
@@ -456,8 +461,6 @@ class TodoApp extends owl.Component {
} }
} }
const ConnectedTodoApp = owl.connect(TodoApp, mapStoreToProps);
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// App Initialization // App Initialization
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -468,7 +471,7 @@ const env = {
store, store,
dispatch: store.dispatch.bind(store), dispatch: store.dispatch.bind(store),
}; };
const app = new ConnectedTodoApp(env); const app = new TodoApp(env);
app.mount(document.body); app.mount(document.body);
`; `;
@@ -483,7 +486,7 @@ const TODO_APP_STORE_XML = `<templates>
<label for="toggle-all"></label> <label for="toggle-all"></label>
<ul class="todo-list"> <ul class="todo-list">
<t t-foreach="visibleTodos" t-as="todo"> <t t-foreach="visibleTodos" t-as="todo">
<t t-widget="TodoItem" t-key="todo.id" id="todo.id" completed="todo.completed" title="todo.title"/> <TodoItem t-key="todo.id" id="todo.id" completed="todo.completed" title="todo.title"/>
</t> </t>
</ul> </ul>
</section> </section>
@@ -910,7 +913,7 @@ const RESPONSIVE = `// In this example, we show how we can modify keys in the gl
// //
// The main idea is to have a "isMobile" key in the environment, then listen // The main idea is to have a "isMobile" key in the environment, then listen
// to resize events and update the env if needed. Then, the whole interface // to resize events and update the env if needed. Then, the whole interface
// will be updated, creating and destroying widgets as needed. // will be updated, creating and destroying components as needed.
// //
// To see this in action, try resizing the window. The application will switch // To see this in action, try resizing the window. The application will switch
// to mobile mode whenever it has less than 768px. // to mobile mode whenever it has less than 768px.
@@ -921,50 +924,51 @@ const RESPONSIVE = `// In this example, we show how we can modify keys in the gl
class Navbar extends owl.Component {} class Navbar extends owl.Component {}
class ControlPanel extends owl.Component { class ControlPanel extends owl.Component {
widgets = { MobileSearchView }; components = { MobileSearchView };
} }
class FormView extends owl.Component { class FormView extends owl.Component {
widgets = { AdvancedWidget }; components = { AdvancedComponent };
} }
class AdvancedWidget extends owl.Component {} class AdvancedComponent extends owl.Component {}
class Chatter extends owl.Component {} class Chatter extends owl.Component {
messages = Array.from(Array(100).keys());
}
class MobileSearchView extends owl.Component {} class MobileSearchView extends owl.Component {}
class App extends owl.Component { class App extends owl.Component {
widgets = { Navbar, ControlPanel, FormView, Chatter }; components = { Navbar, ControlPanel, FormView, Chatter };
}
//------------------------------------------------------------------------------
// Responsive plugin
//------------------------------------------------------------------------------
function setupResponsivePlugin(env) {
const isMobile = () => window.innerWidth <= 768;
env.isMobile = isMobile();
const updateEnv = owl.utils.debounce(() => {
if (env.isMobile !== isMobile()) {
env.isMobile = !env.isMobile;
env.qweb.trigger('update');
}
}, 15);
window.addEventListener("resize", updateEnv);
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Application Startup // Application Startup
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
function isMobile() {
return window.innerWidth <= 768;
}
const env = { const env = {
qweb: new owl.QWeb(TEMPLATES), qweb: new owl.QWeb(TEMPLATES),
isMobile: isMobile()
}; };
setupResponsivePlugin(env);
const app = new App(env); const app = new App(env);
app.mount(document.body); app.mount(document.body);
function updateEnv() {
const _isMobile = isMobile();
if (_isMobile !== env.isMobile) {
app.updateEnv({
isMobile: _isMobile
});
}
}
window.addEventListener("resize", owl.utils.debounce(updateEnv, 20));
`; `;
const RESPONSIVE_XML = `<templates> const RESPONSIVE_XML = `<templates>
@@ -972,38 +976,40 @@ const RESPONSIVE_XML = `<templates>
<div t-name="ControlPanel" class="controlpanel"> <div t-name="ControlPanel" class="controlpanel">
<h2>Control Panel</h2> <h2>Control Panel</h2>
<t t-if="env.isMobile" t-widget="MobileSearchView"/> <MobileSearchView t-if="env.isMobile" />
</div> </div>
<div t-name="FormView" class="formview"> <div t-name="FormView" class="formview">
<h2>Form View</h2> <h2>Form View</h2>
<t t-if="!env.isMobile" t-widget="AdvancedWidget"/> <AdvancedComponent t-if="!env.isMobile" />
</div> </div>
<div t-name="Chatter" class="chatter"> <div t-name="Chatter" class="chatter">
<h2>Chatter</h2> <h2>Chatter</h2>
<t t-foreach="100" t-as="item"><div>Message <t t-esc="item"/></div></t> <t t-foreach="messages" t-as="item"><div>Message <t t-esc="item"/></div></t>
</div> </div>
<div t-name="MobileSearchView">Mobile searchview</div> <div t-name="MobileSearchView">Mobile searchview</div>
<div t-name="AdvancedWidget"> <div t-name="AdvancedComponent">
This widget is only created in desktop mode. This component is only created in desktop mode.
<button>Button!</button> <button>Button!</button>
</div> </div>
<div t-name="App" class="app" t-att-class="{mobile: env.isMobile, desktop: !env.isMobile}"> <div t-name="App" class="app" t-att-class="{mobile: env.isMobile, desktop: !env.isMobile}">
<t t-widget="Navbar"/> <t t-set="maincontent">
<t t-widget="ControlPanel"/> <FormView />
<Chatter />
</t>
<Navbar/>
<ControlPanel/>
<div class="content-wrapper" t-if="!env.isMobile"> <div class="content-wrapper" t-if="!env.isMobile">
<div class="content"> <div class="content">
<t t-widget="FormView"/> <t t-raw="maincontent"/>
<t t-widget="Chatter"/>
</div> </div>
</div> </div>
<t t-else="1"> <t t-else="1">
<t t-widget="FormView"/> <t t-raw="maincontent"/>
<t t-widget="Chatter"/>
</t> </t>
</div> </div>
</templates> </templates>
@@ -1070,7 +1076,7 @@ const RESPONSIVE_CSS = `body {
const SLOTS = `// We show here how slots can be used to create generic components. const SLOTS = `// We show here how slots can be used to create generic components.
// In this example, the Card component is basically only a container. It is not // In this example, the Card component is basically only a container. It is not
// aware of its content. It just knows where it should be (with t-slot). // aware of its content. It just knows where it should be (with t-slot).
// The parent widget define the content with t-set. // The parent component define the content with t-set.
// //
// Note that the t-on-click event, defined in the App template, is executed in // Note that the t-on-click event, defined in the App template, is executed in
// the context of the App component, even though it is inside the Card component // the context of the App component, even though it is inside the Card component
@@ -1091,9 +1097,9 @@ class Counter extends owl.Component {
} }
} }
// Main root widget // Main root component
class App extends owl.Component { class App extends owl.Component {
widgets = {Card, Counter}; components = {Card, Counter};
state = {a: 1, b: 3}; state = {a: 1, b: 3};
inc(key, delta) { inc(key, delta) {
@@ -1126,17 +1132,17 @@ const SLOTS_XML = `<templates>
</div> </div>
<div t-name="App" class="main"> <div t-name="App" class="main">
<t t-widget="Card" title="'Title card A'"> <Card title="'Title card A'">
<t t-set="content">Content of card 1... [<t t-esc="state.a"/>]</t> <t t-set="content">Content of card 1... [<t t-esc="state.a"/>]</t>
<t t-set="footer"><button t-on-click="inc('a', 1)">Increment A</button></t> <t t-set="footer"><button t-on-click="inc('a', 1)">Increment A</button></t>
</t> </Card>
<t t-widget="Card" title="'Title card B'"> <Card title="'Title card B'">
<div t-set="content"> <t t-set="content">
<div>Card 2... [<t t-esc="state.b"/>]</div> <div>Card 2... [<t t-esc="state.b"/>]</div>
<t t-widget="Counter"/> <Counter />
</div> </t>
<t t-set="footer"><button t-on-click="inc('b', -1)">Decrement B</button></t> <t t-set="footer"><button t-on-click="inc('b', -1)">Decrement B</button></t>
</t> </Card>
</div> </div>
</templates>`; </templates>`;
@@ -1186,13 +1192,13 @@ const SLOTS_CSS = `.main {
const ASYNC_COMPONENTS = `// This example will not work if your browser does not support ESNext class fields const ASYNC_COMPONENTS = `// This example will not work if your browser does not support ESNext class fields
// In this example, we have 2 sub widgets, one of them being async (slow). // In this example, we have 2 sub components, one of them being async (slow).
// However, we don't want renderings of the other sub widget to be delayed // However, we don't want renderings of the other sub component to be delayed
// because of the slow widget. We use the 't-asyncroot' directive for this // because of the slow component. We use the 't-asyncroot' directive for this
// purpose. Try removing it to see the difference. // purpose. Try removing it to see the difference.
class App extends owl.Component { class App extends owl.Component {
widgets = {SlowWidget, NotificationList}; components = {SlowComponent, NotificationList};
state = { value: 0, notifs: [] }; state = { value: 0, notifs: [] };
increment() { increment() {
@@ -1206,9 +1212,9 @@ class App extends owl.Component {
} }
} }
class SlowWidget extends owl.Component { class SlowComponent extends owl.Component {
willUpdateProps() { willUpdateProps() {
// simulate a widget that needs to perform async stuff (e.g. an RPC) // simulate a component that needs to perform async stuff (e.g. an RPC)
// with the updated props before re-rendering itself // with the updated props before re-rendering itself
return new Promise(resolve => setTimeout(resolve, 1500)); return new Promise(resolve => setTimeout(resolve, 1500));
} }
@@ -1224,11 +1230,11 @@ app.mount(document.body);
const ASYNC_COMPONENTS_XML = `<templates> const ASYNC_COMPONENTS_XML = `<templates>
<div t-name="App" class="app"> <div t-name="App" class="app">
<button t-on-click="increment">Increment</button> <button t-on-click="increment">Increment</button>
<t t-widget="SlowWidget" value="state.value"/> <SlowComponent value="state.value"/>
<t t-widget="NotificationList" t-asyncroot="1" notifications="state.notifs"/> <NotificationList t-asyncroot="1" notifications="state.notifs"/>
</div> </div>
<div t-name="SlowWidget" class="value" > <div t-name="SlowComponent" class="value" >
Current value: <t t-esc="props.value"/> Current value: <t t-esc="props.value"/>
</div> </div>
@@ -1350,7 +1356,7 @@ export const SAMPLES = [
description: "Lifecycle demo", description: "Lifecycle demo",
code: LIFECYCLE_DEMO, code: LIFECYCLE_DEMO,
xml: LIFECYCLE_DEMO_XML, xml: LIFECYCLE_DEMO_XML,
css: LIFECYCLE_CSS, css: LIFECYCLE_CSS
}, },
{ {
description: "Todo List App (with store)", description: "Todo List App (with store)",
+5 -4
View File
@@ -24,14 +24,14 @@
<a class="btn flash" t-on-click="downloadCode" title="Download a Zip with this Code"><i class="fas fa-download"></i></a> <a class="btn flash" t-on-click="downloadCode" title="Download a Zip with this Code"><i class="fas fa-download"></i></a>
<a class="layout-selector flash" t-on-click="toggleLayout" title="Toggle Layout"><i class="fas" t-att-class="state.splitLayout ? 'fa-toggle-on' : 'fa-toggle-off'"></i></a> <a class="layout-selector flash" t-on-click="toggleLayout" title="Toggle Layout"><i class="fas" t-att-class="state.splitLayout ? 'fa-toggle-on' : 'fa-toggle-off'"></i></a>
</div> </div>
<t t-widget="TabbedEditor" <TabbedEditor
js="state.js" js="state.js"
css="!state.splitLayout and state.css" css="!state.splitLayout and state.css"
xml="!state.splitLayout and state.js" xml="!state.splitLayout and state.xml"
t-att-style="topEditorStyle"/> t-att-style="topEditorStyle"/>
<t t-if="state.splitLayout"> <t t-if="state.splitLayout">
<div class="separator horizontal"/> <div class="separator horizontal"/>
<t t-widget="TabbedEditor" t-keepalive="1" <TabbedEditor t-keepalive="1"
js="false" js="false"
css="state.css" css="state.css"
xml="state.xml" xml="state.xml"
@@ -48,7 +48,8 @@
<div class="note">Note: these examples require a recent browser to work without a transpilation step. </div> <div class="note">Note: these examples require a recent browser to work without a transpilation step. </div>
</div> </div>
<div t-if="state.error" class="error"> <div t-if="state.error" class="error">
<t t-esc="state.error"/> <h3>Error</h3>
<pre t-esc="state.error"/>
</div> </div>
<div class="content" t-ref="content"/> <div class="content" t-ref="content"/>
</div> </div>