Compare commits

..

17 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
29 changed files with 5921 additions and 808 deletions
+2 -2
View File
@@ -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:
- [owl-0.16.0.js](https://github.com/odoo/owl/releases/download/v0.16.0/owl.js)
- [owl-0.16.0.min.js](https://github.com/odoo/owl/releases/download/v0.16.0/owl.min.js)
- [owl-0.17.0.js](https://github.com/odoo/owl/releases/download/v0.17.0/owl.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:
+119 -26
View File
@@ -21,6 +21,7 @@
- [References](#references)
- [Slots](#slots)
- [Asynchronous Rendering](#asynchronous-rendering)
- [Error Handling](#error-handling)
## Overview
@@ -116,11 +117,29 @@ 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
`dev`, this will be used to validate the props each time the component is
created/updated. See [Props Validation](#props-validation) for more information.
- **`defaultProps`** (Object, optional): if given, this object define default
```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
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.
```js
class Counter extends owl.Component {
static defaultProps = {
initialValue: 0
};
}
```
### Methods
We explain here all the public methods of the `Component` class.
@@ -160,7 +179,7 @@ We explain here all the public methods of the `Component` class.
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
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.
@@ -170,15 +189,16 @@ A solid and robust component system needs useful hooks/methods to help
developers write components. Here is a complete description of the lifecycle of
a owl component:
| Method | Description |
| ------------------------------------------------ | ----------------------------------------------------- |
| **[constructor](#constructorparent-props)** | constructor |
| **[willStart](#willstart)** | async, before first rendering |
| **[mounted](#mounted)** | just after component is rendered and added to the DOM |
| **[willUpdateProps](#willupdatepropsnextprops)** | async, before props update |
| **[willPatch](#willpatch)** | just before the DOM is patched |
| **[patched](#patchedsnapshot)** | just after the DOM is patched |
| **[willUnmount](#willunmount)** | just before removing component from DOM |
| Method | Description |
| ------------------------------------------------ | ------------------------------------------------------------ |
| **[constructor](#constructorparent-props)** | constructor |
| **[willStart](#willstart)** | async, before first rendering |
| **[mounted](#mounted)** | just after component is rendered and added to the DOM |
| **[willUpdateProps](#willupdatepropsnextprops)** | async, before props update |
| **[willPatch](#willpatch)** | just before the DOM is patched |
| **[patched](#patchedsnapshot)** | just after the DOM is patched |
| **[willUnmount](#willunmount)** | just before removing component from DOM |
| **[catchError](#catcherrorerror)** | catch errors (see [error handling section](#error-handling)) |
Notes:
@@ -319,6 +339,12 @@ the DOM. This is a good place to remove some listeners, for example.
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`
@@ -353,22 +379,22 @@ 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
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
`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);
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);
}
```
@@ -786,7 +812,8 @@ of the props. Here is how it works in Owl:
- props are validated whenever a component is created/updated
- props are only validated in `dev` mode (see [tooling page](tooling.md#development-mode))
- if a key does not match the description, an error is thrown
- it only validates keys defined in (static) `props`. Additional keys in (component) `props` are not validated.
- it validates keys defined in (static) `props`. Additional keys given by the
parent will cause an error.
For example:
@@ -814,15 +841,16 @@ class ComponentB extends owl.Component {
- it is an object or a list of strings
- a list of strings is a simplified props definition, which only lists the name
of the props.
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`
(in that case, validation is only done if there is a value)
- valid types are: `Number, String, Boolean, Object, Array, Date, Function`, and all
constructor functions (so, if you have a `Person` class, it can be used as a type)
- arrays are homogeneous (all elements have the same type/shape)
For each key, a `prop` definition is either a constructor, a list of constructors, or an object:
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
the props `id` as a number
- a list of constructors. In that case, this means that we allow more than one
@@ -840,6 +868,11 @@ Examples:
static props = ['message', 'id', 'date'];
```
```js
// size is optional
static props = ['message', 'size?'];
```
```js
static props = {
messageIds: {type: Array, element: Number}, // list of number
@@ -854,7 +887,8 @@ Examples:
url: String
]}, // object, with keys id (number), name (string, optional) and url (string)
someFlag: Boolean, // a boolean, mandatory (even if `false`)
someVal: [Boolean, Date] // either a boolean or a date
someVal: [Boolean, Date], // either a boolean or a date
otherValue: true, // indicates that it is a prop
};
```
@@ -994,3 +1028,62 @@ Here are a few tips on how to work with asynchronous components:
<AsyncChild t-asyncroot="1"/>
</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
});
```
+7
View File
@@ -122,6 +122,13 @@ It's API is quite simple:
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
to QWeb's global registry. Globally registered Components can be used in
templates (see the `t-component` directive). This is useful for commonly used
+26 -33
View File
@@ -89,7 +89,7 @@ mutation is not allowed (and should throw an error). Mutations are synchronous.
```js
const mutations = {
setLoginState({ state }, loginState) {
state.loginState = loginState;
state.loginState = loginState;
}
};
```
@@ -165,17 +165,16 @@ const getters = {
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
is a string or a number.
### Connecting a Component
By default, an Owl `Component` is not connected to any store. The `connect`
function is there to create sub Components that are connected versions of
Components.
At some point, we need a way to access the state in the store from a component.
By default, an Owl `Component` is not connected to any store. To do that, we
need to create a component inheriting from `OwlComponent`:
```javascript
const actions = {
@@ -193,19 +192,18 @@ const state = {
};
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() {
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
@@ -214,45 +212,40 @@ const counter = new ConnectedCounter({ store, qweb });
</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
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
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
given, generates a function that uses revision numbers, incremented at
each state change)
- `deep`: [only useful if no hashFunction is given] if false, only watch
for top level state changes (true by default)
The `connect` function returns a sub class of the given `Component` which is
connected to the `store`.
from the `state` of the `Store` and returns them as a dict.
- `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`)
- `hashFunction`: the function to use to detect changes in the state (if not
given, generates a function that uses revision numbers, incremented at
each state change)
- `deep` (boolean): [only useful if no hashFunction is given] if `false`, only watch
for top level state changes (`true` by default)
### Semantics
The `Store` and the `connect` function try to be smart and to optimize as much
as possible the rendering and update process. What is important to know is:
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
- 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
- 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.
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.
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
The playground is an important application designed to help learning and
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "owl",
"version": "0.16.0",
"version": "0.17.0",
"description": "Odoo Web Library (OWL)",
"main": "src/index.ts",
"scripts": {
+89 -106
View File
@@ -1,5 +1,5 @@
import { Observer } from "./observer";
import { QWeb, CompiledTemplate } from "./qweb_core";
import { QWeb, CompiledTemplate, UTILS } from "./qweb_core";
import { h, patch, VNode } from "./vdom";
/**
@@ -119,9 +119,6 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
if (defaultProps) {
props = this.__applyDefaultProps(props, defaultProps);
}
if (QWeb.dev) {
this.__validateProps(props || {});
}
// is this a good idea?
// Pro: if props is empty, we can create easily a component
// Con: this is not really safe
@@ -135,6 +132,11 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
parent.__owl__.children[id] = this;
} else {
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);
@@ -241,6 +243,12 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
*/
willUnmount() {}
/**
* catchError is a method called whenever some error happens in the rendering or
* lifecycle hooks of a child.
*/
catchError(error: Error): void {}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
@@ -272,7 +280,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
}
}
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__;
if (!__owl__.isMounted) {
return;
@@ -281,7 +289,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
if (shouldPatch) {
patchQueue = [];
}
const renderVDom = this.__render(force, patchQueue);
const renderVDom = this.__render(force, patchQueue, scope, vars);
const renderId = __owl__.renderId;
await renderVDom;
@@ -396,7 +404,11 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
for (let key in handlers) {
handlers[key]();
}
this.mounted();
try {
this.mounted();
} catch (e) {
errorHandler(e, this);
}
}
__callWillUnmount() {
@@ -415,7 +427,9 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
async __updateProps(
nextProps: Props,
forceUpdate: boolean = false,
patchQueue?: any[]
patchQueue?: any[],
scope?: any,
vars?: any
): Promise<void> {
const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
if (shouldUpdate) {
@@ -423,12 +437,9 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
if (defaultProps) {
nextProps = this.__applyDefaultProps(nextProps, defaultProps);
}
if (QWeb.dev) {
this.__validateProps(nextProps);
}
await this.willUpdateProps(nextProps);
this.props = nextProps;
await this.render(forceUpdate, patchQueue);
await this.render(forceUpdate, patchQueue, scope, vars);
}
}
@@ -442,15 +453,20 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
__owl__.vnode = patch(target, vnode);
}
__prepare(): Promise<VNode> {
__prepare(scope?: Object, vars?: any): Promise<VNode> {
const __owl__ = this.__owl__;
__owl__.renderProps = this.props;
__owl__.renderPromise = this.__prepareAndRender();
__owl__.renderPromise = this.__prepareAndRender(scope, vars);
return __owl__.renderPromise;
}
async __prepareAndRender(): Promise<VNode> {
await this.willStart();
async __prepareAndRender(scope?: Object, vars?: any): Promise<VNode> {
try {
await this.willStart();
} catch (e) {
errorHandler(e, this);
return Promise.resolve(h("div"));
}
const __owl__ = this.__owl__;
if (__owl__.isDestroyed) {
return Promise.resolve(h("div"));
@@ -481,10 +497,15 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
}
__owl__.render = qweb.render.bind(qweb, this.template);
this.__observeState();
return this.__render();
return this.__render(false, [], scope, vars);
}
async __render(force: boolean = false, patchQueue: any[] = []): Promise<VNode> {
async __render(
force: boolean = false,
patchQueue: any[] = [],
scope?: Object,
vars?: any
): Promise<VNode> {
const __owl__ = this.__owl__;
__owl__.renderId++;
const promises: Promise<void>[] = [];
@@ -495,13 +516,21 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
if (__owl__.observer) {
__owl__.observer.allowMutations = false;
}
let vnode = __owl__.render!(this, {
promises,
handlers: __owl__.boundHandlers,
mountedHandlers: __owl__.mountedHandlers,
forceUpdate: force,
patchQueue
});
let vnode;
try {
vnode = __owl__.render!(this, {
promises,
handlers: __owl__.boundHandlers,
mountedHandlers: __owl__.mountedHandlers,
forceUpdate: force,
patchQueue,
scope,
vars
});
} catch (e) {
vnode = __owl__.vnode || h("div");
errorHandler(e, this);
}
patch.push(vnode);
if (__owl__.observer) {
__owl__.observer.allowMutations = true;
@@ -577,96 +606,50 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
* 3) Call 'patched' on the component of each patch, in inverse order
*/
__applyPatchQueue(patchQueue: any[]) {
const patchLen = patchQueue.length;
for (let i = 0; i < patchLen; i++) {
const patch = patchQueue[i];
patch.push(patch[0].willPatch());
}
for (let i = 0; i < patchLen; i++) {
const patch = patchQueue[i];
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]}' (component '${this.constructor.name}')`);
}
let component = this;
try {
const patchLen = patchQueue.length;
for (let i = 0; i < patchLen; i++) {
const patch = patchQueue[i];
component = patch[0];
patch.push(patch[0].willPatch());
}
} else if (propsDef) {
// propsDef is an object now
for (let propName in propsDef) {
if (!(propName in props)) {
if (propsDef[propName] && !propsDef[propName].optional) {
throw new Error(`Missing props '${propName}' (component '${this.constructor.name}')`);
} else {
break;
}
}
let isValid = isValidProp(props[propName], propsDef[propName]);
if (!isValid) {
throw new Error(
`Props '${propName}' of invalid type in component '${this.constructor.name}'`
);
}
for (let i = 0; i < patchLen; i++) {
const patch = patchQueue[i];
patch[0].__patch(patch[1]);
}
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
//------------------------------------------------------------------------------
/**
* Check if an invidual prop value matches its (static) prop definition
*/
function isValidProp(prop, propDef): boolean {
if (typeof propDef === "function") {
// Check if a value is constructed by some Constructor. Note that there is a
// slight abuse of language: we want to consider primitive values as well.
//
// So, even though 1 is not an instance of Number, we want to consider that
// it is valid.
if (typeof prop === "object") {
return prop instanceof propDef;
}
return typeof prop === propDef.name.toLowerCase();
} else if (propDef instanceof Array) {
// If this code is executed, this means that we want to check if a prop
// matches at least one of its descriptor.
let result = false;
for (let i = 0, iLen = propDef.length; i < iLen; i++) {
result = result || isValidProp(prop, propDef[i]);
}
return result;
function errorHandler(error, component) {
let canCatch = false;
let qweb = component.env.qweb;
let root = component;
while (component && !(canCatch = component.catchError !== Component.prototype.catchError)) {
root = component;
component = component.__owl__.parent;
}
// 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);
}
console.error(error);
// we trigger error on QWeb so it can be logged/handled
qweb.trigger("error", error);
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
@@ -15,7 +15,7 @@ import "./qweb_extensions";
import { QWeb } from "./qweb_core";
export { QWeb };
export { connect, Store } from "./store";
export { Store, ConnectedComponent } from "./store";
import * as _utils from "./utils";
export const __info__ = {};
+119 -29
View File
@@ -1,4 +1,4 @@
import { VNode, h } from "./vdom";
import { VNode, h, patch } from "./vdom";
import { QWebVar, compileExpr } from "./qweb_expressions";
import { EventBus } from "./event_bus";
@@ -117,7 +117,27 @@ function parseXML(xml: string): Document {
const parser = new DOMParser();
const doc = parser.parseFromString(xml, "text/xml");
if (doc.getElementsByTagName("parsererror").length) {
throw new Error("Invalid XML in template");
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;
}
@@ -268,45 +288,66 @@ export class QWeb extends EventBus {
return template.fn.call(this, context, extra);
}
_compile(name: string, elem: Element, parentNode?: number): 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 ctx = new Context(name);
if (parentNode) {
ctx.nextID = parentNode + 1;
ctx.parentNode = parentNode;
if (parentContext) {
ctx.variables = Object.create(parentContext.variables);
ctx.nextID = parentContext.parentNode! + 1;
ctx.parentNode = parentContext.parentNode!;
ctx.allowMultipleRoots = true;
ctx.addLine(`let c${parentNode} = extra.parentNode;`);
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);
if (ctx.shouldProtectContext) {
ctx.code.unshift(" context = Object.create(context);");
}
if (ctx.shouldDefineOwner) {
// this is necessary to prevent some directives (t-forach for ex) to
// pollute the rendering context by adding some keys in it.
ctx.code.unshift(" let owner = context;");
}
if (ctx.shouldDefineQWeb) {
ctx.code.unshift(" let QWeb = this.constructor;");
}
if (ctx.shouldDefineUtils) {
ctx.code.unshift(" let utils = this.utils;");
if (!parentContext) {
if (ctx.shouldDefineResult) {
ctx.addLine(`return result;`);
} else {
if (!ctx.rootNode) {
throw new Error(`A template should have one root node (${ctx.templateName})`);
}
ctx.addLine(`return vn${ctx.rootNode};`);
}
}
if (!parentNode) {
if (!ctx.rootNode) {
throw new Error("A template should have one root node");
}
ctx.addLine(`return vn${ctx.rootNode};`);
}
let code = ctx.generateCode();
let template;
try {
template = new Function("context", "extra", ctx.code.join("\n")) as CompiledTemplate;
template = new Function("context", "extra", code.join("\n")) as CompiledTemplate;
} catch (e) {
const templateName = ctx.templateName.replace(/`/g, "'");
console.groupCollapsed(`Invalid Code generated by ${templateName}`);
console.warn(ctx.code.join("\n"));
console.warn(code.join("\n"));
console.groupEnd();
throw new Error(
`Invalid generated code while compiling template '${templateName}': ${e.message}`
@@ -649,13 +690,18 @@ export class Context {
rootContext: Context;
caller: Element | undefined;
shouldDefineOwner: boolean = false;
shouldDefineParent: boolean = false;
shouldDefineQWeb: boolean = false;
shouldDefineUtils: boolean = false;
shouldDefineResult: boolean = false;
shouldProtectContext: boolean = false;
shouldTrackScope: boolean = false;
inLoop: boolean = false;
inPreTag: boolean = false;
templateName: string;
allowMultipleRoots: boolean = false;
hasParentWidget: boolean = false;
scopeVars: any[] = [];
constructor(name?: string) {
this.rootContext = this;
@@ -668,6 +714,44 @@ export class Context {
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 {
if (
!this.allowMultipleRoots &&
@@ -696,9 +780,15 @@ export class Context {
this.indentLevel--;
}
addLine(line: string) {
addLine(line: string): number {
const prefix = new Array(this.indentLevel + 2).join(" ");
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) {
+5 -5
View File
@@ -255,11 +255,11 @@ QWeb.addDirective({
ctx.addLine(`var _length${keysID} = _${keysID}.length;`);
ctx.addLine(`for (let i = 0; i < _length${keysID}; i++) {`);
ctx.indent();
ctx.addLine(`context.${name}_first = i === 0;`);
ctx.addLine(`context.${name}_last = i === _length${keysID} - 1;`);
ctx.addLine(`context.${name}_index = i;`);
ctx.addLine(`context.${name} = _${keysID}[i];`);
ctx.addLine(`context.${name}_value = _${valuesID}[i];`);
ctx.addToScope(name + '_first', 'i === 0');
ctx.addToScope(name + '_last', `i === _length${keysID} - 1`);
ctx.addToScope(name + '_index', 'i');
ctx.addToScope(name, `_${keysID}[i]`);
ctx.addToScope(name + '_value', `_${valuesID}[i]`);
const nodeCopy = <Element>node.cloneNode(true);
let shouldWarn = nodeCopy.tagName !== "t" && !nodeCopy.hasAttribute("t-key");
if (!shouldWarn && node.tagName === "t") {
+162 -19
View File
@@ -190,6 +190,19 @@ const T_COMPONENT_MODS_CODE = Object.assign({}, MODS_CODE, {
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-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
@@ -359,6 +372,7 @@ QWeb.addDirective({
ctx.addLine("//COMPONENT");
ctx.rootContext.shouldDefineOwner = true;
ctx.rootContext.shouldDefineQWeb = true;
ctx.rootContext.shouldDefineParent = true;
ctx.rootContext.shouldDefineUtils = true;
let keepAlive = node.getAttribute("t-keepalive") ? true : false;
let async = node.getAttribute("t-asyncroot") ? true : false;
@@ -471,7 +485,7 @@ QWeb.addDirective({
if (tattClass) {
let tattExpr = ctx.formatExpression(tattClass);
if (tattExpr[0] !== "{" || tattExpr[tattExpr.length - 1] !== "}") {
tattExpr = `this.utils.toObj(${tattExpr})`;
tattExpr = `utils.toObj(${tattExpr})`;
}
if (classAttr) {
ctx.addLine(`Object.assign(${classObj}, ${tattExpr})`);
@@ -516,16 +530,28 @@ QWeb.addDirective({
}
ctx.addLine(
`let w${componentID} = ${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) {
ctx.addLine(`const patchQueue${componentID} = [];`);
ctx.addLine(
`c${ctx.parentNode}.push(w${componentID} && w${componentID}.__owl__.pvnode || null);`
);
} 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${componentID} = {${propStr}};`);
ctx.addIf(
@@ -545,16 +571,27 @@ QWeb.addDirective({
ctx.addLine(
`let W${componentID} = context.components && context.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}];`
);
// maybe only do this in dev mode...
ctx.addLine(
`if (!W${componentID}) {throw new Error('Cannot find the definition of component "' + componentKey${componentID} + '"')}`
);
ctx.addLine(`w${componentID} = new W${componentID}(owner, props${componentID});`);
ctx.addLine(`context.__owl__.cmap[${templateID}] = w${componentID}.__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
if (node.childNodes.length) {
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 slotNodes = clone.querySelectorAll("[t-set]");
const slotId = qweb.nextSlotId++;
@@ -565,7 +602,7 @@ QWeb.addDirective({
slotNode.parentElement!.removeChild(slotNode);
const key = slotNode.getAttribute("t-set")!;
slotNode.removeAttribute("t-set");
const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx.parentNode!);
const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx);
qweb.slots[`${slotId}_${key}`] = slotFn.bind(qweb);
}
}
@@ -574,24 +611,37 @@ QWeb.addDirective({
for (let child of Object.values(clone.childNodes)) {
t.appendChild(child);
}
const slotFn = qweb._compile(`slot_default_template`, t, ctx.parentNode!);
const slotFn = qweb._compile(`slot_default_template`, t, ctx);
qweb.slots[`${slotId}_default`] = slotFn.bind(qweb);
}
}
ctx.addLine(`def${defID} = w${componentID}.__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
let registerCode = `c${ctx.parentNode}[_${dummyID}_index]=pvnode;`;
if (shouldProxy) {
registerCode = `utils.defineProxy(vn${ctx.rootNode}, pvnode);`;
}
ctx.addLine(
`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}}}});c${
ctx.parentNode
}[_${dummyID}_index]=pvnode;w${componentID}.__owl__.pvnode = pvnode;});`
`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.addElse();
// need to update component
const patchQueueCode = async ? `patchQueue${componentID}` : "extra.patchQueue";
if (QWeb.dev) {
ctx.addLine(`utils.validateProps(w${componentID}.constructor, props${componentID})`);
}
ctx.addLine(
`def${defID} = def${defID} || w${componentID}.__updateProps(props${componentID}, extra.forceUpdate, ${patchQueueCode});`
`def${defID} = def${defID} || w${componentID}.__updateProps(props${componentID}, extra.forceUpdate, ${patchQueueCode}${scopeVars &&
", " + scopeVars});`
);
let keepAliveCode = "";
if (keepAlive) {
@@ -600,9 +650,7 @@ QWeb.addDirective({
ctx.addLine(
`def${defID} = def${defID}.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};${
tattStyle ? `w${componentID}.el.style=${tattStyle};` : ""
}let pvnode=w${componentID}.__owl__.pvnode;${keepAliveCode}c${
ctx.parentNode
}[_${dummyID}_index]=pvnode;});`
}let pvnode=w${componentID}.__owl__.pvnode;${keepAliveCode}${registerCode}});`
);
ctx.closeIf();
@@ -626,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
//------------------------------------------------------------------------------
@@ -670,12 +812,13 @@ QWeb.addDirective({
priority: 80,
atNodeEncounter({ ctx, value }): boolean {
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(
`slot${slotKey}(context.__owl__.parent, Object.assign({}, extra, {parentNode: c${
ctx.parentNode
}}));`
}, vars: extra.vars, parent: owner}));`
);
ctx.closeIf();
return true;
+110 -127
View File
@@ -7,7 +7,7 @@ import { Observer } from "./observer";
*
* We have here:
* - a Store class
* - a connect function
* - the ConnectedComponent class
*
* 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
@@ -185,139 +185,122 @@ function deepRevNumber<T extends Object>(o: T): number {
return 0;
}
type Constructor<T> = new (...args: any[]) => T;
interface EnvWithStore extends Env {
store: Store;
}
type HashFunction = (a: any, b: any) => number;
interface StoreOptions {
getStore?(Env): Store;
hashFunction?: HashFunction;
deep?: boolean;
}
export function connect<E extends EnvWithStore, P, S>(
Comp: Constructor<Component<E, P, S>>,
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;
};
export class ConnectedComponent<T extends Env, P, S> extends Component<T, P, S> {
deep: boolean = true;
getStore(env) {
return env.store;
}
const Result = class extends Comp {
constructor(parent, props?: any) {
const env = parent instanceof Component ? parent.env : parent;
const store = getStore(env);
const ownProps = Object.assign({}, props || {});
const storeProps = mapStoreToProps(store.state, ownProps, store.getters);
const mergedProps = Object.assign({}, props || {}, storeProps);
super(parent, mergedProps);
(<any>this.__owl__).ownProps = ownProps;
(<any>this.__owl__).currentStoreProps = storeProps;
(<any>this.__owl__).store = store;
(<any>this.__owl__).storeHash = (<HashFunction>hashFunction)(
{
state: store.state,
storeProps: storeProps,
revNumber,
deepRevNumber
},
{
currentStoreProps: storeProps
hashFunction: HashFunction = ({ storeProps }, options) => {
let refFunction = this.deep ? deepRevNumber : revNumber;
if ("__owl__" in storeProps) {
return refFunction(storeProps);
}
const { currentStoreProps } = options;
let hash = 0;
for (let key in storeProps) {
const val = storeProps[key];
const hashVal = refFunction(val);
if (hashVal === 0) {
if (val !== currentStoreProps[key]) {
options.didChange = true;
}
);
}
/**
* 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 = 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;
await this.__updateProps(ownProps, false);
} else {
hash += hashVal;
}
}
__updateProps(nextProps, forceUpdate, patchQueue?: any[]) {
const __owl__ = <any>this.__owl__;
__owl__.currentUpdateId = __owl__.store._updateId;
if (__owl__.ownProps !== nextProps) {
__owl__.currentStoreProps = mapStoreToProps(
__owl__.store.state,
nextProps,
__owl__.store.getters
);
}
__owl__.ownProps = nextProps;
const mergedProps = Object.assign({}, nextProps, __owl__.currentStoreProps);
return super.__updateProps(mergedProps, forceUpdate, patchQueue);
}
return hash;
};
// we assign here a unique name to the resulting anonymous class.
// this is necessary for Owl to be able to properly deduce templates.
// Otherwise, all connected components would have the same name, and then
// each component after the first will necessarily have the same template.
let name = `Connected${Comp.name}`;
Object.defineProperty(Result, "name", { value: name });
return Result;
static mapStoreToProps(storeState, ownProps, getters) {
return {};
}
constructor(parent, props?: any) {
super(parent, props);
const store = this.getStore(this.env);
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);
}
}
+8 -6
View File
@@ -5,13 +5,14 @@ exports[`animations t-transition combined with component 1`] = `
) {
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 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;
c1.push(null);
let props4 = {};
@@ -27,8 +28,8 @@ exports[`animations t-transition combined with component 1`] = `
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 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
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;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
w4.destroy();
@@ -48,6 +49,7 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
) {
let utils = this.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
@@ -55,7 +57,7 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
if (context['state'].display) {
//COMPONENT
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;
c1.push(null);
let props4 = {};
@@ -71,8 +73,8 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
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 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
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;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
w4.destroy();
+424 -75
View File
@@ -5,6 +5,7 @@ exports[`async rendering delayed component with t-asyncroot directive 1`] = `
) {
let utils = this.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
@@ -24,7 +25,7 @@ exports[`async rendering delayed component with t-asyncroot directive 1`] = `
c1.push(vn5);
//COMPONENT
let def7;
let w8 = 8 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[8]] : false;
let w8 = 8 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[8]] : false;
let _6_index = c5.length;
c5.push(null);
let props8 = {val:context['state'].val};
@@ -40,8 +41,8 @@ exports[`async rendering delayed component with t-asyncroot directive 1`] = `
let componentKey8 = \`Child\`;
let W8 = context.components && context.components[componentKey8] || QWeb.components[componentKey8];
if (!W8) {throw new Error('Cannot find the definition of component \\"' + componentKey8 + '\\"')}
w8 = new W8(owner, props8);
context.__owl__.cmap[8] = w8.__owl__.id;
w8 = new W8(parent, props8);
parent.__owl__.cmap[8] = w8.__owl__.id;
def7 = w8.__prepare();
def7 = def7.then(vnode=>{let pvnode=h(vnode.sel, {key: 8, hook: {insert(vn) {let nvn=w8.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w8.destroy();}}});c5[_6_index]=pvnode;w8.__owl__.pvnode = pvnode;});
} else {
@@ -51,7 +52,7 @@ exports[`async rendering delayed component with t-asyncroot directive 1`] = `
extra.promises.push(def7);
//COMPONENT
let def10;
let w11 = 11 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[11]] : false;
let w11 = 11 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[11]] : false;
let _9_index = c5.length;
const patchQueue11 = [];
c5.push(w11 && w11.__owl__.pvnode || null);
@@ -68,8 +69,8 @@ exports[`async rendering delayed component with t-asyncroot directive 1`] = `
let componentKey11 = \`AsyncChild\`;
let W11 = context.components && context.components[componentKey11] || QWeb.components[componentKey11];
if (!W11) {throw new Error('Cannot find the definition of component \\"' + componentKey11 + '\\"')}
w11 = new W11(owner, props11);
context.__owl__.cmap[11] = w11.__owl__.id;
w11 = new W11(parent, props11);
parent.__owl__.cmap[11] = w11.__owl__.id;
def10 = w11.__prepare();
def10 = def10.then(vnode=>{let pvnode=h(vnode.sel, {key: 11, hook: {insert(vn) {let nvn=w11.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w11.destroy();}}});c5[_9_index]=pvnode;w11.__owl__.pvnode = pvnode;});
} else {
@@ -86,6 +87,7 @@ exports[`async rendering fast component with t-asyncroot directive 1`] = `
) {
let utils = this.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
@@ -105,7 +107,7 @@ exports[`async rendering fast component with t-asyncroot directive 1`] = `
c1.push(vn5);
//COMPONENT
let def7;
let w8 = 8 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[8]] : false;
let w8 = 8 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[8]] : false;
let _6_index = c5.length;
const patchQueue8 = [];
c5.push(w8 && w8.__owl__.pvnode || null);
@@ -122,8 +124,8 @@ exports[`async rendering fast component with t-asyncroot directive 1`] = `
let componentKey8 = \`Child\`;
let W8 = context.components && context.components[componentKey8] || QWeb.components[componentKey8];
if (!W8) {throw new Error('Cannot find the definition of component \\"' + componentKey8 + '\\"')}
w8 = new W8(owner, props8);
context.__owl__.cmap[8] = w8.__owl__.id;
w8 = new W8(parent, props8);
parent.__owl__.cmap[8] = w8.__owl__.id;
def7 = w8.__prepare();
def7 = def7.then(vnode=>{let pvnode=h(vnode.sel, {key: 8, hook: {insert(vn) {let nvn=w8.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w8.destroy();}}});c5[_6_index]=pvnode;w8.__owl__.pvnode = pvnode;});
} else {
@@ -133,7 +135,7 @@ exports[`async rendering fast component with t-asyncroot directive 1`] = `
def7.then(w8.__applyPatchQueue.bind(w8, patchQueue8));
//COMPONENT
let def10;
let w11 = 11 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[11]] : false;
let w11 = 11 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[11]] : false;
let _9_index = c5.length;
c5.push(null);
let props11 = {val:context['state'].val};
@@ -149,8 +151,8 @@ exports[`async rendering fast component with t-asyncroot directive 1`] = `
let componentKey11 = \`AsyncChild\`;
let W11 = context.components && context.components[componentKey11] || QWeb.components[componentKey11];
if (!W11) {throw new Error('Cannot find the definition of component \\"' + componentKey11 + '\\"')}
w11 = new W11(owner, props11);
context.__owl__.cmap[11] = w11.__owl__.id;
w11 = new W11(parent, props11);
parent.__owl__.cmap[11] = w11.__owl__.id;
def10 = w11.__prepare();
def10 = def10.then(vnode=>{let pvnode=h(vnode.sel, {key: 11, hook: {insert(vn) {let nvn=w11.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w11.destroy();}}});c5[_9_index]=pvnode;w11.__owl__.pvnode = pvnode;});
} else {
@@ -167,6 +169,7 @@ exports[`async rendering t-component with t-asyncroot directive: mixed re-render
) {
let utils = this.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
@@ -186,7 +189,7 @@ exports[`async rendering t-component with t-asyncroot directive: mixed re-render
c1.push(vn5);
//COMPONENT
let def7;
let w8 = 8 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[8]] : false;
let w8 = 8 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[8]] : false;
let _6_index = c5.length;
c5.push(null);
let props8 = {val:context['state'].val};
@@ -202,8 +205,8 @@ exports[`async rendering t-component with t-asyncroot directive: mixed re-render
let componentKey8 = \`Child\`;
let W8 = context.components && context.components[componentKey8] || QWeb.components[componentKey8];
if (!W8) {throw new Error('Cannot find the definition of component \\"' + componentKey8 + '\\"')}
w8 = new W8(owner, props8);
context.__owl__.cmap[8] = w8.__owl__.id;
w8 = new W8(parent, props8);
parent.__owl__.cmap[8] = w8.__owl__.id;
def7 = w8.__prepare();
def7 = def7.then(vnode=>{let pvnode=h(vnode.sel, {key: 8, hook: {insert(vn) {let nvn=w8.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w8.destroy();}}});c5[_6_index]=pvnode;w8.__owl__.pvnode = pvnode;});
} else {
@@ -213,7 +216,7 @@ exports[`async rendering t-component with t-asyncroot directive: mixed re-render
extra.promises.push(def7);
//COMPONENT
let def10;
let w11 = 11 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[11]] : false;
let w11 = 11 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[11]] : false;
let _9_index = c5.length;
const patchQueue11 = [];
c5.push(w11 && w11.__owl__.pvnode || null);
@@ -230,8 +233,8 @@ exports[`async rendering t-component with t-asyncroot directive: mixed re-render
let componentKey11 = \`AsyncChild\`;
let W11 = context.components && context.components[componentKey11] || QWeb.components[componentKey11];
if (!W11) {throw new Error('Cannot find the definition of component \\"' + componentKey11 + '\\"')}
w11 = new W11(owner, props11);
context.__owl__.cmap[11] = w11.__owl__.id;
w11 = new W11(parent, props11);
parent.__owl__.cmap[11] = w11.__owl__.id;
def10 = w11.__prepare();
def10 = def10.then(vnode=>{let pvnode=h(vnode.sel, {key: 11, hook: {insert(vn) {let nvn=w11.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w11.destroy();}}});c5[_9_index]=pvnode;w11.__owl__.pvnode = pvnode;});
} else {
@@ -248,6 +251,7 @@ exports[`class and style attributes with t-component dynamic t-att-style is prop
) {
let utils = this.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
@@ -255,7 +259,7 @@ exports[`class and style attributes with t-component dynamic t-att-style is prop
//COMPONENT
let def3;
const _5 = context['state'].style;
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
@@ -271,8 +275,8 @@ exports[`class and style attributes with t-component dynamic t-att-style is prop
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 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare();
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.style = _5;}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
@@ -289,6 +293,7 @@ exports[`class and style attributes with t-component t-att-class is properly add
) {
let utils = this.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
@@ -298,7 +303,7 @@ exports[`class and style attributes with t-component t-att-class is properly add
const ref5 = \`child\`;
let _6 = {'a':true};
Object.assign(_6, {b:context['state'].b})
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;
c1.push(null);
let props4 = {};
@@ -314,8 +319,8 @@ exports[`class and style attributes with t-component t-att-class is properly add
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 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare();
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;context.refs[ref5] = w4;},remove() {},destroy(vn) {w4.destroy();delete context.refs[ref5];}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
@@ -345,6 +350,7 @@ exports[`class and style attributes with t-component t-att-class is properly add
) {
let utils = this.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
@@ -353,8 +359,8 @@ exports[`class and style attributes with t-component t-att-class is properly add
let def3;
const ref5 = \`child\`;
let _6 = {'a':true};
Object.assign(_6, this.utils.toObj(context['state'].b?'b':''))
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
Object.assign(_6, utils.toObj(context['state'].b?'b':''))
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 = {};
@@ -370,8 +376,8 @@ exports[`class and style attributes with t-component t-att-class is properly add
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 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare();
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;context.refs[ref5] = w4;},remove() {},destroy(vn) {w4.destroy();delete context.refs[ref5];}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
@@ -401,6 +407,7 @@ exports[`class and style attributes with t-component t-att-class is properly add
) {
let utils = this.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
@@ -408,7 +415,7 @@ exports[`class and style attributes with t-component t-att-class is properly add
//COMPONENT
let def3;
let _5 = {a:context['state'].a,b:context['state'].b};
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
@@ -424,8 +431,8 @@ exports[`class and style attributes with t-component t-att-class is properly add
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 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare();
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){}};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 {
@@ -443,6 +450,7 @@ exports[`composition sub components with some state rendered in a loop 1`] = `
) {
let utils = this.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
context = Object.create(context);
var h = this.utils.h;
@@ -465,7 +473,7 @@ exports[`composition sub components with some state rendered in a loop 1`] = `
//COMPONENT
let key8 = context['number'];
let def6;
let w7 = key8 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[key8]] : false;
let w7 = key8 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[key8]] : false;
let _5_index = c1.length;
c1.push(null);
let props7 = {};
@@ -481,8 +489,8 @@ exports[`composition sub components with some state rendered in a loop 1`] = `
let componentKey7 = \`ChildWidget\`;
let W7 = context.components && context.components[componentKey7] || QWeb.components[componentKey7];
if (!W7) {throw new Error('Cannot find the definition of component \\"' + componentKey7 + '\\"')}
w7 = new W7(owner, props7);
context.__owl__.cmap[key8] = w7.__owl__.id;
w7 = new W7(parent, props7);
parent.__owl__.cmap[key8] = w7.__owl__.id;
def6 = w7.__prepare();
def6 = def6.then(vnode=>{let pvnode=h(vnode.sel, {key: key8, hook: {insert(vn) {let nvn=w7.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w7.destroy();}}});c1[_5_index]=pvnode;w7.__owl__.pvnode = pvnode;});
} else {
@@ -500,13 +508,14 @@ exports[`composition t-component with dynamic value 1`] = `
) {
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 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;
c1.push(null);
let props4 = {};
@@ -522,8 +531,8 @@ exports[`composition t-component with dynamic value 1`] = `
let componentKey4 = (context['state'].widget);
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
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 {
@@ -540,13 +549,14 @@ exports[`composition t-component with dynamic value 2 1`] = `
) {
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 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;
c1.push(null);
let props4 = {};
@@ -562,8 +572,8 @@ exports[`composition t-component with dynamic value 2 1`] = `
let componentKey4 = \`Widget\${context['state'].widget}\`;
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
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 {
@@ -580,13 +590,14 @@ exports[`other directives with t-component t-on with handler bound to argument 1
) {
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 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;
c1.push(null);
let props4 = {};
@@ -602,8 +613,8 @@ exports[`other directives with t-component t-on with handler bound to argument 1
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 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare();
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', owner['onEv'].bind(owner, 3));}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
@@ -620,13 +631,14 @@ exports[`other directives with t-component t-on with handler bound to empty obje
) {
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 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;
c1.push(null);
let props4 = {};
@@ -642,8 +654,8 @@ exports[`other directives with t-component t-on with handler bound to empty obje
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 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare();
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', owner['onEv'].bind(owner, {}));}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
@@ -660,13 +672,14 @@ exports[`other directives with t-component t-on with handler bound to empty obje
) {
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 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;
c1.push(null);
let props4 = {};
@@ -682,8 +695,8 @@ exports[`other directives with t-component t-on with handler bound to empty obje
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 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare();
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', owner['onEv'].bind(owner, {}));}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
@@ -700,13 +713,14 @@ exports[`other directives with t-component t-on with handler bound to object 1`]
) {
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 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;
c1.push(null);
let props4 = {};
@@ -722,8 +736,8 @@ exports[`other directives with t-component t-on with handler bound to object 1`]
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 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare();
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', owner['onEv'].bind(owner, {val:3}));}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
@@ -740,13 +754,14 @@ exports[`other directives with t-component t-on with prevent and self modifiers
) {
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 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;
c1.push(null);
let props4 = {};
@@ -762,8 +777,8 @@ exports[`other directives with t-component t-on with prevent and self modifiers
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 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare();
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {e.preventDefault();if (e.target !== vn.elm) {return}owner['onEv'].call(owner, e);});}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
@@ -780,13 +795,14 @@ exports[`other directives with t-component t-on with self and prevent modifiers
) {
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 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;
c1.push(null);
let props4 = {};
@@ -802,8 +818,8 @@ exports[`other directives with t-component t-on with self and prevent modifiers
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 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare();
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (e.target !== vn.elm) {return}e.preventDefault();owner['onEv'].call(owner, e);});}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
@@ -820,13 +836,14 @@ exports[`other directives with t-component t-on with self modifier 1`] = `
) {
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 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;
c1.push(null);
let props4 = {};
@@ -842,8 +859,8 @@ exports[`other directives with t-component t-on with self modifier 1`] = `
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 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare();
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev-1', owner['onEv1'].bind(owner));vn.elm.addEventListener('ev-2', function (e) {if (e.target !== vn.elm) {return}owner['onEv2'].call(owner, e);});}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
@@ -860,13 +877,14 @@ exports[`other directives with t-component t-on with stop and/or prevent modifie
) {
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 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;
c1.push(null);
let props4 = {};
@@ -882,8 +900,8 @@ exports[`other directives with t-component t-on with stop and/or prevent modifie
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 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare();
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev-1', function (e) {e.stopPropagation();owner['onEv1'].call(owner, e);});vn.elm.addEventListener('ev-2', function (e) {e.preventDefault();owner['onEv2'].call(owner, e);});vn.elm.addEventListener('ev-3', function (e) {e.stopPropagation();e.preventDefault();owner['onEv3'].call(owner, e);});}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
@@ -900,6 +918,7 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
) {
let utils = this.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
@@ -907,7 +926,7 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
//COMPONENT
let key5 = 'somestring';
let def3;
let w4 = key5 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[key5]] : false;
let w4 = key5 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[key5]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {flag:context['state'].flag};
@@ -923,8 +942,8 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
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 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[key5] = w4.__owl__.id;
w4 = new W4(parent, props4);
parent.__owl__.cmap[key5] = w4.__owl__.id;
def3 = w4.__prepare();
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: key5, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
@@ -941,6 +960,7 @@ exports[`random stuff/miscellaneous t-on with handler bound to dynamic argument
) {
let utils = this.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
context = Object.create(context);
var h = this.utils.h;
@@ -964,7 +984,7 @@ exports[`random stuff/miscellaneous t-on with handler bound to dynamic argument
let key8 = context['item'];
let def6;
let arg9 = context['item'];
let w7 = key8 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[key8]] : false;
let w7 = key8 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[key8]] : false;
let _5_index = c1.length;
c1.push(null);
let props7 = {};
@@ -980,8 +1000,8 @@ exports[`random stuff/miscellaneous t-on with handler bound to dynamic argument
let componentKey7 = \`Child\`;
let W7 = context.components && context.components[componentKey7] || QWeb.components[componentKey7];
if (!W7) {throw new Error('Cannot find the definition of component \\"' + componentKey7 + '\\"')}
w7 = new W7(owner, props7);
context.__owl__.cmap[key8] = w7.__owl__.id;
w7 = new W7(parent, props7);
parent.__owl__.cmap[key8] = w7.__owl__.id;
def6 = w7.__prepare();
def6 = def6.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', owner['onEv'].bind(owner, arg9));}};let pvnode=h(vnode.sel, {key: key8, hook: {insert(vn) {let nvn=w7.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w7.destroy();}}});c1[_5_index]=pvnode;w7.__owl__.pvnode = pvnode;});
} else {
@@ -1146,13 +1166,14 @@ exports[`t-slot directive can define and call slots 1`] = `
) {
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 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;
c1.push(null);
let props4 = {};
@@ -1168,8 +1189,8 @@ exports[`t-slot directive can define and call slots 1`] = `
let componentKey4 = \`Dialog\`;
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id;
w4.__owl__.slotId = 1;
def3 = w4.__prepare();
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
@@ -1185,6 +1206,7 @@ exports[`t-slot directive can define and call slots 1`] = `
exports[`t-slot directive can define and call slots 2`] = `
"function anonymous(context,extra
) {
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
@@ -1193,15 +1215,342 @@ exports[`t-slot directive can define and call slots 2`] = `
c1.push(vn2);
const slot3 = this.slots[context.__owl__.slotId + '_' + 'header'];
if (slot3) {
slot3(context.__owl__.parent, Object.assign({}, extra, {parentNode: c2}));
slot3(context.__owl__.parent, Object.assign({}, extra, {parentNode: c2, vars: extra.vars, parent: owner}));
}
let c4 = [], p4 = {key:4};
var vn4 = h('div', p4, c4);
c1.push(vn4);
const slot5 = this.slots[context.__owl__.slotId + '_' + 'footer'];
if (slot5) {
slot5(context.__owl__.parent, Object.assign({}, extra, {parentNode: c4}));
slot5(context.__owl__.parent, Object.assign({}, extra, {parentNode: c4, vars: extra.vars, parent: owner}));
}
return vn1;
}"
`;
exports[`t-slot directive slots are rendered with proper context, part 2 1`] = `
"function anonymous(context,extra
) {
let owner = context;
var h = this.utils.h;
var _1 = context['props'].to;
let c2 = [], p2 = {key:2,attrs:{href: _1}};
var vn2 = h('a', p2, c2);
const slot3 = this.slots[context.__owl__.slotId + '_' + 'default'];
if (slot3) {
slot3(context.__owl__.parent, Object.assign({}, extra, {parentNode: c2, vars: extra.vars, parent: owner}));
}
return vn2;
}"
`;
exports[`t-slot directive slots are rendered with proper context, part 2 2`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
context = Object.create(context);
const scope = Object.create(null);
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2};
var vn2 = h('u', p2, c2);
c1.push(vn2);
var _3 = context['state'].users;
if (!_3) { throw new Error('QWeb error: Invalid loop expression')}
var _4 = _5 = _3;
if (!(_3 instanceof Array)) {
_4 = Object.keys(_3);
_5 = Object.values(_3);
}
var _length4 = _4.length;
for (let i = 0; i < _length4; i++) {
context.user_first = i === 0;
scope.user_first = context.user_first;
context.user_last = i === _length4 - 1;
scope.user_last = context.user_last;
context.user_index = i;
scope.user_index = context.user_index;
context.user = _4[i];
scope.user = context.user;
context.user_value = _5[i];
scope.user_value = context.user_value;
let c6 = [], p6 = {key:context['user'].id};
var vn6 = h('li', p6, c6);
c2.push(vn6);
//COMPONENT
let def8;
let w9 = String(-9 - i) in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[String(-9 - i)]] : false;
let _7_index = c6.length;
c6.push(null);
let props9 = {to:'/user/'+context['user'].id};
if (w9 && w9.__owl__.renderPromise && !w9.__owl__.vnode) {
if (utils.shallowEqual(props9, w9.__owl__.renderProps)) {
def8 = w9.__owl__.renderPromise;
} else {
w9.destroy();
w9 = false;
}
}
if (!w9) {
let componentKey9 = \`Link\`;
let W9 = context.components && context.components[componentKey9] || QWeb.components[componentKey9];
if (!W9) {throw new Error('Cannot find the definition of component \\"' + componentKey9 + '\\"')}
w9 = new W9(parent, props9);
parent.__owl__.cmap[String(-9 - i)] = w9.__owl__.id;
w9.__owl__.slotId = 1;
def8 = w9.__prepare(Object.assign({}, scope));
def8 = def8.then(vnode=>{let pvnode=h(vnode.sel, {key: String(-9 - i), hook: {insert(vn) {let nvn=w9.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w9.destroy();}}});c6[_7_index]=pvnode;w9.__owl__.pvnode = pvnode;});
} else {
def8 = def8 || w9.__updateProps(props9, extra.forceUpdate, extra.patchQueue, Object.assign({}, scope));
def8 = def8.then(()=>{if (w9.__owl__.isDestroyed) {return};let pvnode=w9.__owl__.pvnode;c6[_7_index]=pvnode;});
}
extra.promises.push(def8);
}
return vn1;
}"
`;
exports[`t-slot directive slots are rendered with proper context, part 3 1`] = `
"function anonymous(context,extra
) {
let owner = context;
var h = this.utils.h;
var _1 = context['props'].to;
let c2 = [], p2 = {key:2,attrs:{href: _1}};
var vn2 = h('a', p2, c2);
const slot3 = this.slots[context.__owl__.slotId + '_' + 'default'];
if (slot3) {
slot3(context.__owl__.parent, Object.assign({}, extra, {parentNode: c2, vars: extra.vars, parent: owner}));
}
return vn2;
}"
`;
exports[`t-slot directive slots are rendered with proper context, part 3 2`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
context = Object.create(context);
const scope = Object.create(null);
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2};
var vn2 = h('u', p2, c2);
c1.push(vn2);
var _3 = context['state'].users;
if (!_3) { throw new Error('QWeb error: Invalid loop expression')}
var _4 = _5 = _3;
if (!(_3 instanceof Array)) {
_4 = Object.keys(_3);
_5 = Object.values(_3);
}
var _length4 = _4.length;
for (let i = 0; i < _length4; i++) {
context.user_first = i === 0;
scope.user_first = context.user_first;
context.user_last = i === _length4 - 1;
scope.user_last = context.user_last;
context.user_index = i;
scope.user_index = context.user_index;
context.user = _4[i];
scope.user = context.user;
context.user_value = _5[i];
scope.user_value = context.user_value;
let c6 = [], p6 = {key:context['user'].id};
var vn6 = h('li', p6, c6);
c2.push(vn6);
var _7 = 'User '+context['user'].name;
//COMPONENT
let def9;
let w10 = String(-10 - i) in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[String(-10 - i)]] : false;
let _8_index = c6.length;
c6.push(null);
let props10 = {to:'/user/'+context['user'].id};
if (w10 && w10.__owl__.renderPromise && !w10.__owl__.vnode) {
if (utils.shallowEqual(props10, w10.__owl__.renderProps)) {
def9 = w10.__owl__.renderPromise;
} else {
w10.destroy();
w10 = false;
}
}
if (!w10) {
let componentKey10 = \`Link\`;
let W10 = context.components && context.components[componentKey10] || QWeb.components[componentKey10];
if (!W10) {throw new Error('Cannot find the definition of component \\"' + componentKey10 + '\\"')}
w10 = new W10(parent, props10);
parent.__owl__.cmap[String(-10 - i)] = w10.__owl__.id;
w10.__owl__.slotId = 1;
def9 = w10.__prepare(Object.assign({}, scope), {_7});
def9 = def9.then(vnode=>{let pvnode=h(vnode.sel, {key: String(-10 - i), hook: {insert(vn) {let nvn=w10.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w10.destroy();}}});c6[_8_index]=pvnode;w10.__owl__.pvnode = pvnode;});
} else {
def9 = def9 || w10.__updateProps(props10, extra.forceUpdate, extra.patchQueue, Object.assign({}, scope), {_7});
def9 = def9.then(()=>{if (w10.__owl__.isDestroyed) {return};let pvnode=w10.__owl__.pvnode;c6[_8_index]=pvnode;});
}
extra.promises.push(def9);
}
return vn1;
}"
`;
exports[`t-slot directive slots are rendered with proper context, part 4 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);
var _2 = 'User '+context['state'].user.name;
//COMPONENT
let def4;
let w5 = 5 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[5]] : false;
let _3_index = c1.length;
c1.push(null);
let props5 = {to:'/user/'+context['state'].user.id};
if (w5 && w5.__owl__.renderPromise && !w5.__owl__.vnode) {
if (utils.shallowEqual(props5, w5.__owl__.renderProps)) {
def4 = w5.__owl__.renderPromise;
} else {
w5.destroy();
w5 = false;
}
}
if (!w5) {
let componentKey5 = \`Link\`;
let W5 = context.components && context.components[componentKey5] || QWeb.components[componentKey5];
if (!W5) {throw new Error('Cannot find the definition of component \\"' + componentKey5 + '\\"')}
w5 = new W5(parent, props5);
parent.__owl__.cmap[5] = w5.__owl__.id;
w5.__owl__.slotId = 1;
def4 = w5.__prepare({}, {_2});
def4 = def4.then(vnode=>{let pvnode=h(vnode.sel, {key: 5, hook: {insert(vn) {let nvn=w5.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w5.destroy();}}});c1[_3_index]=pvnode;w5.__owl__.pvnode = pvnode;});
} else {
def4 = def4 || w5.__updateProps(props5, extra.forceUpdate, extra.patchQueue, {}, {_2});
def4 = def4.then(()=>{if (w5.__owl__.isDestroyed) {return};let pvnode=w5.__owl__.pvnode;c1[_3_index]=pvnode;});
}
extra.promises.push(def4);
return vn1;
}"
`;
exports[`top level sub widgets basic use 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
let result;
var h = this.utils.h;
//COMPONENT
let def2;
let w3 = 3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[3]] : false;
let vn4 = {};
result = vn4;
let props3 = {p:1};
if (w3 && w3.__owl__.renderPromise && !w3.__owl__.vnode) {
if (utils.shallowEqual(props3, w3.__owl__.renderProps)) {
def2 = w3.__owl__.renderPromise;
} else {
w3.destroy();
w3 = false;
}
}
if (!w3) {
let componentKey3 = \`Child\`;
let W3 = context.components && context.components[componentKey3] || QWeb.components[componentKey3];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3);
parent.__owl__.cmap[3] = w3.__owl__.id;
def2 = w3.__prepare();
def2 = def2.then(vnode=>{let pvnode=h(vnode.sel, {key: 3, hook: {insert(vn) {let nvn=w3.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}});utils.defineProxy(vn4, pvnode);w3.__owl__.pvnode = pvnode;});
} else {
def2 = def2 || w3.__updateProps(props3, extra.forceUpdate, extra.patchQueue);
def2 = def2.then(()=>{if (w3.__owl__.isDestroyed) {return};let pvnode=w3.__owl__.pvnode;utils.defineProxy(vn4, pvnode);});
}
extra.promises.push(def2);
return result;
}"
`;
exports[`top level sub widgets can select a sub widget 1`] = `
"function anonymous(context,extra
) {
let utils = this.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
let result;
var h = this.utils.h;
if (context['env'].flag) {
//COMPONENT
let def2;
let w3 = 3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[3]] : false;
let vn4 = {};
result = vn4;
let props3 = {};
if (w3 && w3.__owl__.renderPromise && !w3.__owl__.vnode) {
if (utils.shallowEqual(props3, w3.__owl__.renderProps)) {
def2 = w3.__owl__.renderPromise;
} else {
w3.destroy();
w3 = false;
}
}
if (!w3) {
let componentKey3 = \`Child\`;
let W3 = context.components && context.components[componentKey3] || QWeb.components[componentKey3];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3);
parent.__owl__.cmap[3] = w3.__owl__.id;
def2 = w3.__prepare();
def2 = def2.then(vnode=>{let pvnode=h(vnode.sel, {key: 3, hook: {insert(vn) {let nvn=w3.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}});utils.defineProxy(vn4, pvnode);w3.__owl__.pvnode = pvnode;});
} else {
def2 = def2 || w3.__updateProps(props3, extra.forceUpdate, extra.patchQueue);
def2 = def2.then(()=>{if (w3.__owl__.isDestroyed) {return};let pvnode=w3.__owl__.pvnode;utils.defineProxy(vn4, pvnode);});
}
extra.promises.push(def2);
}
if (!context['env'].flag) {
//COMPONENT
let def6;
let w7 = 7 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[7]] : false;
let vn8 = {};
result = vn8;
let props7 = {};
if (w7 && w7.__owl__.renderPromise && !w7.__owl__.vnode) {
if (utils.shallowEqual(props7, w7.__owl__.renderProps)) {
def6 = w7.__owl__.renderPromise;
} else {
w7.destroy();
w7 = false;
}
}
if (!w7) {
let componentKey7 = \`OtherChild\`;
let W7 = context.components && context.components[componentKey7] || QWeb.components[componentKey7];
if (!W7) {throw new Error('Cannot find the definition of component \\"' + componentKey7 + '\\"')}
w7 = new W7(parent, props7);
parent.__owl__.cmap[7] = w7.__owl__.id;
def6 = w7.__prepare();
def6 = def6.then(vnode=>{let pvnode=h(vnode.sel, {key: 7, hook: {insert(vn) {let nvn=w7.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w7.destroy();}}});utils.defineProxy(vn8, pvnode);w7.__owl__.pvnode = pvnode;});
} else {
def6 = def6 || w7.__updateProps(props7, extra.forceUpdate, extra.patchQueue);
def6 = def6.then(()=>{if (w7.__owl__.isDestroyed) {return};let pvnode=w7.__owl__.pvnode;utils.defineProxy(vn8, pvnode);});
}
extra.promises.push(def6);
}
return result;
}"
`;
@@ -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;
}"
`;
+586 -13
View File
@@ -879,18 +879,22 @@ describe("composition", () => {
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
});
test("throw a nice error if it cannot find component", async () => {
expect.assertions(1);
test("display a nice error if it cannot find component", async () => {
const consoleError = console.error;
console.error = jest.fn();
env.qweb.addTemplate("Parent", `<div><SomeMispelledWidget /></div>`);
class Parent extends Widget {
components = { SomeWidget: Widget };
}
const parent = new Parent(env);
try {
await parent.mount(fixture);
} catch (e) {
expect(e.message).toBe('Cannot find the definition of component "SomeMispelledWidget"');
}
await parent.mount(fixture);
expect(console.error).toBeCalledTimes(1);
expect((<any>console.error).mock.calls[0][0].message).toMatch(
'Cannot find the definition of component "SomeMispelledWidget"'
);
console.error = consoleError;
});
test("t-refs on widget are components", async () => {
@@ -2726,7 +2730,8 @@ describe("widget and observable state", () => {
});
test("subcomponents cannot change observable state received from parent", async () => {
expect.assertions(1);
const consoleError = console.error;
console.error = jest.fn();
env.qweb.addTemplate("Parent", `<div><Child obj="state.obj"/></div>`);
class Parent extends Widget {
state = { obj: { coffee: 1 } };
@@ -2739,11 +2744,13 @@ describe("widget and observable state", () => {
}
}
const parent = new Parent(env);
try {
await parent.mount(fixture);
} catch (e) {
expect(e.message).toBe('Observed state cannot be changed here! (key: "coffee", val: "2")');
}
await parent.mount(fixture);
expect(console.error).toBeCalledTimes(1);
expect((<any>console.error).mock.calls[0][0].message).toMatch(
'Observed state cannot be changed here! (key: "coffee", val: "2")'
);
console.error = consoleError;
});
});
@@ -2920,6 +2927,113 @@ describe("t-slot directive", () => {
);
});
test("slots are rendered with proper context, part 2", async () => {
env.qweb.addTemplates(`
<templates>
<a t-name="Link" t-att-href="props.to">
<t t-slot="default"/>
</a>
<div t-name="App">
<u><li t-foreach="state.users" t-as="user" t-key="user.id">
<Link to="'/user/' + user.id">User <t t-esc="user.name"/></Link>
</li></u>
</div>
</templates>
`);
class Link extends Widget {}
class App extends Widget {
state = { users: [{ id: 1, name: "Aaron" }, { id: 2, name: "David" }] };
components = { Link };
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
'<div><u><li><a href="/user/1">User Aaron</a></li><li><a href="/user/2">User David</a></li></u></div>'
);
expect(env.qweb.templates.Link.fn.toString()).toMatchSnapshot();
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
// test updateprops here
app.state.users[1].name = "Mathieu";
await nextTick();
expect(fixture.innerHTML).toBe(
'<div><u><li><a href="/user/1">User Aaron</a></li><li><a href="/user/2">User Mathieu</a></li></u></div>'
);
});
test("slots are rendered with proper context, part 3", async () => {
env.qweb.addTemplates(`
<templates>
<a t-name="Link" t-att-href="props.to">
<t t-slot="default"/>
</a>
<div t-name="App">
<u><li t-foreach="state.users" t-as="user" t-key="user.id" >
<t t-set="userdescr" t-value="'User ' + user.name"/>
<Link to="'/user/' + user.id"><t t-esc="userdescr"/></Link>
</li></u>
</div>
</templates>
`);
class Link extends Widget {}
class App extends Widget {
state = { users: [{ id: 1, name: "Aaron" }, { id: 2, name: "David" }] };
components = { Link };
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
'<div><u><li><a href="/user/1">User Aaron</a></li><li><a href="/user/2">User David</a></li></u></div>'
);
expect(env.qweb.templates.Link.fn.toString()).toMatchSnapshot();
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
// test updateprops here
app.state.users[1].name = "Mathieu";
await nextTick();
expect(fixture.innerHTML).toBe(
'<div><u><li><a href="/user/1">User Aaron</a></li><li><a href="/user/2">User Mathieu</a></li></u></div>'
);
});
test("slots are rendered with proper context, part 4", async () => {
env.qweb.addTemplates(`
<templates>
<a t-name="Link" t-att-href="props.to">
<t t-slot="default"/>
</a>
<div t-name="App">
<t t-set="userdescr" t-value="'User ' + state.user.name"/>
<Link to="'/user/' + state.user.id"><t t-esc="userdescr"/></Link>
</div>
</templates>
`);
class Link extends Widget {}
class App extends Widget {
state = { user: { id: 1, name: "Aaron" } };
components = { Link };
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe('<div><a href="/user/1">User Aaron</a></div>');
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
// test updateprops here
app.state.user.name = "David";
await nextTick();
expect(fixture.innerHTML).toBe('<div><a href="/user/1">User David</a></div>');
});
test("refs are properly bound in slots", async () => {
env.qweb.addTemplates(`
<templates>
@@ -3087,6 +3201,76 @@ describe("t-slot directive", () => {
expect(console.log).toHaveBeenCalledTimes(0);
console.log = consoleLog;
});
test("slot preserves properly parented relationship", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<Child>
<GrandChild/>
</Child>
</div>
<div t-name="Child"><t t-slot="default"/></div>
<div t-name="GrandChild">Grand Child</div>
</templates>
`);
class Child extends Widget {}
class GrandChild extends Widget {}
class Parent extends Widget {
components = { Child, GrandChild };
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><div>Grand Child</div></div></div>");
const parentChildren = children(parent);
expect(parentChildren.length).toBe(1);
expect(parentChildren[0]).toBeInstanceOf(Child);
const childrenChildren = children(parentChildren[0]);
expect(childrenChildren.length).toBe(1);
expect(childrenChildren[0]).toBeInstanceOf(GrandChild);
});
test("slot are properly rendered if inner props are changed", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="SomeComponent">
SC:<t t-esc="props.val"/>
</div>
<div t-name="GenericComponent">
<t t-slot="default" />
</div>
<div t-name="App">
<button t-on-click="inc">Inc[<t t-esc="state.val"/>]</button>
<GenericComponent>
<SomeComponent val="state.val"/>
</GenericComponent>
</div>
</templates>
`);
class SomeComponent extends Widget {}
class GenericComponent extends Widget {}
class App extends Widget {
components = { GenericComponent, SomeComponent };
state = { val: 4 };
inc() {
this.state.val++;
}
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><button>Inc[4]</button><div><div> SC:4</div></div></div>");
(<any>fixture.querySelector("button")).click();
await nextTick();
expect(fixture.innerHTML).toBe("<div><button>Inc[5]</button><div><div> SC:5</div></div></div>");
});
});
describe("t-model directive", () => {
@@ -3347,3 +3531,392 @@ describe("environment and plugins", () => {
expect(fixture.innerHTML).toBe("<div>Blue</div>");
});
});
describe("component error handling (catchError)", () => {
/**
* This test suite requires often to wait for 3 ticks. Here is why:
* - First tick is to let the app render and crash.
* - When we crash, we call the catchError handler in a setTimeout (because we
* need to wait for the previous rendering to be completely stopped). So, we
* need to wait for the second tick.
* - Then, when the handler changes the state, we need to wait for the interface
* to be rerendered.
* */
test("can catch an error in a component render function", async () => {
const consoleError = console.error;
console.error = jest.fn();
env.qweb.addTemplates(`
<templates>
<div t-name="ErrorBoundary">
<t t-if="state.error">Error handled</t>
<t t-else="1"><t t-slot="default" /></t>
</div>
<div t-name="ErrorComponent">hey<t t-esc="props.flag and state.this.will.crash"/>
</div>
<div t-name="App">
<ErrorBoundary><ErrorComponent flag="state.flag"/></ErrorBoundary>
</div>
</templates>`);
const handler = jest.fn();
env.qweb.on("error", null, handler);
class ErrorComponent extends Widget {}
class ErrorBoundary extends Widget {
state = { error: false };
catchError() {
this.state.error = true;
}
}
class App extends Widget {
state = { flag: false };
components = { ErrorBoundary, ErrorComponent };
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><div>hey</div></div></div>");
app.state.flag = true;
await nextTick();
await nextTick();
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
expect(console.error).toBeCalledTimes(1);
console.error = consoleError;
expect(handler).toBeCalledTimes(1);
});
test("no component catching error lead to full app destruction", async () => {
const handler = jest.fn();
env.qweb.on("error", null, handler);
const consoleError = console.error;
console.error = jest.fn();
env.qweb.addTemplates(`
<templates>
<div t-name="ErrorComponent">hey<t t-esc="props.flag and state.this.will.crash"/>
</div>
<div t-name="App">
<ErrorComponent flag="state.flag"/>
</div>
</templates>`);
class ErrorComponent extends Widget {}
class App extends Widget {
state = { flag: false };
components = { ErrorComponent };
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>hey</div></div>");
app.state.flag = true;
await nextTick();
await nextTick();
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(console.error).toBeCalledTimes(1);
console.error = consoleError;
expect(app.__owl__.isDestroyed).toBe(true);
expect(handler).toBeCalledTimes(1);
});
test("can catch an error in the initial call of a component render function", async () => {
const handler = jest.fn();
env.qweb.on("error", null, handler);
const consoleError = console.error;
console.error = jest.fn();
env.qweb.addTemplates(`
<templates>
<div t-name="ErrorBoundary">
<t t-if="state.error">Error handled</t>
<t t-else="1"><t t-slot="default" /></t>
</div>
<div t-name="ErrorComponent">hey<t t-esc="state.this.will.crash"/>
</div>
<div t-name="App">
<ErrorBoundary><ErrorComponent /></ErrorBoundary>
</div>
</templates>`);
class ErrorComponent extends Widget {}
class ErrorBoundary extends Widget {
state = { error: false };
catchError() {
this.state.error = true;
}
}
class App extends Widget {
components = { ErrorBoundary, ErrorComponent };
}
const app = new App(env);
await app.mount(fixture);
await nextTick();
await nextTick();
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
expect(console.error).toBeCalledTimes(1);
console.error = consoleError;
expect(handler).toBeCalledTimes(1);
});
test("can catch an error in the constructor call of a component render function", async () => {
const handler = jest.fn();
env.qweb.on("error", null, handler);
const consoleError = console.error;
console.error = jest.fn();
env.qweb.addTemplates(`
<templates>
<div t-name="ErrorBoundary">
<t t-if="state.error">Error handled</t>
<t t-else="1"><t t-slot="default" /></t>
</div>
<div t-name="ErrorComponent">Some text</div>
<div t-name="App">
<ErrorBoundary><ErrorComponent /></ErrorBoundary>
</div>
</templates>`);
class ErrorComponent extends Widget {
constructor(parent) {
super(parent);
throw new Error("NOOOOO");
}
}
class ErrorBoundary extends Widget {
state = { error: false };
catchError() {
this.state.error = true;
}
}
class App extends Widget {
components = { ErrorBoundary, ErrorComponent };
}
const app = new App(env);
await app.mount(fixture);
await nextTick();
await nextTick();
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
expect(console.error).toBeCalledTimes(1);
console.error = consoleError;
expect(handler).toBeCalledTimes(1);
});
test("can catch an error in the willStart call", async () => {
const consoleError = console.error;
console.error = jest.fn();
env.qweb.addTemplates(`
<templates>
<div t-name="ErrorBoundary">
<t t-if="state.error">Error handled</t>
<t t-else="1"><t t-slot="default" /></t>
</div>
<div t-name="ErrorComponent">Some text</div>
<div t-name="App">
<ErrorBoundary><ErrorComponent /></ErrorBoundary>
</div>
</templates>`);
class ErrorComponent extends Widget {
async willStart() {
// we wait a little bit to be in a different stack frame
await nextTick();
throw new Error("NOOOOO");
}
}
class ErrorBoundary extends Widget {
state = { error: false };
catchError() {
this.state.error = true;
}
}
class App extends Widget {
components = { ErrorBoundary, ErrorComponent };
}
const app = new App(env);
await app.mount(fixture);
await nextTick();
await nextTick();
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
expect(console.error).toBeCalledTimes(1);
console.error = consoleError;
});
test("can catch an error in the mounted call", async () => {
console.error = jest.fn();
env.qweb.addTemplates(`
<templates>
<div t-name="ErrorBoundary">
<t t-if="state.error">Error handled</t>
<t t-else="1"><t t-slot="default" /></t>
</div>
<div t-name="ErrorComponent">Some text</div>
<div t-name="App">
<ErrorBoundary><ErrorComponent /></ErrorBoundary>
</div>
</templates>`);
class ErrorComponent extends Widget {
mounted() {
throw new Error("NOOOOO");
}
}
class ErrorBoundary extends Widget {
state = { error: false };
catchError() {
this.state.error = true;
}
}
class App extends Widget {
components = { ErrorBoundary, ErrorComponent };
}
const app = new App(env);
await app.mount(fixture);
await nextTick();
await nextTick();
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
});
test("can catch an error in the willPatch call", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="ErrorBoundary">
<t t-if="state.error">Error handled</t>
<t t-else="1"><t t-slot="default" /></t>
</div>
<div t-name="ErrorComponent"><t t-esc="props.message"/></div>
<div t-name="App">
<ErrorBoundary><ErrorComponent message="state.message" /></ErrorBoundary>
</div>
</templates>`);
class ErrorComponent extends Widget {
willPatch() {
throw new Error("NOOOOO");
}
}
class ErrorBoundary extends Widget {
state = { error: false };
catchError() {
this.state.error = true;
}
}
class App extends Widget {
state = { message: "abc" };
components = { ErrorBoundary, ErrorComponent };
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><div>abc</div></div></div>");
app.state.message = "def";
await nextTick();
await nextTick();
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
});
});
describe("top level sub widgets", () => {
test("basic use", async () => {
env.qweb.addTemplates(`
<templates>
<t t-name="Parent">
<Child p="1"/>
</t>
<span t-name="Child">child<t t-esc="props.p"/></span>
</templates>`);
class Child extends Widget {}
class Parent extends Widget {
components = { Child };
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<span>child1</span>");
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
});
test("sub widget is interactive", async () => {
env.qweb.addTemplates(`
<templates>
<t t-name="Parent">
<Child p="1"/>
</t>
<span t-name="Child"><button t-on-click="inc">click</button>child<t t-esc="state.val"/></span>
</templates>`);
class Child extends Widget {
state = {val: 1};
inc() {
this.state.val++;
}
}
class Parent extends Widget {
components = { Child };
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<span><button>click</button>child1</span>");
const button = fixture.querySelector('button')!;
button.click();
await nextTick();
expect(fixture.innerHTML).toBe("<span><button>click</button>child2</span>");
});
test("can select a sub widget ", async () => {
env.qweb.addTemplates(`
<templates>
<t t-name="Parent">
<t t-if="env.flag"><Child /></t>
<t t-if="!env.flag"><OtherChild /></t>
</t>
<span t-name="Child">CHILD 1</span>
<div t-name="OtherChild">CHILD 2</div>
</templates>`);
class Child extends Widget {}
class OtherChild extends Widget {}
class Parent extends Widget {
components = { Child, OtherChild };
}
(<any>env).flag = true;
let parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<span>CHILD 1</span>");
parent.destroy();
(<any>env).flag = false;
parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div>CHILD 2</div>");
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
});
test("can select a sub widget, part 2", async () => {
env.qweb.addTemplates(`
<templates>
<t t-name="Parent">
<t t-if="state.flag"><Child /></t>
<t t-if="!state.flag"><OtherChild /></t>
</t>
<span t-name="Child">CHILD 1</span>
<div t-name="OtherChild">CHILD 2</div>
</templates>`);
class Child extends Widget {}
class OtherChild extends Widget {}
class Parent extends Widget {
state = {flag: true}
components = { Child, OtherChild };
}
let parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<span>CHILD 1</span>");
parent.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe("<div>CHILD 2</div>");
});
});
+14 -1
View File
@@ -68,9 +68,22 @@ export function renderToDOM(
return result.elm as HTMLElement;
}
/**
* Render a template to an html string. The big difference with the
* renderToString method in QWeb is that we use the renderToDom method, which
* snapshots the resulting template function. Doing so gives us a large body
* 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);
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
+91 -14
View File
@@ -1,6 +1,6 @@
import { Component, Env } from "../src/component";
import { makeTestFixture, makeTestEnv } from "./helpers";
import { QWeb } from "../src";
import { QWeb, UTILS } from "../src/qweb_core";
//------------------------------------------------------------------------------
// Setup and helpers
@@ -45,19 +45,6 @@ describe("props validation", () => {
}).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' (component 'TestWidget')");
}
});
test("props: list of strings", async () => {
class TestWidget extends Widget {
static props = ["message"];
@@ -234,6 +221,96 @@ describe("props validation", () => {
new TestWidget(env, { p: { id: 1, url: [12, true] } });
}).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", () => {
+221 -308
View File
@@ -1,5 +1,5 @@
import { Component, Env } from "../src/component";
import { connect, Store } from "../src/store";
import { Store, ConnectedComponent } from "../src/store";
import { makeTestFixture, makeTestEnv, nextMicroTick, nextTick } from "./helpers";
import { Observer } from "../src";
@@ -561,18 +561,21 @@ describe("connecting a component to store", () => {
});
test("connecting a component works", async () => {
env.qweb.addTemplate(
"App",
`
<div>
env.qweb.addTemplates(`
<templates>
<div t-name="App">
<t t-foreach="props.todos" t-as="todo" >
<Todo msg="todo.msg" t-key="todo"/>
</t>
</div>`
);
env.qweb.addTemplate("Todo", `<span><t t-esc="props.msg"/></span>`);
class App extends Component<any, any, any> {
</div>
<span t-name="Todo"><t t-esc="props.msg"/></span>
</templates>
`);
class App extends ConnectedComponent<any, any, any> {
components = { Todo };
static mapStoreToProps(s) {
return { todos: s.todos };
}
}
class Todo extends Component<any, any, any> {}
const state = { todos: [] };
@@ -581,16 +584,9 @@ describe("connecting a component to store", () => {
state.todos.push({ msg });
}
};
function mapStoreToProps(s) {
return { todos: s.todos };
}
const TodoApp = connect(
App,
mapStoreToProps
);
const store = new Store({ state, mutations });
(<any>env).store = store;
const app = new TodoApp(env);
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toMatchSnapshot();
@@ -601,38 +597,35 @@ describe("connecting a component to store", () => {
});
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 mutations = {
edit({ state }, title) {
state.todos[0].title = title;
}
};
function mapStoreToProps(s) {
return { todos: s.todos };
}
const store = new Store({ state, mutations });
env.qweb.addTemplate(
"App",
`
<div>
<span t-foreach="props.todos" t-as="todo" t-key="todo">
<t t-esc="todo.title"/>
</span>
</div>`
);
class App extends Component<any, any, any> {}
class App extends ConnectedComponent<any, any, any> {
static mapStoreToProps(s) {
return { todos: s.todos };
}
}
class DeepTodoApp extends App {
deep = true;
}
class ShallowTodoApp extends App {
deep = false;
}
const DeepTodoApp = connect(
App,
mapStoreToProps,
{ deep: true }
);
const ShallowTodoApp = connect(
App,
mapStoreToProps,
{ deep: false }
);
(<any>env).store = store;
const deepTodoApp = new DeepTodoApp(env);
const shallowTodoApp = new ShallowTodoApp(env);
@@ -662,9 +655,6 @@ describe("connecting a component to store", () => {
<span t-name="Todo"><t t-esc="props.msg"/></span>
</templates>
`);
class App extends Component<any, any, any> {
components = { Todo };
}
class Todo extends Component<any, any, any> {}
(<any>env).store = new Store({});
@@ -676,17 +666,16 @@ describe("connecting a component to store", () => {
}
}
});
function mapStoreToProps(s) {
return { todos: s.todos };
}
const TodoApp = connect(
App,
mapStoreToProps,
{
getStore: () => store
class App extends ConnectedComponent<any, any, any> {
components = { Todo };
static mapStoreToProps(s) {
return { todos: s.todos };
}
);
const app = new TodoApp(env);
getStore() {
return store;
}
}
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toMatchSnapshot();
@@ -698,8 +687,18 @@ describe("connecting a component to store", () => {
test("connected child components with custom hooks", async () => {
let steps: any = [];
env.qweb.addTemplate("Child", `<div/>`);
class Child extends Component<any, any, any> {
env.qweb.addTemplates(`
<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() {
steps.push("child:mounted");
}
@@ -708,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-component="ConnectedChild"/>
</div>`
);
class Parent extends Component<any, any, any> {
components = { ConnectedChild };
components = { Child };
constructor(env: Env) {
super(env);
@@ -741,7 +728,7 @@ describe("connecting a component to store", () => {
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" }] };
let nextId = 2;
const mutations = {
@@ -751,38 +738,32 @@ describe("connecting a component to store", () => {
};
const store = new Store({ state, mutations });
env.qweb.addTemplate("TodoItem", `<span><t t-esc="props.text"/></span>`);
class TodoItem extends Component<any, any, any> {}
const ConnectedTodo = connect(
TodoItem,
(state, props) => {
env.qweb.addTemplates(`
<templates>
<span t-name="TodoItem"><t t-esc="props.text"/></span>
<div t-name="TodoList">
<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);
return todo;
}
);
env.qweb.addTemplate(
"TodoList",
`<div>
<t t-foreach="props.todos" t-as="todo">
<ConnectedTodo id="todo.id" t-key="todo.id"/>
</t>
</div>`
);
class TodoList extends Component<any, any, any> {
components = { ConnectedTodo };
}
function mapStoreToProps(state) {
return { todos: state.todos };
class TodoList extends ConnectedComponent<any, any, any> {
components = { TodoItem };
static mapStoreToProps(state) {
return { todos: state.todos };
}
}
const ConnectedTodoList = connect(
TodoList,
mapStoreToProps
);
(<any>env).store = store;
const app = new ConnectedTodoList(env);
const app = new TodoList(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
@@ -792,7 +773,7 @@ describe("connecting a component to store", () => {
expect(fixture.innerHTML).toBe("<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 = {
importantID: 1,
todos: [{ id: 1, text: "jupiler" }, { id: 2, text: "bertinchamps" }]
@@ -807,47 +788,39 @@ describe("connecting a component to store", () => {
};
const store = new Store({ state, getters });
env.qweb.addTemplate(
"TodoItem",
`<div>
<span><t t-esc="props.activeTodoText"/></span>
<span><t t-esc="props.importantTodoText"/></span>
</div>`
);
class TodoItem extends Component<any, any, any> {}
const ConnectedTodo = connect(
TodoItem,
(state, props, getters) => {
env.qweb.addTemplates(`
<templates>
<div t-name="TodoItem">
<span><t t-esc="props.activeTodoText"/></span>
<span><t t-esc="props.importantTodoText"/></span>
</div>
<div t-name="TodoList">
<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, getters) {
const todo = state.todos.find(t => t.id === props.id);
return {
activeTodoText: getters.text(todo.id),
importantTodoText: getters.importantTodoText()
};
}
);
env.qweb.addTemplate(
"TodoList",
`<div>
<t t-foreach="props.todos" t-as="todo">
<ConnectedTodo id="todo.id" t-key="todo.id"/>
</t>
</div>`
);
class TodoList extends Component<any, any, any> {
components = { ConnectedTodo };
}
function mapStoreToProps(state) {
return { todos: state.todos };
class TodoList extends ConnectedComponent<any, any, any> {
components = { TodoItem };
static mapStoreToProps(state) {
return { todos: state.todos };
}
}
const ConnectedTodoList = connect(
TodoList,
mapStoreToProps
);
(<any>env).store = store;
const app = new ConnectedTodoList(env);
const app = new TodoList(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
@@ -856,23 +829,23 @@ describe("connecting a component to store", () => {
});
test("connected component is updated when props are updated", async () => {
env.qweb.addTemplate("Beer", `<span><t t-esc="props.name"/></span>`);
class Beer extends Component<any, any, any> {}
const ConnectedBeer = connect(
Beer,
(state, props) => {
env.qweb.addTemplates(`
<templates>
<span t-name="Beer"><t t-esc="props.name"/></span>
<div t-name="App">
<Beer id="state.beerId"/>
</div>
</templates>
`);
class Beer extends ConnectedComponent<any, any, any> {
static mapStoreToProps(state, props) {
return state.beers[props.id];
}
);
}
env.qweb.addTemplate(
"App",
`<div>
<ConnectedBeer id="state.beerId"/>
</div>`
);
class App extends Component<any, any, any> {
components = { ConnectedBeer };
components = { Beer };
state = { beerId: 1 };
}
@@ -890,14 +863,19 @@ describe("connecting a component to store", () => {
});
test("connected component is updated when store is changed", async () => {
env.qweb.addTemplate(
"App",
`
<div>
env.qweb.addTemplates(`
<templates>
<div t-name="App">
<span t-foreach="props.beers" t-as="beer" t-key="beer.name"><t t-esc="beer.name"/></span>
</div>`
);
class App extends Component<any, any, any> {}
</div>
</templates>
`);
class App extends ConnectedComponent<any, any, any> {
static mapStoreToProps(state) {
return { beers: state.beers, otherKey: 1 };
}
}
const mutations = {
addBeer({ state }, name) {
@@ -909,14 +887,7 @@ describe("connecting a component to store", () => {
const store = new Store({ state, mutations });
(<any>env).store = store;
function mapStoreToProps(state) {
return { beers: state.beers, otherKey: 1 };
}
const ConnectedApp = connect(
App,
mapStoreToProps
);
const app = new ConnectedApp(env);
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
@@ -927,34 +898,31 @@ describe("connecting a component to store", () => {
});
test("connected component with undefined, null and string props", async () => {
env.qweb.addTemplate(
"Beer",
`<div>
<span>taster:<t t-esc="props.taster"/></span>
<span t-if="props.selected">selected:<t t-esc="props.selected.name"/></span>
<span t-if="props.consumed">consumed:<t t-esc="props.consumed.name"/></span>
</div>`
);
class Beer extends Component<any, any, any> {}
const ConnectedBeer = connect(
Beer,
(state, props) => {
env.qweb.addTemplates(`
<templates>
<div t-name="Beer">
<span>taster:<t t-esc="props.taster"/></span>
<span t-if="props.selected">selected:<t t-esc="props.selected.name"/></span>
<span t-if="props.consumed">consumed:<t t-esc="props.consumed.name"/></span>
</div>
<div t-name="App">
<Beer id="state.beerId"/>
</div>
</templates>
`);
class Beer extends ConnectedComponent<any, any, any> {
static mapStoreToProps(state, props) {
return {
selected: state.beers[props.id],
consumed: state.beers[state.consumedID] || null,
taster: state.taster
};
}
);
}
env.qweb.addTemplate(
"App",
`<div>
<ConnectedBeer id="state.beerId"/>
</div>`
);
class App extends Component<any, any, any> {
components = { ConnectedBeer };
components = { Beer };
state = { beerId: 0 };
}
@@ -997,34 +965,31 @@ describe("connecting a component to store", () => {
});
test("connected component deeply reactive with undefined, null and string props", async () => {
env.qweb.addTemplate(
"Beer",
`<div>
<span>taster:<t t-esc="props.taster"/></span>
<span t-if="props.selected">selected:<t t-esc="props.selected.name"/></span>
<span t-if="props.consumed">consumed:<t t-esc="props.consumed.name"/></span>
</div>`
);
class Beer extends Component<any, any, any> {}
const ConnectedBeer = connect(
Beer,
(state, props) => {
env.qweb.addTemplates(`
<templates>
<div t-name="Beer">
<span>taster:<t t-esc="props.taster"/></span>
<span t-if="props.selected">selected:<t t-esc="props.selected.name"/></span>
<span t-if="props.consumed">consumed:<t t-esc="props.consumed.name"/></span>
</div>
<div t-name="App">
<Beer id="state.beerId"/>
</div>
</templates>
`);
class Beer extends ConnectedComponent<any, any, any> {
static mapStoreToProps(storeState, props) {
return {
selected: state.beers[props.id],
consumed: state.beers[state.consumedID] || null,
taster: state.taster
selected: storeState.beers[props.id],
consumed: storeState.beers[storeState.consumedID] || null,
taster: storeState.taster
};
}
);
}
env.qweb.addTemplate(
"App",
`<div>
<ConnectedBeer id="state.beerId"/>
</div>`
);
class App extends Component<any, any, any> {
components = { ConnectedBeer };
components = { Beer };
state = { beerId: 0 };
}
@@ -1093,35 +1058,29 @@ describe("connecting a component to store", () => {
test("correct update order when parent/children are connected", async () => {
const steps: string[] = [];
env.qweb.addTemplate(
"Parent",
`
<div>
<Child key="props.current"/>
</div>
`
);
class Parent extends Component<any, any, any> {
components = { Child: ConnectedChild };
}
const ConnectedParent = connect(
Parent,
function(s) {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<Child key="props.current"/>
</div>
<span t-name="Child"><t t-esc="props.msg"/></span>
</templates>
`);
class Parent extends ConnectedComponent<any, any, any> {
components = { Child };
static mapStoreToProps(s) {
steps.push("parent");
return { current: s.current, isvisible: s.isvisible };
}
);
}
env.qweb.addTemplate("Child", `<span><t t-esc="props.msg"/></span>`);
class Child extends Component<any, any, any> {}
const ConnectedChild = connect(
Child,
function(s, props) {
class Child extends ConnectedComponent<any, any, any> {
static mapStoreToProps(s, props) {
steps.push("child");
return { msg: s.msg[props.key] };
}
);
}
const state = { current: "a", msg: { a: "a", b: "b" } };
const mutations = {
@@ -1132,7 +1091,7 @@ describe("connecting a component to store", () => {
const store = new Store({ state, mutations });
(<any>env).store = store;
const app = new ConnectedParent(env);
const app = new Parent(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>a</span></div>");
@@ -1163,7 +1122,7 @@ describe("connecting a component to store", () => {
<templates>
<div t-name="TodoApp" class="todoapp">
<t t-foreach="Object.values(props.todos)" t-as="todo">
<ConnectedTodoItem t-key="todo.id" id="todo.id"/>
<TodoItem t-key="todo.id" id="todo.id"/>
</t>
</div>
@@ -1174,33 +1133,26 @@ describe("connecting a component to store", () => {
</templates>
`);
function mapStoreToPropsTodoApp(state) {
return {
todos: state.todos
};
class TodoApp extends ConnectedComponent<any, any, any> {
components = { TodoItem };
static mapStoreToProps(state) {
return {
todos: state.todos
};
}
}
class TodoApp extends Component<any, any, any> {
components = { ConnectedTodoItem };
}
const ConnectedTodoApp = connect(
TodoApp,
mapStoreToPropsTodoApp
);
let renderCount = 0;
let fCount = 0;
function mapStoreToPropsTodoItem(state, ownProps) {
fCount++;
return {
todo: state.todos[ownProps.id]
};
}
class TodoItem extends Component<any, any, any> {
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");
@@ -1211,13 +1163,8 @@ describe("connecting a component to store", () => {
}
}
const ConnectedTodoItem = connect(
TodoItem,
mapStoreToPropsTodoItem
);
(<any>env).store = store;
const app = new ConnectedTodoApp(env);
const app = new TodoApp(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
@@ -1254,7 +1201,7 @@ describe("connecting a component to store", () => {
<templates>
<div t-name="TodoApp" class="todoapp">
<t t-foreach="Object.values(props.todos)" t-as="todo">
<ConnectedTodoItem t-key="todo.id" id="todo.id"/>
<TodoItem t-key="todo.id" id="todo.id"/>
</t>
</div>
@@ -1265,34 +1212,27 @@ describe("connecting a component to store", () => {
</templates>
`);
function mapStoreToPropsTodoApp(state) {
return {
todos: state.todos
};
class TodoApp extends ConnectedComponent<any, any, any> {
components = { TodoItem };
static mapStoreToProps(state) {
return {
todos: state.todos
};
}
}
class TodoApp extends Component<any, any, any> {
components = { ConnectedTodoItem };
}
const ConnectedTodoApp = connect(
TodoApp,
mapStoreToPropsTodoApp
);
let renderCount = 0;
let fCount = 0;
function mapStoreToPropsTodoItem(state, ownProps) {
fCount++;
return {
todo: state.todos[ownProps.id]
};
}
class TodoItem extends Component<any, any, any> {
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");
}
@@ -1302,13 +1242,8 @@ describe("connecting a component to store", () => {
}
}
const ConnectedTodoItem = connect(
TodoItem,
mapStoreToPropsTodoItem
);
(<any>env).store = store;
const app = new ConnectedTodoApp(env);
const app = new TodoApp(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
@@ -1326,8 +1261,17 @@ describe("connecting a component to store", () => {
test("connected component willpatch/patch hooks are called on store updates", async () => {
const steps: string[] = [];
env.qweb.addTemplate("App", `<div><t t-esc="props.msg"/></div>`);
class App extends Component<any, any, any> {
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() {
steps.push("willpatch");
}
@@ -1335,12 +1279,6 @@ describe("connecting a component to store", () => {
steps.push("patched");
}
}
const ConnectedApp = connect(
App,
function(s) {
return { msg: s.msg };
}
);
const state = { msg: "a" };
const mutations = {
@@ -1351,7 +1289,7 @@ describe("connecting a component to store", () => {
const store = new Store({ state, mutations });
(<any>env).store = store;
const app = new ConnectedApp(env);
const app = new App(env);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>a</div>");
@@ -1362,29 +1300,4 @@ describe("connecting a component to store", () => {
expect(steps).toEqual(["willpatch", "patched"]);
});
test("connected component has its own name", () => {
function mapStoreToProps() {}
class Named extends Component<any, any, any> {}
const namedConnected = connect(
Named,
mapStoreToProps
);
expect(namedConnected.name).toMatch("ConnectedNamed");
class ParentNamed extends Component<any, any, any> {}
class ChildNamed extends ParentNamed {}
const childConnected = connect(
ChildNamed,
mapStoreToProps
);
expect(childConnected.name).toMatch("ConnectedChildNamed");
const Anonymous = class extends Component<any, any, any> {};
const anonymousConnected = connect(
Anonymous,
mapStoreToProps
);
expect(anonymousConnected.name).toMatch(/^Connectedclass_\d+/);
});
});
+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>
+1 -3
View File
@@ -1,9 +1,7 @@
html,
body {
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial,
sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-family: Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}
body {
+1
View File
@@ -36,6 +36,7 @@
<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.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>
</ul>
<ul>
+1 -3
View File
@@ -1,9 +1,7 @@
html,
body {
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial,
sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-family: Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}
.title,
+10 -8
View File
@@ -2,9 +2,7 @@
html,
body {
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial,
sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-family: Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}
body {
@@ -181,10 +179,14 @@ body {
.right-pane .error {
height: 100%;
padding-top: 40%;
font-size: 30px;
width: 90%;
padding-top: 30%;
font-size: 18px;
color: darkred;
text-align: center;
padding-left: 30px;
padding-right: 30px;
margin-left: 5%;
}
.right-pane .error pre {
overflow: auto;
width: 100%;
}
+32 -27
View File
@@ -1,16 +1,17 @@
const COMPONENTS = `// In this example, we show how components can be defined and created.
class Counter extends owl.Component {
state = { value: 0 };
class Greeter extends owl.Component {
state = { word: 'Hello' };
increment() {
this.state.value++;
toggle() {
this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi'
}
}
// Main root component
class App extends owl.Component {
components = { Counter };
components = { Greeter };
state = { name: 'World'};
}
// Application setup
@@ -21,20 +22,25 @@ app.mount(document.body);
`;
const COMPONENTS_XML = `<templates>
<button t-name="Counter" t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>
<div t-name="Greeter" class="greeter" t-on-click="toggle">
<t t-esc="state.word"/>, <t t-esc="props.name"/>
</div>
<div t-name="App">
<Counter />
<Counter />
<Greeter name="state.name"/>
</div>
</templates>`;
</templates>
`;
const COMPONENTS_CSS = `button {
const COMPONENTS_CSS = `.greeter {
font-size: 20px;
width: 220px;
width: 300px;
height: 100px;
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
@@ -399,16 +405,15 @@ class TodoItem extends owl.Component {
//------------------------------------------------------------------------------
// TodoApp
//------------------------------------------------------------------------------
function mapStoreToProps(state) {
return {
todos: state.todos
};
}
class TodoApp extends owl.Component {
class TodoApp extends owl.ConnectedComponent {
components = { TodoItem };
state = { filter: "all" };
static mapStoreToProps(state) {
return {
todos: state.todos
};
}
get visibleTodos() {
let todos = this.props.todos;
if (this.state.filter === "active") {
@@ -456,8 +461,6 @@ class TodoApp extends owl.Component {
}
}
const ConnectedTodoApp = owl.connect(TodoApp, mapStoreToProps);
//------------------------------------------------------------------------------
// App Initialization
//------------------------------------------------------------------------------
@@ -468,7 +471,7 @@ const env = {
store,
dispatch: store.dispatch.bind(store),
};
const app = new ConnectedTodoApp(env);
const app = new TodoApp(env);
app.mount(document.body);
`;
@@ -994,17 +997,19 @@ const RESPONSIVE_XML = `<templates>
</div>
<div t-name="App" class="app" t-att-class="{mobile: env.isMobile, desktop: !env.isMobile}">
<t t-set="maincontent">
<FormView />
<Chatter />
</t>
<Navbar/>
<ControlPanel/>
<div class="content-wrapper" t-if="!env.isMobile">
<div class="content">
<FormView />
<Chatter />
<t t-raw="maincontent"/>
</div>
</div>
<t t-else="1">
<FormView />
<Chatter />
<t t-raw="maincontent"/>
</t>
</div>
</templates>
+2 -1
View File
@@ -48,7 +48,8 @@
<div class="note">Note: these examples require a recent browser to work without a transpilation step. </div>
</div>
<div t-if="state.error" class="error">
<t t-esc="state.error"/>
<h3>Error</h3>
<pre t-esc="state.error"/>
</div>
<div class="content" t-ref="content"/>
</div>