Compare commits

..

1 Commits

Author SHA1 Message Date
Simon Genin (ges) 9e2202c02a [IMP] devtools: add __owl__ access.
To be able to make devtools, we need access some inner state of a
component. We create a __owl_devtools__ variable on components.

Just like in Vue js, we attach it to the HTMLElement of a component.
It's how we give it to the "outside world", through the DOM.

For its update, we use the onMounted and onPatched hook.
2020-11-10 17:22:37 +01:00
69 changed files with 626 additions and 2683 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [12.x, 14.x, 16.x] node-version: [10.x, 12.x, 14.x]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
+1 -4
View File
@@ -28,7 +28,4 @@ node_modules
release-notes.md release-notes.md
.rpt2_cache .rpt2_cache
# useful in some cases
/temp
+1 -5
View File
@@ -1,9 +1,5 @@
<h1 align="center">🦉 <a href="https://odoo.github.io/owl/">OWL Framework</a> 🦉</h1> <h1 align="center">🦉 <a href="https://odoo.github.io/owl/">OWL Framework</a> 🦉</h1>
[![License: LGPL v3](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0)
[![npm version](https://badge.fury.io/js/@odoo%2Fowl.svg)](https://badge.fury.io/js/@odoo%2Fowl)
[![Downloads](https://img.shields.io/npm/dm/@odoo%2Fowl.svg)](https://www.npmjs.com/package/@odoo/owl)
_Class based components with hooks, reactive state and concurrent mode_ _Class based components with hooks, reactive state and concurrent mode_
## Project Overview ## Project Overview
@@ -124,7 +120,7 @@ npm install @odoo/owl
If you want to use a simple `<script>` tag, the last release can be downloaded here: If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-1.4.5](https://github.com/odoo/owl/releases/tag/v1.4.5) - [owl-1.0.13](https://github.com/odoo/owl/releases/tag/v1.0.13)
## License ## License
+5 -7
View File
@@ -70,9 +70,7 @@ just put the following code:
Note that we put everything inside an immediately executed function to avoid leaking Note that we put everything inside an immediately executed function to avoid leaking
anything to the global scope. anything to the global scope.
Finally, `owl.js` should be the last version downloaded from the Owl repository (you can use `owl.min.js` if you prefer). Be aware that you should download the `owl.iife.js` or `owl.iife.min.js`, because these files Finally, `owl.js` should be the last version downloaded from the Owl repository (you can use `owl.min.js` if you prefer).
are built to run directly on the browser (other files such as `owl.cjs.js` are
built to be bundled by other tools).
Now, the project should be ready. Loading the `index.html` file into a browser Now, the project should be ready. Loading the `index.html` file into a browser
should show an empty page, with the title `Owl Todo App`, and it should log a should show an empty page, with the title `Owl Todo App`, and it should log a
@@ -547,7 +545,7 @@ application), since it involves extracting all task related code out of the
components. Here is the new content of the `app.js` file: components. Here is the new content of the `app.js` file:
```js ```js
const { Component, Store, mount } = owl; const { Component, Store } = owl;
const { xml } = owl.tags; const { xml } = owl.tags;
const { whenReady } = owl.utils; const { whenReady } = owl.utils;
const { useRef, useDispatch, useStore } = owl.hooks; const { useRef, useDispatch, useStore } = owl.hooks;
@@ -665,7 +663,7 @@ function makeStore() {
function setup() { function setup() {
owl.config.mode = "dev"; owl.config.mode = "dev";
const env = { store: makeStore() }; const env = {store = makeStore()};
mount(App, { target: document.body, env }); mount(App, { target: document.body, env });
} }
``` ```
@@ -810,7 +808,7 @@ For reference, here is the final code:
```js ```js
(function () { (function () {
const { Component, Store, mount } = owl; const { Component, Store } = owl;
const { xml } = owl.tags; const { xml } = owl.tags;
const { whenReady } = owl.utils; const { whenReady } = owl.utils;
const { useRef, useDispatch, useState, useStore } = owl.hooks; const { useRef, useDispatch, useState, useStore } = owl.hooks;
@@ -941,7 +939,7 @@ For reference, here is the final code:
function setup() { function setup() {
owl.config.mode = "dev"; owl.config.mode = "dev";
const env = { store: makeStore() }; const env = {store = makeStore()};
mount(App, { target: document.body, env }); mount(App, { target: document.body, env });
} }
+2 -2
View File
@@ -4,7 +4,7 @@ OWL, React and Vue have the same main feature: they allow developers to build
declarative user interfaces. To do that, all these frameworks uses a virtual dom. However, there are still obviously many differences. declarative user interfaces. To do that, all these frameworks uses a virtual dom. However, there are still obviously many differences.
In this page, we try to highlight some of these differences. Obviously, a lot of In this page, we try to highlight some of these differences. Obviously, a lot of
effort was put to be fair. However, if you disagree with some of the points effort was done to be fair. However, if you disagree with some of the points
discussed, feel free to open an issue/submit a PR to correct this text. discussed, feel free to open an issue/submit a PR to correct this text.
## Content ## Content
@@ -47,7 +47,7 @@ components are fast enough for all our usecases, and making it as simple as
possible for developers is more valuable (for us). possible for developers is more valuable (for us).
Also, functions or class based components are more than just syntax. Functions Also, functions or class based components are more than just syntax. Functions
come with a mindset of composition and class are about inheritance. Clearly, comes with a mindset of composition and class are about inheritance. Clearly,
both of these are important mechanisms for reusing code. Also, one does not both of these are important mechanisms for reusing code. Also, one does not
exclude the other. exclude the other.
-1
View File
@@ -17,7 +17,6 @@ You will find here a complete reference of every feature, class or object
provided by Owl. provided by Owl.
- [Animations](reference/animations.md) - [Animations](reference/animations.md)
- [Browser](reference/browser.md)
- [Component](reference/component.md) - [Component](reference/component.md)
- [Content](reference/content.md) - [Content](reference/content.md)
- [Concurrency Model](reference/concurrency_model.md) - [Concurrency Model](reference/concurrency_model.md)
+9 -41
View File
@@ -52,20 +52,21 @@ sequence of events will happen:
At node insertion: At node insertion:
- the css classes `name-enter` and `name-enter-active` will be added directly - the css classes `name-enter` and `name-enter-active` will be added directly
when the node is inserted into the DOM. when the node is inserted into the DOM,
- on the next animation frame: the css class `name-enter` will be removed and the - on the next animation frame: the css class `name-enter` will be removed and the
class `name-enter-to` will be added (so they can be used to trigger css class `name-enter-to` will be added (so they can be used to trigger css
transition effects). transition effects),
- at the end of the transition, `name-enter-to` and `name-enter-active` will be removed. - the css class `name-enter-active` will be removed whenever a css transition
ends.
At node destruction: At node destruction:
- the css classes `name-leave` and `name-leave-active` will be added before the - the css classes `name-leave` and `name-leave-active` will be added before the
node is removed to the DOM. node is removed to the DOM,
- on the next animation frame: the css class `name-leave` will be removed and the - the css class `name-leave` will be removed on the next animation frame (so it
class `name-leave-to` will be added (so they can be used to trigger css can be used to trigger css transition effects),
transition effects). - the css class `name-leave-active` will be removed whenever a css transition
- at the end of the transition, `name-leave-to` and `name-leave-active` will be removed. ends. Only then will the element be removed from the DOM.
For example, a simple fade in/out effect can be done with this: For example, a simple fade in/out effect can be done with this:
@@ -92,36 +93,3 @@ Notes:
Owl does not support more than one transition on a single node, so the Owl does not support more than one transition on a single node, so the
`t-transition` expression must be a single value (i.e. no space allowed). `t-transition` expression must be a single value (i.e. no space allowed).
## SCSS Mixins
If you use SCSS, you can use mixins to make generic animations. Here is an exemple with a fade in / fade out animation:
```scss
@mixin animation-fade($time, $name) {
.#{$name}_fade-enter-active,
.#{$name}_fade-active {
transition: all $time;
}
.#{$name}_fade-enter {
opacity: 0;
}
.#{$name}_fade-leave-to {
opacity: 0;
}
}
```
Usage:
```scss
@include animation-fade(0.5s, "o_notification");
```
You can now have in your template:
```xml
<SomeTag t-transition="o_notification_fade"/>
```
-33
View File
@@ -1,33 +0,0 @@
# 🦉 Browser 🦉
## Content
- [Overview](#overview)
- [Browser Content](#browser-content)
## Overview
The browser object contains some browser native APIs, such as `setTimeout`, that
are used by Owl and its utility functions. They are exposed with the intent of
making them mockable if necessary.
```js
owl.browser.setTimeout === window.setTimeout; // return true
```
For now, this object contains some functions that are not used by Owl. They
will eventually be removed in Owl 2.0.
## Browser Content
More specifically, the `browser` object contains the following methods and objects:
- `setTimeout`
- `clearTimeout`
- `setInterval`
- `clearInterval`
- `requestAnimationFrame`
- `random`
- `Date`
- `fetch`
- `localStorage`
+5 -36
View File
@@ -10,22 +10,13 @@
- [Static Properties](#static-properties) - [Static Properties](#static-properties)
- [Methods](#methods) - [Methods](#methods)
- [Lifecycle](#lifecycle) - [Lifecycle](#lifecycle)
- [`constructor(parent, props)`](#constructorparent-props)
- [`setup()`](#setup)
- [`willStart()`](#willstart)
- [`mounted()`](#mounted)
- [`willUpdateProps(nextProps)`](#willupdatepropsnextprops)
- [`willPatch()`](#willpatch)
- [`patched(snapshot)`](#patchedsnapshot)
- [`willUnmount()`](#willunmount)
- [`catchError(error)`](#catcherrorerror)
- [Root Component](#root-component) - [Root Component](#root-component)
- [Composition](#composition) - [Composition](#composition)
- [Form Input Bindings](#form-input-bindings) - [Form Input Bindings](#form-input-bindings)
- [References](#references) - [References](#references)
- [Dynamic sub components](#dynamic-sub-components) - [Dynamic sub components](#dynamic-sub-components)
- [Functional Components](#functional-components) - [Functional Components](#functional-components)
- [SVG Components](#svg-components) - [SVG components](#svg-components)
## Overview ## Overview
@@ -298,13 +289,8 @@ We explain here all the public methods of the `Component` class.
are updated. It returns a boolean, which indicates if the component should are updated. It returns a boolean, which indicates if the component should
ignore a props update. If it returns false, then `willUpdateProps` will not ignore a props update. If it returns false, then `willUpdateProps` will not
be called, and no rendering will occur. Its default implementation is to be called, and no rendering will occur. Its default implementation is to
always return true. Note that this is an optimization, similar to React's `shouldComponentUpdate`. Most of the time, this should not be used, but it always return true. This is an optimization, similar to React's `shouldComponentUpdate`. Most of the time, this should not be used, but it
can be useful if we are handling large number of components. Since this is an can be useful if we are handling large number of components.
optimization, Owl has the freedom to ignore the result of `shouldUpdate` in
some cases (for example, if a component is remounted, or if we want to force
a full rerender of the UI). However, if `shouldUpdate` returns true, then Owl
provides the guarantee that the component will be rendered at some point in
the future (except if the component is destroyed or if some part of the UI crashes).
* **`destroy()`**. As its name suggests, this method will remove the component, * **`destroy()`**. As its name suggests, this method will remove the component,
and perform all necessary cleanup, such as unmounting the component, its children, and perform all necessary cleanup, such as unmounting the component, its children,
@@ -325,7 +311,7 @@ a owl component:
| Method | Description | | Method | Description |
| ------------------------------------------------ | ----------------------------------------------------------- | | ------------------------------------------------ | ----------------------------------------------------------- |
| **[setup](#setup)** | setup | | **[constructor](#constructorparent-props)** | constructor |
| **[willStart](#willstart)** | async, before first rendering | | **[willStart](#willstart)** | async, before first rendering |
| **[mounted](#mounted)** | just after component is rendered and added to the DOM | | **[mounted](#mounted)** | just after component is rendered and added to the DOM |
| **[willUpdateProps](#willupdatepropsnextprops)** | async, before props update | | **[willUpdateProps](#willupdatepropsnextprops)** | async, before props update |
@@ -370,23 +356,6 @@ class ClickCounter extends owl.Component {
} }
``` ```
Hook functions can be called in the constructor.
#### `setup()`
_setup_ is run just after the component is constructed. It is a lifecycle method,
very similar to the _constructor_, except that it does not receive any argument.
It is a valid method to call hook functions. Note that one of the main reason to
have the `setup` hook in the component lifecycle is to make it possible to
monkey patch it. It is a common need in the Odoo ecosystem.
```javascript
setup() {
useSetupAutofocus();
}
```
#### `willStart()` #### `willStart()`
willStart is an asynchronous hook that can be implemented to willStart is an asynchronous hook that can be implemented to
@@ -789,7 +758,7 @@ template rendered with `props`. In Owl, this can be done by
simply defining a template, that will access the `props` object: simply defining a template, that will access the `props` object:
```js ```js
const Welcome = xml`<h1>Hello, <t t-esc="props.name"/></h1>`; const Welcome = xml`<h1>Hello, {props.name}</h1>`;
class MyComponent extends Component { class MyComponent extends Component {
static template = xml` static template = xml`
-3
View File
@@ -7,7 +7,6 @@ For example, `Component` is available at `owl.Component` and `EventBus` is
exported as `owl.core.EventBus`. exported as `owl.core.EventBus`.
``` ```
browser
Component misc Component misc
Context AsyncRoot Context AsyncRoot
QWeb Portal QWeb Portal
@@ -29,8 +28,6 @@ hooks utils
useContext useContext
useState useState
useRef useRef
useComponent
useEnv
useSubEnv useSubEnv
useStore useStore
useDispatch useDispatch
+14 -1
View File
@@ -133,4 +133,17 @@ the `QWeb` instance and a `browser` object:
- `qweb` will be set to an empty `QWeb` instance. This is absolutely necessary - `qweb` will be set to an empty `QWeb` instance. This is absolutely necessary
for Owl to be able to render anything for Owl to be able to render anything
- `browser`: this is an object that contains some common access points to the - `browser`: this is an object that contains some common access points to the
browser methods with a side effect. See [browser](browser.md) for more information. Note that the browser object will be removed from the environment in Owl 2.0. browser methods with a side effect. This is particularly useful when one want
to test more advanced components, and be able to mock those methods.
More specifically, the `browser` object contains the following methods and objects:
- `setTimeout`
- `clearTimeout`
- `setInterval`
- `clearInterval`
- `requestAnimationFrame`
- `random`
- `Date`
- `fetch`
- `localStorage`
+5 -22
View File
@@ -21,8 +21,6 @@
- [`useStore`](#usestore) - [`useStore`](#usestore)
- [`useDispatch`](#usedispatch) - [`useDispatch`](#usedispatch)
- [`useGetters`](#usegetters) - [`useGetters`](#usegetters)
- [`useComponent`](#usecomponent)
- [`useEnv`](#useenv)
- [Making customized hooks](#making-customized-hooks) - [Making customized hooks](#making-customized-hooks)
## Overview ## Overview
@@ -131,7 +129,7 @@ class SomeComponent extends Component {
### One rule ### One rule
There is only one rule: every hook for a component has to be called in the There is only one rule: every hook for a component has to be called in the
constructor, in the _setup_ method, or in class fields: constructor (or in class fields):
```js ```js
// ok // ok
@@ -147,13 +145,6 @@ class SomeComponent extends Component {
} }
} }
// also ok
class SomeComponent extends Component {
setup() {
this.state = useState({ value: 0 });
}
}
// not ok: this is executed after the constructor is called // not ok: this is executed after the constructor is called
class SomeComponent extends Component { class SomeComponent extends Component {
async willStart() { async willStart() {
@@ -390,16 +381,6 @@ The `useDispatch` hook is the way for components to get a reference to the store
The `useGetters` hook is the way for components to get a reference to the store The `useGetters` hook is the way for components to get a reference to the store
getters. See the [store documentation](store.md) for more information. getters. See the [store documentation](store.md) for more information.
### `useComponent`
The `useComponent` hook is useful as a building block for some customized hooks,
that may need a reference to the component calling them.
### `useEnv`
The `useEnv` hook is useful as a building block for some customized hooks,
that may need a reference to the env of the component calling them.
### Making customized hooks ### Making customized hooks
Hooks are a wonderful way to organize the code of a complex component by feature Hooks are a wonderful way to organize the code of a complex component by feature
@@ -454,11 +435,13 @@ not the solution to every problem.
```js ```js
function useRouter() { function useRouter() {
const env = useEnv(); return Component.current.env.router;
return env.router;
} }
``` ```
This means that we give control to the application developer to create the This means that we give control to the application developer to create the
router, which is good, so they can set it up, subclass it, ... And then, to router, which is good, so they can set it up, subclass it, ... And then, to
test our components, we can just add a mock router in the environment. test our components, we can just add a mock router in the environment.
Note: the code above makes use of the `Component.current` property. This is the
way hooks are able to get a reference to the component currently being created.
+5 -5
View File
@@ -15,7 +15,7 @@ use cases, there is no need to directly instantiate an observer.
For example, this code will display `update` in the console: For example, this code will display `update` in the console:
```javascript ```javascript
const observer = new owl.core.Observer(); const observer = new owl.Observer();
observer.notifyCB = () => console.log("update"); observer.notifyCB = () => console.log("update");
const obj = observer.observe({ a: { b: 1 } }); const obj = observer.observe({ a: { b: 1 } });
@@ -39,14 +39,14 @@ is incremented every time the value is observed. Sometimes, it can be useful
to obtain that number: to obtain that number:
```js ```js
const observer = new owl.core.Observer(); const observer = new owl.Observer();
const obj = observer.observe({ a: { b: 1 } }); const obj = observer.observe({ a: { b: 1 } });
observer.revNumber(obj.a); // 1 observer.deepRevNumber(obj.a); // 1
obj.a.b = 2; obj.a.b = 2;
observer.revNumber(obj.a); // 2 observer.deepRevNumber(obj.a); // 2
``` ```
The `revNumber` can also return 0, which indicates that the value is not The `deepRevNumber` can also return 0, which indicates that the value is not
observed. observed.
-37
View File
@@ -13,8 +13,6 @@
- [Setting Variables](#setting-variables) - [Setting Variables](#setting-variables)
- [Conditionals](#conditionals) - [Conditionals](#conditionals)
- [Dynamic Attributes](#dynamic-attributes) - [Dynamic Attributes](#dynamic-attributes)
- [Dynamic Class Attribute](#dynamic-class-attribute)
- [Dynamic Tag Names](#dynamic-tag-names)
- [Loops](#loops) - [Loops](#loops)
- [Rendering Sub Templates](#rendering-sub-templates) - [Rendering Sub Templates](#rendering-sub-templates)
- [Dynamic Sub Templates](#dynamic-sub-templates) - [Dynamic Sub Templates](#dynamic-sub-templates)
@@ -78,7 +76,6 @@ needs. Here is a list of all Owl specific directives:
| `t-transition` | [Defining an animation](animations.md#css-transitions) | | `t-transition` | [Defining an animation](animations.md#css-transitions) |
| `t-slot` | [Rendering a slot](slots.md) | | `t-slot` | [Rendering a slot](slots.md) |
| `t-model` | [Form input bindings](component.md#form-input-bindings) | | `t-model` | [Form input bindings](component.md#form-input-bindings) |
| `t-tag` | [Rendering nodes with dynamic tag name](#dynamic-tag-names) |
## Reference ## Reference
@@ -327,40 +324,6 @@ values) or a pair `[key, value]`. For example:
<div t-att="['a', 'b']"/> <!-- <div a="b"></div> --> <div t-att="['a', 'b']"/> <!-- <div a="b"></div> -->
``` ```
### Dynamic class attribute
For convenience, Owl supports a special case for the `t-att-class` case: one can
use an object with keys describing the classes, and values boolean value denoting
if the class is or is not present:
```xml
<div t-att-class="{'a': true, 'b': true}"/> <!-- result: <div class="a b"></div> -->
<div t-att-class="{'a b': true, 'c': true}"/> <!-- result: <div class="a b c"></div> -->
```
Note that it can be combined with normal class attribute:
```xml
<div class="a" t-att-class="{'b': true}"/> <!-- result: <div class="a b"></div> -->
```
### Dynamic tag names
When writing generic components or templates, the specific concrete tag for an
HTML element is not known yet. In those situations, the `t-tag` directive is
useful. It simply evaluates dynamically an expression to use as a tag name. The
template:
```xml
<t t-tag="tag">
<span>content</span>
</t>
```
will be rendered as `<div><span>content</span></div>` if the `tag` context key
is set to `div`.
### Loops ### Loops
QWeb has an iteration directive `t-foreach` which take an expression returning the QWeb has an iteration directive `t-foreach` which take an expression returning the
+1 -1
View File
@@ -44,7 +44,7 @@ Slots are defined by the caller, with the `t-set-slot` directive:
```xml ```xml
<div t-name="SomeComponent"> <div t-name="SomeComponent">
<div>some component</div> <div>some component</div>
<Dialog title="'Some Dialog'"> <Dialog title="Some Dialog">
<t t-set-slot="content"> <t t-set-slot="content">
<div>hey</div> <div>hey</div>
</t> </t>
+6 -6
View File
@@ -62,14 +62,14 @@ The CSS tag is useful to define a css stylesheet in the javascript file:
```js ```js
class MyComponent extends Component { class MyComponent extends Component {
static template = xml` static template = xml`
<div class="my-component">some template</div> <div class="my-component">some template</div>
`; `;
static style = css` static css`
.my-component { .my-component {
color: red; color: red;
} }
`; `;
} }
``` ```
+10 -13
View File
@@ -1,6 +1,6 @@
{ {
"name": "@odoo/owl", "name": "@odoo/owl",
"version": "1.4.5", "version": "1.0.13",
"description": "Odoo Web Library (OWL)", "description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js", "main": "dist/owl.cjs.js",
"browser": "dist/owl.iife.js", "browser": "dist/owl.iife.js",
@@ -10,11 +10,11 @@
"dist" "dist"
], ],
"engines": { "engines": {
"node": ">=12.18.3" "node": ">=10.15.3"
}, },
"scripts": { "scripts": {
"build:bundle": "rollup -c", "dev": "rollup -c",
"build": "npm run build:bundle", "build": "NODE_ENV=production rollup -c",
"test": "jest", "test": "jest",
"test:watch": "jest --watch", "test:watch": "jest --watch",
"tools:serve": "python3 tools/server.py || python tools/server.py", "tools:serve": "python3 tools/server.py || python tools/server.py",
@@ -22,7 +22,6 @@
"pretools:watch": "npm run build", "pretools:watch": "npm run build",
"tools:watch": "npm-run-all --parallel tools:serve \"build:* -- --watch\"", "tools:watch": "npm-run-all --parallel tools:serve \"build:* -- --watch\"",
"prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --write", "prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --write",
"check-formatting": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --check",
"publish": "npm run build && npm publish", "publish": "npm run build && npm publish",
"release": "node tools/release.js" "release": "node tools/release.js"
}, },
@@ -37,29 +36,27 @@
}, },
"homepage": "https://github.com/odoo/owl#readme", "homepage": "https://github.com/odoo/owl#readme",
"devDependencies": { "devDependencies": {
"@types/jest": "^27.0.1", "@types/jest": "^23.3.14",
"@types/node": "^14.11.8", "@types/node": "^14.11.8",
"chalk": "^3.0.0", "chalk": "^3.0.0",
"cpx": "^1.5.0", "cpx": "^1.5.0",
"current-git-branch": "^1.1.0",
"git-rev-sync": "^1.12.0", "git-rev-sync": "^1.12.0",
"github-api": "^3.3.0", "github-api": "^3.3.0",
"jest": "^27.1.0", "jest": "^23.6.0",
"jest-environment-jsdom": "^27.1.0", "jest-environment-jsdom": "^24.7.1",
"live-server": "^1.2.1", "live-server": "^1.2.1",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^2.0.4", "prettier": "^2.0.4",
"rollup": "^2.56.3", "rollup": "^1.6.0",
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.30.0", "rollup-plugin-typescript2": "^0.27.3",
"sass": "^1.16.1", "sass": "^1.16.1",
"source-map-support": "^0.5.10", "source-map-support": "^0.5.10",
"ts-jest": "^27.0.5", "ts-jest": "^23.10.5",
"typescript": "^3.7.2", "typescript": "^3.7.2",
"uglify-es": "^3.3.9" "uglify-es": "^3.3.9"
}, },
"jest": { "jest": {
"testEnvironment": "jsdom",
"roots": [ "roots": [
"<rootDir>/src", "<rootDir>/src",
"<rootDir>/tests" "<rootDir>/tests"
+1 -1
View File
@@ -1,6 +1,6 @@
# 🦉 OWL Roadmap 🦉 # 🦉 OWL Roadmap 🦉
- Current version: 1.4.5 - Current version: 1.0.13
- Status: stable - Status: stable
This roadmap is only an attempt at predicting Owl's future. Everything may This roadmap is only an attempt at predicting Owl's future. Everything may
+1 -8
View File
@@ -10,8 +10,6 @@ export interface Browser {
localStorage: Window["localStorage"]; localStorage: Window["localStorage"];
} }
let localStorage: Window["localStorage"] | null = null;
export const browser: Browser = { export const browser: Browser = {
setTimeout: window.setTimeout.bind(window), setTimeout: window.setTimeout.bind(window),
clearTimeout: window.clearTimeout.bind(window), clearTimeout: window.clearTimeout.bind(window),
@@ -21,10 +19,5 @@ export const browser: Browser = {
random: Math.random, random: Math.random,
Date: window.Date, Date: window.Date,
fetch: (window.fetch || (() => {})).bind(window), fetch: (window.fetch || (() => {})).bind(window),
get localStorage() { localStorage: window.localStorage,
return localStorage || window.localStorage;
},
set localStorage(newLocalStorage: Window["localStorage"]) {
localStorage = newLocalStorage;
},
}; };
+116 -98
View File
@@ -8,6 +8,7 @@ import "./props_validation";
import { Scheduler, scheduler } from "./scheduler"; import { Scheduler, scheduler } from "./scheduler";
import { activateSheet } from "./styles"; import { activateSheet } from "./styles";
import { Browser, browser } from "../browser"; import { Browser, browser } from "../browser";
import { onMounted, onPatched } from "../hooks";
/** /**
* Owl Component System * Owl Component System
@@ -44,15 +45,6 @@ interface MountOptions {
position?: MountPosition; position?: MountPosition;
} }
export const enum STATUS {
CREATED,
WILLSTARTED, // willstart has been called
RENDERED, // first render is completed (so, vnode is now defined)
MOUNTED, // is ready, and in DOM. It has a valid el
UNMOUNTED, // has a valid el, but is not in DOM
DESTROYED,
}
/** /**
* This is mostly an internal detail of implementation. The Meta interface is * This is mostly an internal detail of implementation. The Meta interface is
* useful to typecheck and describe the internal keys used by Owl to manage the * useful to typecheck and describe the internal keys used by Owl to manage the
@@ -65,7 +57,8 @@ interface Internal<T extends Env> {
depth: number; depth: number;
vnode: VNode | null; vnode: VNode | null;
pvnode: VNode | null; pvnode: VNode | null;
status: STATUS; isMounted: boolean;
isDestroyed: boolean;
// parent and children keys are obviously useful to setup the parent-children // parent and children keys are obviously useful to setup the parent-children
// relationship. // relationship.
@@ -99,8 +92,24 @@ interface Internal<T extends Env> {
refs: { [key: string]: Component<any, T> | HTMLElement | undefined } | null; refs: { [key: string]: Component<any, T> | HTMLElement | undefined } | null;
} }
interface DevToolsAccess {
props?: any;
defaultProps?: any;
template?: string | null;
state: Observer;
tag: String;
depth: number,
}
export const portalSymbol = Symbol("portal"); // FIXME export const portalSymbol = Symbol("portal"); // FIXME
/**
* It is required for the dev tools to have access to the __owl__ element.
*/
interface HTMLElementWithDevToolsAccess extends HTMLElement {
__owl_devtools__: DevToolsAccess
}
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Component // Component
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -118,11 +127,13 @@ export class Component<Props extends {} = any, T extends Env = Env> {
// expose scheduler s.t. it can be mocked for testing purposes // expose scheduler s.t. it can be mocked for testing purposes
static scheduler: Scheduler = scheduler; static scheduler: Scheduler = scheduler;
__devtools__: DevToolsAccess;
/** /**
* The `el` is the root element of the component. Note that it could be null: * The `el` is the root element of the component. Note that it could be null:
* this is the case if the component is not mounted yet, or is destroyed. * this is the case if the component is not mounted yet, or is destroyed.
*/ */
get el(): HTMLElement | null { get el(): HTMLElementWithDevToolsAccess | null {
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null; return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
} }
@@ -167,23 +178,20 @@ export class Component<Props extends {} = any, T extends Env = Env> {
if (!this.env.qweb) { if (!this.env.qweb) {
this.env.qweb = new QWeb(); this.env.qweb = new QWeb();
} }
// TODO: remove this in owl 2.0
if (!this.env.browser) { if (!this.env.browser) {
this.env.browser = browser; this.env.browser = browser;
} }
this.env.qweb.on("update", this, () => { this.env.qweb.on("update", this, () => {
switch (this.__owl__.status) { if (this.__owl__.isMounted) {
case STATUS.MOUNTED: this.render(true);
this.render(true); }
break; if (this.__owl__.isDestroyed) {
case STATUS.DESTROYED: // this is unlikely to happen, but if a root widget is destroyed,
// this is unlikely to happen, but if a root widget is destroyed, // we want to remove our subscription. The usual way to do that
// we want to remove our subscription. The usual way to do that // would be to perform some check in the destroy method, but since
// would be to perform some check in the destroy method, but since // it is very performance sensitive, and since this is a rare event,
// it is very performance sensitive, and since this is a rare event, // we simply do it lazily
// we simply do it lazily this.env.qweb.off("update", this);
this.env.qweb.off("update", this);
break;
} }
}); });
depth = 0; depth = 0;
@@ -196,7 +204,8 @@ export class Component<Props extends {} = any, T extends Env = Env> {
depth: depth, depth: depth,
vnode: null, vnode: null,
pvnode: null, pvnode: null,
status: STATUS.CREATED, isMounted: false,
isDestroyed: false,
parent: parent || null, parent: parent || null,
children: {}, children: {},
cmap: {}, cmap: {},
@@ -218,19 +227,25 @@ export class Component<Props extends {} = any, T extends Env = Env> {
if (constr.style) { if (constr.style) {
this.__applyStyles(constr); this.__applyStyles(constr);
} }
this.setup();
}
/** // DevTools hooks
* setup is run just after the component is constructed. This is the standard onMounted(() => {
* location where the component can setup its hooks. It has some advantages this.__devtools__ = {
* over the constructor: depth: this.__owl__.depth,
* - it can be patched (useful in odoo ecosystem) state: this.__owl__.observer,
* - it does not need to propagate the arguments to the super call tag: this.constructor.name
* };
* Note: this method should not be called manually. this.__devtools__.defaultProps = defaultProps;
*/ this.__devtools__.props = this.props;
setup() {} this.__devtools__.template = template;
this.el.__owl_devtools__ = this.__devtools__;
})
onPatched(() => {
this.__devtools__.depth = this.__owl__.depth,
this.el.__owl_devtools__ = this.__devtools__;
})
}
/** /**
* willStart is an asynchronous hook that can be implemented to perform some * willStart is an asynchronous hook that can be implemented to perform some
@@ -326,49 +341,42 @@ export class Component<Props extends {} = any, T extends Env = Env> {
* Note that a component can be mounted an unmounted several times * Note that a component can be mounted an unmounted several times
*/ */
async mount(target: HTMLElement | DocumentFragment, options: MountOptions = {}): Promise<void> { async mount(target: HTMLElement | DocumentFragment, options: MountOptions = {}): Promise<void> {
const position = options.position || "last-child";
const __owl__ = this.__owl__;
if (__owl__.isMounted) {
if (position !== "self" && this.el!.parentNode !== target) {
// in this situation, we are trying to mount a component on a different
// target. In this case, we need to unmount first, otherwise it will
// not work.
this.unmount();
} else {
return Promise.resolve();
}
}
if (__owl__.isDestroyed) {
throw new Error("Cannot mount a destroyed component");
}
if (__owl__.currentFiber) {
const currentFiber = __owl__.currentFiber;
if (currentFiber.target === target && currentFiber.position === position) {
return scheduler.addFiber(currentFiber);
} else {
scheduler.rejectFiber(currentFiber, "Mounting operation cancelled");
}
}
if (!(target instanceof HTMLElement || target instanceof DocumentFragment)) { if (!(target instanceof HTMLElement || target instanceof DocumentFragment)) {
let message = `Component '${this.constructor.name}' cannot be mounted: the target is not a valid DOM node.`; let message = `Component '${this.constructor.name}' cannot be mounted: the target is not a valid DOM node.`;
message += `\nMaybe the DOM is not ready yet? (in that case, you can use owl.utils.whenReady)`; message += `\nMaybe the DOM is not ready yet? (in that case, you can use owl.utils.whenReady)`;
throw new Error(message); throw new Error(message);
} }
const position = options.position || "last-child"; const fiber = new Fiber(null, this, false, target, position);
const __owl__ = this.__owl__; fiber.shouldPatch = false;
const currentFiber = __owl__.currentFiber; if (!__owl__.vnode) {
this.__prepareAndRender(fiber, () => {});
switch (__owl__.status) { } else {
case STATUS.CREATED: { this.__render(fiber);
const fiber = new Fiber(null, this, true, target, position);
fiber.shouldPatch = false;
this.__prepareAndRender(fiber, () => {});
return scheduler.addFiber(fiber);
}
case STATUS.WILLSTARTED:
case STATUS.RENDERED:
currentFiber.target = target;
currentFiber.position = position;
return scheduler.addFiber(currentFiber);
case STATUS.UNMOUNTED: {
const fiber = new Fiber(null, this, true, target, position);
fiber.shouldPatch = false;
this.__render(fiber);
return scheduler.addFiber(fiber);
}
case STATUS.MOUNTED: {
if (position !== "self" && this.el!.parentNode !== target) {
const fiber = new Fiber(null, this, true, target, position);
fiber.shouldPatch = false;
this.__render(fiber);
return scheduler.addFiber(fiber);
} else {
return Promise.resolve();
}
}
case STATUS.DESTROYED:
throw new Error("Cannot mount a destroyed component");
} }
return scheduler.addFiber(fiber);
} }
/** /**
@@ -376,7 +384,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
* to call willUnmount calls and remove the component from the DOM. * to call willUnmount calls and remove the component from the DOM.
*/ */
unmount() { unmount() {
if (this.__owl__.status === STATUS.MOUNTED) { if (this.__owl__.isMounted) {
this.__callWillUnmount(); this.__callWillUnmount();
this.el!.remove(); this.el!.remove();
} }
@@ -394,7 +402,10 @@ export class Component<Props extends {} = any, T extends Env = Env> {
async render(force: boolean = false): Promise<void> { async render(force: boolean = false): Promise<void> {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
const currentFiber = __owl__.currentFiber; const currentFiber = __owl__.currentFiber;
if (!__owl__.vnode && !currentFiber) { if (!__owl__.isMounted && !currentFiber) {
// if we get here, this means that the component was either never mounted,
// or was unmounted and some state change triggered a render. Either way,
// we do not want to actually render anything in this case.
return; return;
} }
if (currentFiber && !currentFiber.isRendered && !currentFiber.isCompleted) { if (currentFiber && !currentFiber.isRendered && !currentFiber.isCompleted) {
@@ -403,13 +414,15 @@ export class Component<Props extends {} = any, T extends Env = Env> {
// if we aren't mounted at this point, it implies that there is a // if we aren't mounted at this point, it implies that there is a
// currentFiber that is already rendered (isRendered is true), so we are // currentFiber that is already rendered (isRendered is true), so we are
// about to be mounted // about to be mounted
const status = __owl__.status; const isMounted = __owl__.isMounted;
const fiber = new Fiber(null, this, force, null, null); const fiber = new Fiber(null, this, force, null, null);
Promise.resolve().then(() => { Promise.resolve().then(() => {
if (__owl__.status === STATUS.MOUNTED || status !== STATUS.MOUNTED) { if (__owl__.isMounted || !isMounted) {
if (fiber.isCompleted || fiber.isRendered) { if (fiber.isCompleted) {
return; return;
} }
// we are mounted (__owl__.isMounted), or if we are currently being
// mounted (!isMounted), so we call __render
this.__render(fiber); this.__render(fiber);
} else { } else {
// we were mounted when render was called, but we aren't anymore, so we // we were mounted when render was called, but we aren't anymore, so we
@@ -433,7 +446,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
*/ */
destroy() { destroy() {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
if (__owl__.status !== STATUS.DESTROYED) { if (!__owl__.isDestroyed) {
const el = this.el; const el = this.el;
this.__destroy(__owl__.parent); this.__destroy(__owl__.parent);
if (el) { if (el) {
@@ -478,12 +491,13 @@ export class Component<Props extends {} = any, T extends Env = Env> {
*/ */
__destroy(parent: Component | null) { __destroy(parent: Component | null) {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
if (__owl__.status === STATUS.MOUNTED) { const isMounted = __owl__.isMounted;
if (isMounted) {
if (__owl__.willUnmountCB) { if (__owl__.willUnmountCB) {
__owl__.willUnmountCB(); __owl__.willUnmountCB();
} }
this.willUnmount(); this.willUnmount();
__owl__.status = STATUS.UNMOUNTED; __owl__.isMounted = false;
} }
const children = __owl__.children; const children = __owl__.children;
for (let key in children) { for (let key in children) {
@@ -494,7 +508,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
delete parent.__owl__.children[id]; delete parent.__owl__.children[id];
__owl__.parent = null; __owl__.parent = null;
} }
__owl__.status = STATUS.DESTROYED; __owl__.isDestroyed = true;
delete __owl__.vnode; delete __owl__.vnode;
if (__owl__.currentFiber) { if (__owl__.currentFiber) {
__owl__.currentFiber.isCompleted = true; __owl__.currentFiber.isCompleted = true;
@@ -504,7 +518,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
__callMounted() { __callMounted() {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
__owl__.status = STATUS.MOUNTED; __owl__.isMounted = true;
__owl__.currentFiber = null; __owl__.currentFiber = null;
this.mounted(); this.mounted();
if (__owl__.mountedCB) { if (__owl__.mountedCB) {
@@ -518,7 +532,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
__owl__.willUnmountCB(); __owl__.willUnmountCB();
} }
this.willUnmount(); this.willUnmount();
__owl__.status = STATUS.UNMOUNTED; __owl__.isMounted = false;
if (__owl__.currentFiber) { if (__owl__.currentFiber) {
__owl__.currentFiber.isCompleted = true; __owl__.currentFiber.isCompleted = true;
__owl__.currentFiber.root.counter = 0; __owl__.currentFiber.root.counter = 0;
@@ -526,7 +540,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
const children = __owl__.children; const children = __owl__.children;
for (let id in children) { for (let id in children) {
const comp = children[id]; const comp = children[id];
if (comp.__owl__.status === STATUS.MOUNTED) { if (comp.__owl__.isMounted) {
comp.__callWillUnmount(); comp.__callWillUnmount();
} }
} }
@@ -647,25 +661,18 @@ export class Component<Props extends {} = any, T extends Env = Env> {
} }
return p._template; return p._template;
} }
async __prepareAndRender(fiber: Fiber, cb: CallableFunction) { async __prepareAndRender(fiber: Fiber, cb: CallableFunction) {
try { try {
const proms = Promise.all([ await Promise.all([this.willStart(), this.__owl__.willStartCB && this.__owl__.willStartCB()]);
this.willStart(),
this.__owl__.willStartCB && this.__owl__.willStartCB(),
]);
this.__owl__.status = STATUS.WILLSTARTED;
await proms;
if (this.__owl__.status === <any>STATUS.DESTROYED) {
return Promise.resolve();
}
} catch (e) { } catch (e) {
fiber.handleError(e); fiber.handleError(e);
return Promise.resolve(); return Promise.resolve();
} }
if (this.__owl__.isDestroyed) {
return Promise.resolve();
}
if (!fiber.isCompleted) { if (!fiber.isCompleted) {
this.__render(fiber); this.__render(fiber);
this.__owl__.status = STATUS.RENDERED;
cb(); cb();
} }
} }
@@ -688,7 +695,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
for (let childKey in __owl__.children) { for (let childKey in __owl__.children) {
const child = __owl__.children[childKey]; const child = __owl__.children[childKey];
const childOwl = child.__owl__; const childOwl = child.__owl__;
if (childOwl.status !== STATUS.MOUNTED && childOwl.parentLastFiberId < fiber.id) { if (!childOwl.isMounted && childOwl.parentLastFiberId < fiber.id) {
// we only do here a "soft" destroy, meaning that we leave the child // we only do here a "soft" destroy, meaning that we leave the child
// dom node alone, without removing it. Most of the time, it does not // dom node alone, without removing it. Most of the time, it does not
// matter, because the child component is already unmounted. However, // matter, because the child component is already unmounted. However,
@@ -735,6 +742,17 @@ export class Component<Props extends {} = any, T extends Env = Env> {
} }
} }
/**
* Only called by qweb t-component directive (when t-keepalive is set)
*/
__remount() {
const __owl__ = this.__owl__;
if (!__owl__.isMounted) {
__owl__.isMounted = true;
this.mounted();
}
}
/** /**
* Apply default props (only top level). * Apply default props (only top level).
* *
+13 -33
View File
@@ -1,7 +1,6 @@
import { QWeb } from "../qweb/index"; import { QWeb } from "../qweb/index";
import { INTERP_REGEXP } from "../qweb/compilation_context"; import { INTERP_REGEXP } from "../qweb/compilation_context";
import { makeHandlerCode, MODS_CODE } from "../qweb/extensions"; import { makeHandlerCode, MODS_CODE } from "../qweb/extensions";
import { STATUS } from "./component";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// t-component // t-component
@@ -245,18 +244,7 @@ QWeb.addDirective({
.join(","); .join(",");
let componentID = ctx.generateID(); let componentID = ctx.generateID();
let hasDefinedKey = false; const templateKey = ctx.generateTemplateKey();
let templateKey;
if (node.tagName === "t" && !node.hasAttribute("t-key") && value.match(INTERP_REGEXP)) {
defineComponentKey();
const id = ctx.generateID();
// the ___ is to make sure we have no possible conflict with normal
// template keys
ctx.addLine(`let k${id} = '___' + componentKey${componentID}`);
templateKey = `k${id}`;
} else {
templateKey = ctx.generateTemplateKey();
}
let ref = node.getAttribute("t-ref"); let ref = node.getAttribute("t-ref");
let refExpr = ""; let refExpr = "";
let refKey: string = ""; let refKey: string = "";
@@ -302,7 +290,7 @@ QWeb.addDirective({
if (tattClass) { if (tattClass) {
let tattExpr = ctx.formatExpression(tattClass); let tattExpr = ctx.formatExpression(tattClass);
if (tattExpr[0] !== "{" || tattExpr[tattExpr.length - 1] !== "}") { if (tattExpr[0] !== "{" || tattExpr[tattExpr.length - 1] !== "}") {
tattExpr = `utils.toClassObj(${tattExpr})`; tattExpr = `utils.toObj(${tattExpr})`;
} }
if (classAttr) { if (classAttr) {
ctx.addLine(`Object.assign(${classObj}, ${tattExpr})`); ctx.addLine(`Object.assign(${classObj}, ${tattExpr})`);
@@ -347,7 +335,7 @@ QWeb.addDirective({
} }
if (hasDynamicProps) { if (hasDynamicProps) {
const dynamicProp = ctx.formatExpression(node.getAttribute("t-props")!); const dynamicProp = ctx.formatExpression(node.getAttribute("t-props")!);
ctx.addLine(`let props${componentID} = Object.assign({}, ${dynamicProp}, {${propStr}});`); ctx.addLine(`let props${componentID} = Object.assign({${propStr}}, ${dynamicProp});`);
} else { } else {
ctx.addLine(`let props${componentID} = {${propStr}};`); ctx.addLine(`let props${componentID} = {${propStr}};`);
} }
@@ -366,14 +354,14 @@ QWeb.addDirective({
// SLOTS // SLOTS
const hasSlots = node.childNodes.length; const hasSlots = node.childNodes.length;
let scope = hasSlots ? `utils.combine(context, scope)` : "undefined"; let scope = hasSlots ? `Object.assign(Object.create(context), scope)` : "undefined";
ctx.addIf(`w${componentID}`); ctx.addIf(`w${componentID}`);
// need to update component // need to update component
let styleCode = ""; let styleCode = "";
if (tattStyle) { if (tattStyle) {
styleCode = `.then(()=>{if (w${componentID}.__owl__.status === ${STATUS.DESTROYED}) {return};w${componentID}.el.style=${tattStyle};});`; styleCode = `.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};w${componentID}.el.style=${tattStyle};});`;
} }
ctx.addLine( ctx.addLine(
`w${componentID}.__updateProps(props${componentID}, extra.fiber, ${scope})${styleCode};` `w${componentID}.__updateProps(props${componentID}, extra.fiber, ${scope})${styleCode};`
@@ -389,17 +377,14 @@ QWeb.addDirective({
ctx.addElse(); ctx.addElse();
// new component // new component
function defineComponentKey() { let dynamicFallback = "";
if (!hasDefinedKey) { if (!value.match(INTERP_REGEXP)) {
const interpValue = ctx.interpolate(value); dynamicFallback = `|| ${ctx.formatExpression(value)}`;
ctx.addLine(`let componentKey${componentID} = ${interpValue};`);
hasDefinedKey = true;
}
} }
defineComponentKey(); const interpValue = ctx.interpolate(value);
const contextualValue = value.match(INTERP_REGEXP) ? "false" : ctx.formatExpression(value); ctx.addLine(`let componentKey${componentID} = ${interpValue};`);
ctx.addLine( ctx.addLine(
`let W${componentID} = ${contextualValue} || context.constructor.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}];` `let W${componentID} = context.constructor.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}]${dynamicFallback};`
); );
// maybe only do this in dev mode... // maybe only do this in dev mode...
@@ -469,17 +454,12 @@ QWeb.addDirective({
} }
} }
if (clone.childNodes.length) { if (clone.childNodes.length) {
let hasContent = false;
const t = clone.ownerDocument!.createElement("t"); const t = clone.ownerDocument!.createElement("t");
for (let child of Object.values(clone.childNodes)) { for (let child of Object.values(clone.childNodes)) {
hasContent =
hasContent || (child instanceof Text ? Boolean(child.textContent.trim().length) : true);
t.appendChild(child); t.appendChild(child);
} }
if (hasContent) { const slotFn = qweb._compile(`slot_default_template`, { elem: t, hasParent: true });
const slotFn = qweb._compile(`slot_default_template`, { elem: t, hasParent: true }); QWeb.slots[`${slotId}_default`] = slotFn;
QWeb.slots[`${slotId}_default`] = slotFn;
}
} }
} }
+29 -65
View File
@@ -1,5 +1,5 @@
import { h, VNode } from "../vdom/index"; import { h, VNode } from "../vdom/index";
import { Component, MountPosition, STATUS } from "./component"; import { Component, MountPosition } from "./component";
import { scheduler } from "./scheduler"; import { scheduler } from "./scheduler";
/** /**
@@ -82,7 +82,6 @@ export class Fiber {
let oldFiber = __owl__.currentFiber; let oldFiber = __owl__.currentFiber;
if (oldFiber && !oldFiber.isCompleted) { if (oldFiber && !oldFiber.isCompleted) {
this.force = true;
if (oldFiber.root === oldFiber && !parent) { if (oldFiber.root === oldFiber && !parent) {
// both oldFiber and this fiber are root fibers // both oldFiber and this fiber are root fibers
this._reuseFiber(oldFiber); this._reuseFiber(oldFiber);
@@ -107,8 +106,6 @@ export class Fiber {
*/ */
_reuseFiber(oldFiber: Fiber) { _reuseFiber(oldFiber: Fiber) {
oldFiber.cancel(); // cancel children fibers oldFiber.cancel(); // cancel children fibers
oldFiber.target = this.target || oldFiber.target;
oldFiber.position = this.position || oldFiber.position;
oldFiber.isCompleted = false; // keep the root fiber alive oldFiber.isCompleted = false; // keep the root fiber alive
oldFiber.isRendered = false; // the fiber has to be re-rendered oldFiber.isRendered = false; // the fiber has to be re-rendered
if (oldFiber.child) { if (oldFiber.child) {
@@ -190,8 +187,7 @@ export class Fiber {
complete() { complete() {
let component = this.component; let component = this.component;
this.isCompleted = true; this.isCompleted = true;
const status = component.__owl__.status; if (!this.target && !component.__owl__.isMounted) {
if (status === STATUS.DESTROYED) {
return; return;
} }
@@ -205,16 +201,14 @@ export class Fiber {
const patchLen = patchQueue.length; const patchLen = patchQueue.length;
// call willPatch hook on each fiber of patchQueue // call willPatch hook on each fiber of patchQueue
if (status === STATUS.MOUNTED) { for (let i = 0; i < patchLen; i++) {
for (let i = 0; i < patchLen; i++) { const fiber = patchQueue[i];
const fiber = patchQueue[i]; if (fiber.shouldPatch) {
if (fiber.shouldPatch) { component = fiber.component;
component = fiber.component; if (component.__owl__.willPatchCB) {
if (component.__owl__.willPatchCB) { component.__owl__.willPatchCB();
component.__owl__.willPatchCB();
}
component.willPatch();
} }
component.willPatch();
} }
} }
@@ -255,9 +249,8 @@ export class Fiber {
component.__owl__.pvnode!.elm = component.__owl__.vnode!.elm; component.__owl__.pvnode!.elm = component.__owl__.vnode!.elm;
} }
} }
const compOwl = component.__owl__; if (fiber === component.__owl__.currentFiber) {
if (fiber === compOwl.currentFiber) { component.__owl__.currentFiber = null;
compOwl.currentFiber = null;
} }
} }
@@ -277,24 +270,16 @@ export class Fiber {
} }
// call patched/mounted hook on each fiber of (reversed) patchQueue // call patched/mounted hook on each fiber of (reversed) patchQueue
if (status === STATUS.MOUNTED || inDOM) { for (let i = patchLen - 1; i >= 0; i--) {
for (let i = patchLen - 1; i >= 0; i--) { const fiber = patchQueue[i];
const fiber = patchQueue[i]; component = fiber.component;
component = fiber.component; if (fiber.shouldPatch && !this.target) {
if (fiber.shouldPatch && !this.target) { component.patched();
component.patched(); if (component.__owl__.patchedCB) {
if (component.__owl__.patchedCB) { component.__owl__.patchedCB();
component.__owl__.patchedCB();
}
} else {
component.__callMounted();
} }
} } else if (this.target ? inDOM : true) {
} else { component.__callMounted();
for (let i = patchLen - 1; i >= 0; i--) {
const fiber = patchQueue[i];
component = fiber.component;
component.__owl__.status = STATUS.UNMOUNTED;
} }
} }
} }
@@ -326,43 +311,22 @@ export class Fiber {
const qweb = component.env.qweb; const qweb = component.env.qweb;
let root = component; let root = component;
let canCatch = false;
function handle(error) { while (component && !(canCatch = !!component.catchError)) {
let canCatch = false; root = component;
qweb.trigger("error", error); component = component.__owl__.parent!;
while (component && !(canCatch = !!component.catchError)) {
root = component;
component = component.__owl__.parent!;
}
if (canCatch) {
try {
component.catchError!(error);
} catch (e) {
root = component;
component = component.__owl__.parent!;
return handle(e);
}
return true;
}
return false;
} }
qweb.trigger("error", error);
let isHandled = handle(error); if (canCatch) {
component.catchError!(error);
if (!isHandled) { } else {
// the 3 next lines aim to mark the root fiber as being in error, and // the 3 next lines aim to mark the root fiber as being in error, and
// to force it to end, without waiting for its children // to force it to end, without waiting for its children
this.root.counter = 0; this.root.counter = 0;
this.root.error = error; this.root.error = error;
scheduler.flush(); scheduler.flush();
// at this point, the state of the application is corrupted and we could root.destroy();
// have a lot of issues or crashes. So we destroy the application in a try
// catch and swallow these errors because the fiber is already in error,
// and this is the actual issue that needs to be solved, not those followup
// errors.
try {
root.destroy();
} catch (e) {}
} }
} }
} }
+5 -9
View File
@@ -1,5 +1,4 @@
import { QWeb } from "./qweb/index"; import { QWeb } from "./qweb/index";
import { TRANSLATABLE_ATTRS } from "./qweb/qweb";
/** /**
* This file creates and exports the OWL 'config' object, with keys: * This file creates and exports the OWL 'config' object, with keys:
@@ -10,12 +9,9 @@ import { TRANSLATABLE_ATTRS } from "./qweb/qweb";
interface Config { interface Config {
mode: string; mode: string;
enableTransitions: boolean; enableTransitions: boolean;
translatableAttributes: string[];
} }
export const config = { export const config = {} as Config;
translatableAttributes: TRANSLATABLE_ATTRS,
} as Config;
Object.defineProperty(config, "mode", { Object.defineProperty(config, "mode", {
get() { get() {
@@ -24,10 +20,10 @@ Object.defineProperty(config, "mode", {
set(mode: string) { set(mode: string) {
QWeb.dev = mode === "dev"; QWeb.dev = mode === "dev";
if (QWeb.dev) { if (QWeb.dev) {
console.info(`Owl is running in 'dev' mode. const url = `https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode`;
console.warn(
This is not suitable for production use. `Owl is running in 'dev' mode. This is not suitable for production use. See ${url} for more information.`
See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for more information.`); );
} else { } else {
console.log(`Owl is now running in 'prod' mode.`); console.log(`Owl is now running in 'prod' mode.`);
} }
+10
View File
@@ -115,6 +115,16 @@ export function useContextWithCB(ctx: Context, component: Component, method): an
__owl__.observer = new Observer(); __owl__.observer = new Observer();
__owl__.observer.notifyCB = component.render.bind(component); __owl__.observer.notifyCB = component.render.bind(component);
} }
const currentCB = __owl__.observer.notifyCB;
__owl__.observer.notifyCB = function () {
if (ctx.rev > mapping[id]) {
// in this case, the context has been updated since we were rendering
// last, and we do not need to render here with the observer. A
// rendering is coming anyway, with the correct props.
return;
}
currentCB();
};
mapping[id] = 0; mapping[id] = 0;
const renderFn = __owl__.renderFn; const renderFn = __owl__.renderFn;
-1
View File
@@ -21,7 +21,6 @@ export class Observer {
rev: number = 1; rev: number = 1;
allowMutations: boolean = true; allowMutations: boolean = true;
weakMap: WeakMap<any, any> = new WeakMap(); weakMap: WeakMap<any, any> = new WeakMap();
notifyCB() {} notifyCB() {}
observe<T>(value: T, parent?: any): T { observe<T>(value: T, parent?: any): T {
+1 -21
View File
@@ -1,4 +1,4 @@
import { Component, Env } from "./component/component"; import { Component } from "./component/component";
import { Observer } from "./core/observer"; import { Observer } from "./core/observer";
/** /**
@@ -118,26 +118,6 @@ export function useRef<C extends Component = Component>(name: string): Ref<C> {
}; };
} }
// -----------------------------------------------------------------------------
// "Builder" hooks
// -----------------------------------------------------------------------------
/**
* This hook is useful as a building block for some customized hooks, that may
* need a reference to the component calling them.
*/
export function useComponent<P, E extends Env>(): Component<P, E> {
return Component.current as any;
}
/**
* This hook is useful as a building block for some customized hooks, that may
* need a reference to the env of the component calling them.
*/
export function useEnv<E extends Env>(): E {
return Component.current.env as any;
}
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// useSubEnv // useSubEnv
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
-1
View File
@@ -22,7 +22,6 @@ import { Router } from "./router/router";
export { Component, mount } from "./component/component"; export { Component, mount } from "./component/component";
export { QWeb }; export { QWeb };
export { config }; export { config };
export { browser } from "./browser";
export const Context = _context.Context; export const Context = _context.Context;
export const useState = _hooks.useState; export const useState = _hooks.useState;
+1 -3
View File
@@ -281,9 +281,7 @@ QWeb.addDirective({
// Step 4: add the appropriate function call to current component // Step 4: add the appropriate function call to current component
// ------------------------------------------------ // ------------------------------------------------
const parentComponent = ctx.rootContext.shouldDefineParent const parentComponent = `utils.getComponent(context)`;
? `parent`
: `utils.getComponent(context)`;
const key = ctx.generateTemplateKey(); const key = ctx.generateTemplateKey();
const parentNode = ctx.parentNode ? `c${ctx.parentNode}` : "result"; const parentNode = ctx.parentNode ? `c${ctx.parentNode}` : "result";
const extra = `Object.assign({}, extra, {parentNode: ${parentNode}, parent: ${parentComponent}, key: ${key}})`; const extra = `Object.assign({}, extra, {parentNode: ${parentNode}, parent: ${parentComponent}, key: ${key}})`;
+6 -53
View File
@@ -29,14 +29,14 @@ const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in
"," ","
); );
const WORD_REPLACEMENT = Object.assign(Object.create(null), { const WORD_REPLACEMENT = {
and: "&&", and: "&&",
or: "||", or: "||",
gt: ">", gt: ">",
gte: ">=", gte: ">=",
lt: "<", lt: "<",
lte: "<=", lte: "<=",
}); };
export interface QWebVar { export interface QWebVar {
id: string; // foo id: string; // foo
@@ -57,7 +57,6 @@ type TKind =
| "RIGHT_PAREN" | "RIGHT_PAREN"
| "COMMA" | "COMMA"
| "VALUE" | "VALUE"
| "TEMPLATE_STRING"
| "SYMBOL" | "SYMBOL"
| "OPERATOR" | "OPERATOR"
| "COLON"; | "COLON";
@@ -68,10 +67,9 @@ interface Token {
originalValue?: string; originalValue?: string;
size?: number; size?: number;
varName?: string; varName?: string;
replace?: Function;
} }
const STATIC_TOKEN_MAP: { [key: string]: TKind } = Object.assign(Object.create(null), { const STATIC_TOKEN_MAP: { [key: string]: TKind } = {
"{": "LEFT_BRACE", "{": "LEFT_BRACE",
"}": "RIGHT_BRACE", "}": "RIGHT_BRACE",
"[": "LEFT_BRACKET", "[": "LEFT_BRACKET",
@@ -80,7 +78,7 @@ const STATIC_TOKEN_MAP: { [key: string]: TKind } = Object.assign(Object.create(n
",": "COMMA", ",": "COMMA",
"(": "LEFT_PAREN", "(": "LEFT_PAREN",
")": "RIGHT_PAREN", ")": "RIGHT_PAREN",
}); };
// note that the space after typeof is relevant. It makes sure that the formatted // note that the space after typeof is relevant. It makes sure that the formatted
// expression has a space after typeof // expression has a space after typeof
@@ -91,7 +89,7 @@ type Tokenizer = (expr: string) => Token | false;
let tokenizeString: Tokenizer = function (expr) { let tokenizeString: Tokenizer = function (expr) {
let s = expr[0]; let s = expr[0];
let start = s; let start = s;
if (s !== "'" && s !== '"' && s !== "`") { if (s !== "'" && s !== '"') {
return false; return false;
} }
let i = 1; let i = 1;
@@ -113,17 +111,6 @@ let tokenizeString: Tokenizer = function (expr) {
throw new Error("Invalid expression"); throw new Error("Invalid expression");
} }
s += start; s += start;
if (start === "`") {
return {
type: "TEMPLATE_STRING",
value: s,
replace(replacer) {
return s.replace(/\$\{(.*?)\}/g, (match, group) => {
return "${" + replacer(group) + "}";
});
},
};
}
return { type: "VALUE", value: s }; return { type: "VALUE", value: s };
}; };
@@ -223,10 +210,6 @@ export function tokenize(expr: string): Token[] {
// Expression "evaluator" // Expression "evaluator"
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const isLeftSeparator = (token) => token && (token.type === "LEFT_BRACE" || token.type === "COMMA");
const isRightSeparator = (token) =>
token && (token.type === "RIGHT_BRACE" || token.type === "COMMA");
/** /**
* This is the main function exported by this file. This is the code that will * This is the main function exported by this file. This is the code that will
* process an expression (given as a string) and returns another expression with * process an expression (given as a string) and returns another expression with
@@ -255,39 +238,13 @@ const isRightSeparator = (token) =>
export function compileExprToArray(expr: string, scope: { [key: string]: QWebVar }): Token[] { export function compileExprToArray(expr: string, scope: { [key: string]: QWebVar }): Token[] {
scope = Object.create(scope); scope = Object.create(scope);
const tokens = tokenize(expr); const tokens = tokenize(expr);
for (let i = 0; i < tokens.length; i++) {
let i = 0;
let stack = []; // to track last opening [ or {
while (i < tokens.length) {
let token = tokens[i]; let token = tokens[i];
let prevToken = tokens[i - 1]; let prevToken = tokens[i - 1];
let nextToken = tokens[i + 1]; let nextToken = tokens[i + 1];
let groupType = stack[stack.length - 1];
switch (token.type) {
case "LEFT_BRACE":
case "LEFT_BRACKET":
stack.push(token.type);
break;
case "RIGHT_BRACE":
case "RIGHT_BRACKET":
stack.pop();
}
let isVar = token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value); let isVar = token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value);
if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) { if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) {
if (prevToken) { if (prevToken) {
// normalize missing tokens: {a} should be equivalent to {a:a}
if (
groupType === "LEFT_BRACE" &&
isLeftSeparator(prevToken) &&
isRightSeparator(nextToken)
) {
tokens.splice(i + 1, 0, { type: "COLON", value: ":" }, { ...token });
nextToken = tokens[i + 1];
}
if (prevToken.type === "OPERATOR" && prevToken.value === ".") { if (prevToken.type === "OPERATOR" && prevToken.value === ".") {
isVar = false; isVar = false;
} else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") { } else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") {
@@ -297,9 +254,6 @@ export function compileExprToArray(expr: string, scope: { [key: string]: QWebVar
} }
} }
} }
if (token.type === "TEMPLATE_STRING") {
token.value = token.replace((expr) => compileExpr(expr, scope));
}
if (nextToken && nextToken.type === "OPERATOR" && nextToken.value === "=>") { if (nextToken && nextToken.type === "OPERATOR" && nextToken.value === "=>") {
if (token.type === "RIGHT_PAREN") { if (token.type === "RIGHT_PAREN") {
let j = i - 1; let j = i - 1;
@@ -324,7 +278,6 @@ export function compileExprToArray(expr: string, scope: { [key: string]: QWebVar
token.value = `scope['${token.value}']`; token.value = `scope['${token.value}']`;
} }
} }
i++;
} }
return tokens; return tokens;
} }
+2 -12
View File
@@ -1,8 +1,6 @@
import { STATUS } from "../component/component";
import { VNode } from "../vdom/index"; import { VNode } from "../vdom/index";
import { INTERP_REGEXP } from "./compilation_context"; import { INTERP_REGEXP } from "./compilation_context";
import { QWeb } from "./qweb"; import { QWeb } from "./qweb";
import { browser } from "../browser";
/** /**
* Owl QWeb Extensions * Owl QWeb Extensions
@@ -76,10 +74,9 @@ export function makeHandlerCode(
// we need to capture every variable in it // we need to capture every variable in it
putInCache = false; putInCache = false;
code = ctx.captureExpression(value); code = ctx.captureExpression(value);
code = `const res = (() => { return ${code} })(); if (typeof res === 'function') { res(e) }`;
} }
const modCode = mods.map((mod) => modcodes[mod]).join(""); const modCode = mods.map((mod) => modcodes[mod]).join("");
let handler = `function (e) {if (context.__owl__.status === ${STATUS.DESTROYED}){return}${modCode}${code}}`; let handler = `function (e) {if (!context.__owl__.isMounted){return}${modCode}${code}}`;
if (putInCache) { if (putInCache) {
const key = ctx.generateTemplateKey(event); const key = ctx.generateTemplateKey(event);
ctx.addLine(`extra.handlers[${key}] = extra.handlers[${key}] || ${handler};`); ctx.addLine(`extra.handlers[${key}] = extra.handlers[${key}] || ${handler};`);
@@ -199,14 +196,7 @@ function whenTransitionEnd(elm: HTMLElement, cb) {
const durations: Array<string> = (styles.transitionDuration || "").split(", "); const durations: Array<string> = (styles.transitionDuration || "").split(", ");
const timeout: number = getTimeout(delays, durations); const timeout: number = getTimeout(delays, durations);
if (timeout > 0) { if (timeout > 0) {
const transitionEndCB = () => { elm.addEventListener("transitionend", cb, { once: true });
if (!elm.parentNode) return;
cb();
browser.clearTimeout(fallbackTimeout);
elm.removeEventListener("transitionend", transitionEndCB);
};
elm.addEventListener("transitionend", transitionEndCB, { once: true });
const fallbackTimeout = browser.setTimeout(transitionEndCB, timeout + 1);
} else { } else {
cb(); cb();
} }
+20 -90
View File
@@ -66,11 +66,10 @@ interface QWebConfig {
// Const/global stuff/helpers // Const/global stuff/helpers
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
export const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"]; const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
const lineBreakRE = /[\r\n]/; const lineBreakRE = /[\r\n]/;
const whitespaceRE = /\s+/g; const whitespaceRE = /\s+/g;
const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
const NODE_HOOKS_PARAMS = { const NODE_HOOKS_PARAMS = {
create: "(_, n)", create: "(_, n)",
@@ -80,7 +79,7 @@ const NODE_HOOKS_PARAMS = {
}; };
interface Utils { interface Utils {
toClassObj(expr: any): Object; toObj(expr: any): Object;
shallowEqual(p1: Object, p2: Object): boolean; shallowEqual(p1: Object, p2: Object): boolean;
[key: string]: any; [key: string]: any;
} }
@@ -111,53 +110,20 @@ function vDomToString(vdom: VNode[]): string {
const UTILS: Utils = { const UTILS: Utils = {
zero: Symbol("zero"), zero: Symbol("zero"),
toClassObj(expr) { toObj(expr) {
const result = {};
if (typeof expr === "string") { if (typeof expr === "string") {
// we transform here a list of classes into an object:
// 'hey you' becomes {hey: true, you: true}
expr = expr.trim(); expr = expr.trim();
if (!expr) { if (!expr) {
return {}; return {};
} }
let words = expr.split(/\s+/); let words = expr.split(/\s+/);
let result = {};
for (let i = 0; i < words.length; i++) { for (let i = 0; i < words.length; i++) {
result[words[i]] = true; result[words[i]] = true;
} }
return result; return result;
} }
// this is already an object, but we may need to split keys: return expr;
// {'a b': true, 'a c': false} should become {a: true, b: true, c: false}
for (let key in expr) {
const value = expr[key];
const words = key.split(/\s+/);
for (let word of words) {
result[word] = result[word] || value;
}
}
return result;
},
/**
* This method combines the current context with the variables defined in a
* scope for use in a slot.
*
* The implementation is kind of tricky because we want to preserve the
* prototype chain structure of the cloned result. So we need to traverse the
* prototype chain, cloning each level respectively.
*/
combine(context, scope) {
let clone = context;
const scopeStack = [];
while (!isComponent(scope)) {
scopeStack.push(scope);
scope = scope.__proto__;
}
while (scopeStack.length) {
let scope = scopeStack.pop();
clone = Object.create(clone);
Object.assign(clone, scope);
}
return clone;
}, },
shallowEqual, shallowEqual,
addNameSpace(vnode) { addNameSpace(vnode) {
@@ -236,7 +202,6 @@ export class QWeb extends EventBus {
att: 1, att: 1,
attf: 1, attf: 1,
translation: 1, translation: 1,
tag: 1,
}; };
static DIRECTIVES: Directive[] = []; static DIRECTIVES: Directive[] = [];
@@ -329,9 +294,6 @@ export class QWeb extends EventBus {
* template, with the name given by the t-name attribute. * template, with the name given by the t-name attribute.
*/ */
addTemplates(xmlstr: string | Document) { addTemplates(xmlstr: string | Document) {
if (!xmlstr) {
return;
}
const doc = typeof xmlstr === "string" ? parseXML(xmlstr) : xmlstr; const doc = typeof xmlstr === "string" ? parseXML(xmlstr) : xmlstr;
const templates = doc.getElementsByTagName("templates")[0]; const templates = doc.getElementsByTagName("templates")[0];
if (!templates) { if (!templates) {
@@ -472,7 +434,6 @@ export class QWeb extends EventBus {
ctx.variables = Object.create(null); ctx.variables = Object.create(null);
ctx.parentNode = ctx.generateID(); ctx.parentNode = ctx.generateID();
ctx.allowMultipleRoots = true; ctx.allowMultipleRoots = true;
ctx.shouldDefineParent = true;
ctx.hasParentWidget = true; ctx.hasParentWidget = true;
ctx.shouldDefineResult = false; ctx.shouldDefineResult = false;
ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`); ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`);
@@ -535,8 +496,7 @@ export class QWeb extends EventBus {
} }
if (this.translateFn) { if (this.translateFn) {
if ((node.parentNode as any).getAttribute("t-translation") !== "off") { if ((node.parentNode as any).getAttribute("t-translation") !== "off") {
const match = translationRE.exec(text); text = this.translateFn(text);
text = match[1] + this.translateFn(match[2]) + match[3];
} }
} }
if (ctx.parentNode) { if (ctx.parentNode) {
@@ -560,11 +520,7 @@ export class QWeb extends EventBus {
} }
if (node.tagName !== "t" && node.hasAttribute("t-call")) { if (node.tagName !== "t" && node.hasAttribute("t-call")) {
const tCallNode = document.implementation.createDocument( const tCallNode = document.createElement("t");
"http://www.w3.org/1999/xhtml",
"t",
null
).documentElement;
tCallNode.setAttribute("t-call", node.getAttribute("t-call")!); tCallNode.setAttribute("t-call", node.getAttribute("t-call")!);
node.removeAttribute("t-call"); node.removeAttribute("t-call");
node.prepend(tCallNode); node.prepend(tCallNode);
@@ -600,11 +556,7 @@ export class QWeb extends EventBus {
throw new Error(`Unknown QWeb directive: '${attrName}'`); throw new Error(`Unknown QWeb directive: '${attrName}'`);
} }
if (node.tagName !== "t" && (attrName === "t-esc" || attrName === "t-raw")) { if (node.tagName !== "t" && (attrName === "t-esc" || attrName === "t-raw")) {
const tNode = document.implementation.createDocument( const tNode = document.createElement("t");
"http://www.w3.org/1999/xhtml",
"t",
null
).documentElement;
tNode.setAttribute(attrName, node.getAttribute(attrName)!); tNode.setAttribute(attrName, node.getAttribute(attrName)!);
for (let child of Array.from(node.childNodes)) { for (let child of Array.from(node.childNodes)) {
tNode.appendChild(child); tNode.appendChild(child);
@@ -660,22 +612,14 @@ export class QWeb extends EventBus {
} }
} }
if (node.nodeName !== "t" || node.hasAttribute("t-tag")) { if (node.nodeName !== "t") {
let nodeID = this._compileGenericNode(node, ctx, withHandlers);
ctx = ctx.withParent(nodeID);
let nodeHooks = {}; let nodeHooks = {};
let addNodeHook = function (hook, handler) { let addNodeHook = function (hook, handler) {
nodeHooks[hook] = nodeHooks[hook] || []; nodeHooks[hook] = nodeHooks[hook] || [];
nodeHooks[hook].push(handler); nodeHooks[hook].push(handler);
}; };
if (node.tagName === "select" && node.hasAttribute("t-att-value")) {
const value = node.getAttribute("t-att-value");
let exprId = ctx.generateID();
ctx.addLine(`let expr${exprId} = ${ctx.formatExpression(value)};`);
let expr = `expr${exprId}`;
node.setAttribute("t-att-value", expr);
addNodeHook("create", `n.elm.value=${expr};`);
}
let nodeID = this._compileGenericNode(node, ctx, withHandlers);
ctx = ctx.withParent(nodeID);
for (let { directive, value, fullName } of validDirectives) { for (let { directive, value, fullName } of validDirectives) {
if (directive.atNodeCreation) { if (directive.atNodeCreation) {
@@ -757,18 +701,16 @@ export class QWeb extends EventBus {
isProp = key === "selected" || key === "disabled"; isProp = key === "selected" || key === "disabled";
break; break;
case "textarea": case "textarea":
isProp = key === "readonly" || key === "disabled" || key === "value"; isProp = key === "readonly" || key === "disabled";
break;
case "select":
isProp = key === "disabled" || key === "value";
break; break;
case "button": case "button":
case "select":
case "optgroup": case "optgroup":
isProp = key === "disabled"; isProp = key === "disabled";
break; break;
} }
if (isProp) { if (isProp) {
props.push(`${key}: ${val}`); props.push(`${key}: _${val}`);
} }
} }
let classObj = ""; let classObj = "";
@@ -804,7 +746,7 @@ export class QWeb extends EventBus {
name = '"' + name + '"'; name = '"' + name + '"';
} }
attrs.push(`${name}: _${attID}`); attrs.push(`${name}: _${attID}`);
handleProperties(name, `_${attID}`); handleProperties(name, attID);
} }
} }
@@ -816,7 +758,7 @@ export class QWeb extends EventBus {
if (attName === "class") { if (attName === "class") {
ctx.rootContext.shouldDefineUtils = true; ctx.rootContext.shouldDefineUtils = true;
formattedValue = `utils.toClassObj(${formattedValue})`; formattedValue = `utils.toObj(${formattedValue})`;
if (classObj) { if (classObj) {
ctx.addLine(`Object.assign(${classObj}, ${formattedValue})`); ctx.addLine(`Object.assign(${classObj}, ${formattedValue})`);
} else { } else {
@@ -839,14 +781,9 @@ export class QWeb extends EventBus {
const attrIndex = attrs.findIndex((att) => att.startsWith(attName + ":")); const attrIndex = attrs.findIndex((att) => att.startsWith(attName + ":"));
attrs.splice(attrIndex, 1); attrs.splice(attrIndex, 1);
} }
if (node.nodeName === "select" && attName === "value") { ctx.addLine(`let _${attID} = ${formattedValue};`);
attrs.push(`${attName}: ${v}`); attrs.push(`${attName}: _${attID}`);
handleProperties(attName, v); handleProperties(attName, attID);
} else {
ctx.addLine(`let _${attID} = ${formattedValue};`);
attrs.push(`${attName}: _${attID}`);
handleProperties(attName, "_" + attID);
}
} }
} }
@@ -902,14 +839,7 @@ export class QWeb extends EventBus {
ctx.addLine(`}`); ctx.addLine(`}`);
ctx.closeIf(); ctx.closeIf();
} }
let nodeName = `'${node.nodeName}'`; ctx.addLine(`let vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`);
if ((<Element>node).hasAttribute("t-tag")) {
const tagExpr = (<Element>node).getAttribute("t-tag");
(<Element>node).removeAttribute("t-tag");
nodeName = `tag${ctx.generateID()}`;
ctx.addLine(`let ${nodeName} = ${ctx.formatExpression(tagExpr)};`);
}
ctx.addLine(`let vn${nodeID} = h(${nodeName}, p${nodeID}, c${nodeID});`);
if (ctx.parentNode) { if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`); ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`);
} else if (ctx.loopNumber || ctx.hasKey0) { } else if (ctx.loopNumber || ctx.hasKey0) {
+32 -43
View File
@@ -11,7 +11,6 @@ type NavigationGuard = (info: {
export interface Route { export interface Route {
name: string; name: string;
path: string; path: string;
extractionRegExp: RegExp;
component?: any; component?: any;
redirect?: Destination; redirect?: Destination;
params: string[]; params: string[];
@@ -55,7 +54,6 @@ export interface EnvWithRouter extends Env {
} }
const paramRegexp = /\{\{(.*?)\}\}/; const paramRegexp = /\{\{(.*?)\}\}/;
const globalParamRegexp = new RegExp(paramRegexp.source, "g");
export class Router { export class Router {
currentRoute: Route | null = null; currentRoute: Route | null = null;
@@ -89,7 +87,6 @@ export class Router {
this.validateDestination(partialRoute.redirect); this.validateDestination(partialRoute.redirect);
} }
partialRoute.params = partialRoute.path ? findParams(partialRoute.path) : []; partialRoute.params = partialRoute.path ? findParams(partialRoute.path) : [];
partialRoute.extractionRegExp = makeExtractionRegExp(partialRoute.path);
this.routes[partialRoute.name] = partialRoute as Route; this.routes[partialRoute.name] = partialRoute as Route;
this.routeIds.push(partialRoute.name); this.routeIds.push(partialRoute.name);
} }
@@ -125,10 +122,7 @@ export class Router {
const initialParams = this.currentParams; const initialParams = this.currentParams;
const result = await this.matchAndApplyRules(path); const result = await this.matchAndApplyRules(path);
if (result.type === "match") { if (result.type === "match") {
let finalPath = this.routeToPath(result.route, result.params); const finalPath = this.routeToPath(result.route, result.params);
if (path.indexOf("?") > -1) {
finalPath += "?" + path.split("?")[1];
}
const isPopStateEvent = ev && ev instanceof PopStateEvent; const isPopStateEvent = ev && ev instanceof PopStateEvent;
if (!isPopStateEvent) { if (!isPopStateEvent) {
this.setUrlFromPath(finalPath); this.setUrlFromPath(finalPath);
@@ -176,14 +170,19 @@ export class Router {
} }
private routeToPath(route: Route, params: RouteParams): string { private routeToPath(route: Route, params: RouteParams): string {
const path = route.path;
const parts = path.split("/");
const l = parts.length;
for (let i = 0; i < l; i++) {
const part = parts[i];
const match = part.match(paramRegexp);
if (match) {
const key = match[1].split(".")[0];
parts[i] = <string>params[key];
}
}
const prefix = this.mode === "hash" ? "#" : ""; const prefix = this.mode === "hash" ? "#" : "";
return ( return prefix + parts.join("/");
prefix +
route.path.replace(globalParamRegexp, (match, param) => {
const [key] = param.split(".");
return <string>params[key];
})
);
} }
private currentPath(): string { private currentPath(): string {
@@ -242,53 +241,43 @@ export class Router {
if (route.path === "*") { if (route.path === "*") {
return {}; return {};
} }
if (path.indexOf("?") > -1) {
path = path.split("?")[0];
}
if (path.startsWith("#")) { if (path.startsWith("#")) {
path = path.slice(1); path = path.slice(1);
} }
const paramsMatch = path.match(route.extractionRegExp); const descrParts = route.path.split("/");
if (!paramsMatch) { const targetParts = path.split("/");
const l = descrParts.length;
if (l !== targetParts.length) {
return false; return false;
} }
const result = {}; const result = {};
route.params.forEach((param, index) => { for (let i = 0; i < l; i++) {
const [key, suffix] = param.split("."); const descr = descrParts[i];
const paramValue = paramsMatch[index + 1]; let target: string | number = targetParts[i];
if (suffix === "number") { const match = descr.match(paramRegexp);
return (result[key] = parseInt(paramValue, 10)); if (match) {
const [key, suffix] = match[1].split(".");
if (suffix === "number") {
target = parseInt(target, 10);
}
result[key] = target;
} else if (descr !== target) {
return false;
} }
return (result[key] = paramValue); }
});
return result; return result;
} }
} }
function findParams(str: string): string[] { function findParams(str: string): string[] {
const globalParamRegexp = /\{\{(.*?)\}\}/g;
const result: string[] = []; const result: string[] = [];
let m; let m;
do { do {
m = globalParamRegexp.exec(str); m = globalParamRegexp.exec(str);
if (m) { if (m) {
result.push(m[1]); result.push(m[1].split(".")[0]);
} }
} while (m); } while (m);
return result; return result;
} }
function escapeRegExp(str: string) {
return str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
}
function makeExtractionRegExp(path: string) {
// replace param strings with capture groups so that we can build a regex to match over the path
const extractionString = path
.split(paramRegexp)
.map((part, index) => {
return index % 2 ? "(.*)" : escapeRegExp(part);
})
.join("");
// Example: /home/{{param1}}/{{param2}} => ^\/home\/(.*)\/(.*)$
return new RegExp(`^${extractionString}$`);
}
+5 -15
View File
@@ -1,4 +1,5 @@
import { Component, Env } from "./component/component"; import { Component } from "./component/component";
import { Env } from "./component/component";
import { Context, useContextWithCB } from "./context"; import { Context, useContextWithCB } from "./context";
import { onWillUpdateProps } from "./hooks"; import { onWillUpdateProps } from "./hooks";
@@ -75,11 +76,6 @@ export class Store extends Context {
); );
return result; return result;
} }
__notifyComponents(): Promise<void> {
this.trigger("before-update");
return super.__notifyComponents();
}
} }
interface SelectorOptions { interface SelectorOptions {
@@ -110,16 +106,13 @@ export function useStore(selector, options: SelectorOptions = {}): any {
const newRevNumber = hashFn(result); const newRevNumber = hashFn(result);
if ((newRevNumber > 0 && revNumber !== newRevNumber) || !isEqual(oldResult, result)) { if ((newRevNumber > 0 && revNumber !== newRevNumber) || !isEqual(oldResult, result)) {
revNumber = newRevNumber; revNumber = newRevNumber;
if (options.onUpdate) {
options.onUpdate(result);
}
return true; return true;
} }
return false; return false;
} }
if (options.onUpdate) {
store.on("before-update", component, () => {
const newValue = selector(store!.state, component.props!);
options.onUpdate(newValue);
});
}
store.updateFunctions[componentId].push(function (): boolean { store.updateFunctions[componentId].push(function (): boolean {
return selectCompareUpdate(store!.state, component.props); return selectCompareUpdate(store!.state, component.props);
}); });
@@ -140,9 +133,6 @@ export function useStore(selector, options: SelectorOptions = {}): any {
const __destroy = component.__destroy; const __destroy = component.__destroy;
component.__destroy = (parent) => { component.__destroy = (parent) => {
delete store.updateFunctions[componentId]; delete store.updateFunctions[componentId];
if (options.onUpdate) {
store.off("before-update", component);
}
__destroy.call(component, parent); __destroy.call(component, parent);
}; };
+1 -2
View File
@@ -231,8 +231,7 @@ function updateClass(oldVnode: VNode, vnode: VNode): void {
elm = vnode.elm as Element; elm = vnode.elm as Element;
for (name in oldClass) { for (name in oldClass) {
if (name && !klass[name] && !Object.prototype.hasOwnProperty.call(klass, name)) { if (name && !klass[name]) {
// was `true` and now not provided
elm.classList.remove(name); elm.classList.remove(name);
} }
} }
+3 -3
View File
@@ -24,7 +24,7 @@ exports[`animations t-transition combined with component 1`] = `
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey2 = \`Child\`; let componentKey2 = \`Child\`;
let W2 = scope['Child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2]; let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')} if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2); w2 = new W2(parent, props2);
const __patch2 = w2.__patch; const __patch2 = w2.__patch;
@@ -69,7 +69,7 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey2 = \`Child\`; let componentKey2 = \`Child\`;
let W2 = scope['Child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2]; let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')} if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2); w2 = new W2(parent, props2);
const __patch2 = w2.__patch; const __patch2 = w2.__patch;
@@ -115,7 +115,7 @@ exports[`animations t-transition combined with t-component, remove and re-add be
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey2 = \`Child\`; let componentKey2 = \`Child\`;
let W2 = scope['Child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2]; let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')} if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2); w2 = new W2(parent, props2);
const __patch2 = w2.__patch; const __patch2 = w2.__patch;
+5 -5
View File
@@ -420,29 +420,29 @@ describe("animations", () => {
widget.state.flag = true; widget.state.flag = true;
await nextFrame(3); await nextFrame();
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend"));
expect(fixture.innerHTML).toBe('<div><span class="">blue</span></div>'); expect(fixture.innerHTML).toBe('<div><span class="">blue</span></div>');
expect(QWeb.utils.transitionInsert).toBeCalledTimes(1); expect(QWeb.utils.transitionInsert).toBeCalledTimes(1);
widget.state.flag = false; widget.state.flag = false;
await nextFrame(3); await nextFrame();
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe(
'<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="__3__">blue</span></div>' '<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="__3__">blue</span></div>'
); );
expect(QWeb.utils.transitionInsert).toBeCalledTimes(1); expect(QWeb.utils.transitionInsert).toBeCalledTimes(1);
widget.state.flag = true; widget.state.flag = true;
await nextFrame(3); await nextFrame();
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe(
'<div><span class="chimay-enter-active chimay-enter-to" data-owl-key="__3__">blue</span></div>' '<div><span class="chimay-enter-active chimay-enter-to" data-owl-key="__3__">blue</span></div>'
); );
expect(QWeb.utils.transitionInsert).toBeCalledTimes(2); expect(QWeb.utils.transitionInsert).toBeCalledTimes(2);
widget.state.flag = false; widget.state.flag = false;
await nextFrame(3); await nextFrame();
widget.state.flag = true; widget.state.flag = true;
await nextFrame(3); await nextFrame();
expect(QWeb.utils.transitionInsert).toBeCalledTimes(3); expect(QWeb.utils.transitionInsert).toBeCalledTimes(3);
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend"));
@@ -20,12 +20,12 @@ exports[`class and style attributes with t-component dynamic t-att-style is prop
w2 = false; w2 = false;
} }
if (w2) { if (w2) {
w2.__updateProps(props2, extra.fiber, undefined).then(()=>{if (w2.__owl__.status === 5) {return};w2.el.style=_4;});; w2.__updateProps(props2, extra.fiber, undefined).then(()=>{if (w2.__owl__.isDestroyed) {return};w2.el.style=_4;});;
let pvnode = w2.__owl__.pvnode; let pvnode = w2.__owl__.pvnode;
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey2 = \`child\`; let componentKey2 = \`child\`;
let W2 = scope['child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2]; let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')} if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2); w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id; parent.__owl__.cmap['__3__'] = w2.__owl__.id;
@@ -67,7 +67,7 @@ exports[`class and style attributes with t-component t-att-class is properly add
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey2 = \`Child\`; let componentKey2 = \`Child\`;
let W2 = scope['Child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2]; let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')} if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2); w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id; parent.__owl__.cmap['__3__'] = w2.__owl__.id;
@@ -90,7 +90,7 @@ exports[`class and style attributes with t-component t-att-class is properly add
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let _7 = {'c':true}; let _7 = {'c':true};
Object.assign(_7, utils.toClassObj({d:scope['state'].d})) Object.assign(_7, utils.toObj({d:scope['state'].d}))
let c8 = [], p8 = {key:8,class:_7}; let c8 = [], p8 = {key:8,class:_7};
let vn8 = h('span', p8, c8); let vn8 = h('span', p8, c8);
return vn8; return vn8;
@@ -112,7 +112,7 @@ exports[`class and style attributes with t-component t-att-class is properly add
// Component 'Child' // Component 'Child'
const ref4 = \`child\`; const ref4 = \`child\`;
let _5 = {'a':true}; let _5 = {'a':true};
Object.assign(_5, utils.toClassObj(scope['state'].b?'b':'')) Object.assign(_5, utils.toObj(scope['state'].b?'b':''))
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false; let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = {}; let props2 = {};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) { if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
@@ -125,7 +125,7 @@ exports[`class and style attributes with t-component t-att-class is properly add
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey2 = \`Child\`; let componentKey2 = \`Child\`;
let W2 = scope['Child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2]; let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')} if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2); w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id; parent.__owl__.cmap['__3__'] = w2.__owl__.id;
@@ -148,7 +148,7 @@ exports[`class and style attributes with t-component t-att-class is properly add
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let _7 = {'c':true}; let _7 = {'c':true};
Object.assign(_7, utils.toClassObj(scope['state'].d?'d':'')) Object.assign(_7, utils.toObj(scope['state'].d?'d':''))
let c8 = [], p8 = {key:8,class:_7}; let c8 = [], p8 = {key:8,class:_7};
let vn8 = h('span', p8, c8); let vn8 = h('span', p8, c8);
return vn8; return vn8;
@@ -24,7 +24,7 @@ exports[`basic widget properties can handle empty props 1`] = `
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey2 = \`Child\`; let componentKey2 = \`Child\`;
let W2 = scope['Child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2]; let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')} if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2); w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id; parent.__owl__.cmap['__3__'] = w2.__owl__.id;
@@ -97,7 +97,7 @@ exports[`basic widget properties reconciliation alg works for t-foreach in t-for
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey10 = \`Child\`; let componentKey10 = \`Child\`;
let W10 = scope['Child'] || context.constructor.components[componentKey10] || QWeb.components[componentKey10]; let W10 = context.constructor.components[componentKey10] || QWeb.components[componentKey10]|| scope['Child'];
if (!W10) {throw new Error('Cannot find the definition of component \\"' + componentKey10 + '\\"')} if (!W10) {throw new Error('Cannot find the definition of component \\"' + componentKey10 + '\\"')}
w10 = new W10(parent, props10); w10 = new W10(parent, props10);
parent.__owl__.cmap[k11] = w10.__owl__.id; parent.__owl__.cmap[k11] = w10.__owl__.id;
@@ -145,7 +145,7 @@ exports[`basic widget properties same t-keys in two different places 1`] = `
c2.push(pvnode); c2.push(pvnode);
} else { } else {
let componentKey3 = \`Child\`; let componentKey3 = \`Child\`;
let W3 = scope['Child'] || context.constructor.components[componentKey3] || QWeb.components[componentKey3]; let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| scope['Child'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')} if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3); w3 = new W3(parent, props3);
parent.__owl__.cmap[k4] = w3.__owl__.id; parent.__owl__.cmap[k4] = w3.__owl__.id;
@@ -175,7 +175,7 @@ exports[`basic widget properties same t-keys in two different places 1`] = `
c5.push(pvnode); c5.push(pvnode);
} else { } else {
let componentKey6 = \`Child\`; let componentKey6 = \`Child\`;
let W6 = scope['Child'] || context.constructor.components[componentKey6] || QWeb.components[componentKey6]; let W6 = context.constructor.components[componentKey6] || QWeb.components[componentKey6]|| scope['Child'];
if (!W6) {throw new Error('Cannot find the definition of component \\"' + componentKey6 + '\\"')} if (!W6) {throw new Error('Cannot find the definition of component \\"' + componentKey6 + '\\"')}
w6 = new W6(parent, props6); w6 = new W6(parent, props6);
parent.__owl__.cmap[k7] = w6.__owl__.id; parent.__owl__.cmap[k7] = w6.__owl__.id;
@@ -218,7 +218,7 @@ exports[`basic widget properties t-key on a component with t-if, and a sibling c
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey2 = \`Child\`; let componentKey2 = \`Child\`;
let W2 = scope['Child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2]; let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')} if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2); w2 = new W2(parent, props2);
parent.__owl__.cmap[k3] = w2.__owl__.id; parent.__owl__.cmap[k3] = w2.__owl__.id;
@@ -243,7 +243,7 @@ exports[`basic widget properties t-key on a component with t-if, and a sibling c
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey4 = \`Child\`; let componentKey4 = \`Child\`;
let W4 = scope['Child'] || context.constructor.components[componentKey4] || QWeb.components[componentKey4]; let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4]|| scope['Child'];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')} if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(parent, props4); w4 = new W4(parent, props4);
parent.__owl__.cmap['__5__'] = w4.__owl__.id; parent.__owl__.cmap['__5__'] = w4.__owl__.id;
@@ -257,45 +257,6 @@ exports[`basic widget properties t-key on a component with t-if, and a sibling c
}" }"
`; `;
exports[`composition can switch between dynamic components without the need for a t-key 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__3\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
// Component '{{state.child}}'
let componentKey2 = (scope['state'].child);
let k3 = '___' + componentKey2
let w2 = k3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k3]] : false;
let props2 = {};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, undefined);
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let W2 = false || context.constructor.components[componentKey2] || QWeb.components[componentKey2];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap[k3] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k3, hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
}
w2.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
exports[`composition sub components with some state rendered in a loop 1`] = ` exports[`composition sub components with some state rendered in a loop 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
@@ -340,7 +301,7 @@ exports[`composition sub components with some state rendered in a loop 1`] = `
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey6 = \`ChildWidget\`; let componentKey6 = \`ChildWidget\`;
let W6 = scope['ChildWidget'] || context.constructor.components[componentKey6] || QWeb.components[componentKey6]; let W6 = context.constructor.components[componentKey6] || QWeb.components[componentKey6]|| scope['ChildWidget'];
if (!W6) {throw new Error('Cannot find the definition of component \\"' + componentKey6 + '\\"')} if (!W6) {throw new Error('Cannot find the definition of component \\"' + componentKey6 + '\\"')}
w6 = new W6(parent, props6); w6 = new W6(parent, props6);
parent.__owl__.cmap[k7] = w6.__owl__.id; parent.__owl__.cmap[k7] = w6.__owl__.id;
@@ -369,9 +330,7 @@ exports[`composition t-component with dynamic value 1`] = `
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1); let vn1 = h('div', p1, c1);
// Component '{{state.widget}}' // Component '{{state.widget}}'
let componentKey2 = (scope['state'].widget); let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let k3 = '___' + componentKey2
let w2 = k3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k3]] : false;
let props2 = {}; let props2 = {};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) { if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy(); w2.destroy();
@@ -382,12 +341,13 @@ exports[`composition t-component with dynamic value 1`] = `
let pvnode = w2.__owl__.pvnode; let pvnode = w2.__owl__.pvnode;
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let W2 = false || context.constructor.components[componentKey2] || QWeb.components[componentKey2]; let componentKey2 = (scope['state'].widget);
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')} if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2); w2 = new W2(parent, props2);
parent.__owl__.cmap[k3] = w2.__owl__.id; parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k3, hook: {remove() {},destroy(vn) {w2.destroy();}}}); let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode); c1.push(pvnode);
w2.__owl__.pvnode = pvnode; w2.__owl__.pvnode = pvnode;
} }
@@ -408,9 +368,7 @@ exports[`composition t-component with dynamic value 2 1`] = `
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1); let vn1 = h('div', p1, c1);
// Component 'Widget{{state.widget}}' // Component 'Widget{{state.widget}}'
let componentKey2 = \`Widget\${scope['state'].widget}\`; let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let k3 = '___' + componentKey2
let w2 = k3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k3]] : false;
let props2 = {}; let props2 = {};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) { if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy(); w2.destroy();
@@ -421,12 +379,13 @@ exports[`composition t-component with dynamic value 2 1`] = `
let pvnode = w2.__owl__.pvnode; let pvnode = w2.__owl__.pvnode;
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let W2 = false || context.constructor.components[componentKey2] || QWeb.components[componentKey2]; let componentKey2 = \`Widget\${scope['state'].widget}\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')} if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2); w2 = new W2(parent, props2);
parent.__owl__.cmap[k3] = w2.__owl__.id; parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k3, hook: {remove() {},destroy(vn) {w2.destroy();}}}); let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode); c1.push(pvnode);
w2.__owl__.pvnode = pvnode; w2.__owl__.pvnode = pvnode;
} }
@@ -481,11 +440,11 @@ exports[`composition t-ref on a node, and t-on-click 2`] = `
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey2 = \`Child\`; let componentKey2 = \`Child\`;
let W2 = scope['Child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2]; let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')} if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2); w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id; parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('click', function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['doSomething'](e);});}});}); let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('click', function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['doSomething'](e);});}});});
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}}); let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode); c1.push(pvnode);
w2.__owl__.pvnode = pvnode; w2.__owl__.pvnode = pvnode;
@@ -508,7 +467,7 @@ exports[`dynamic t-props basic use 1`] = `
let vn1 = h('div', p1, c1); let vn1 = h('div', p1, c1);
// Component 'Child' // Component 'Child'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false; let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = Object.assign({}, scope['some'].obj, {}); let props2 = Object.assign({}, scope['some'].obj);
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) { if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy(); w2.destroy();
w2 = false; w2 = false;
@@ -519,7 +478,7 @@ exports[`dynamic t-props basic use 1`] = `
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey2 = \`Child\`; let componentKey2 = \`Child\`;
let W2 = scope['Child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2]; let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')} if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2); w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id; parent.__owl__.cmap['__3__'] = w2.__owl__.id;
@@ -559,17 +518,17 @@ exports[`other directives with t-component slot setted value (with t-set) not ac
w3 = false; w3 = false;
} }
if (w3) { if (w3) {
w3.__updateProps(props3, extra.fiber, utils.combine(context, scope)); w3.__updateProps(props3, extra.fiber, Object.assign(Object.create(context), scope));
let pvnode = w3.__owl__.pvnode; let pvnode = w3.__owl__.pvnode;
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey3 = \`ChildWidget\`; let componentKey3 = \`ChildWidget\`;
let W3 = scope['ChildWidget'] || context.constructor.components[componentKey3] || QWeb.components[componentKey3]; let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| scope['ChildWidget'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')} if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3); w3 = new W3(parent, props3);
parent.__owl__.cmap['__4__'] = w3.__owl__.id; parent.__owl__.cmap['__4__'] = w3.__owl__.id;
w3.__owl__.slotId = 1; w3.__owl__.slotId = 1;
let fiber = w3.__prepare(extra.fiber, utils.combine(context, scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); let fiber = w3.__prepare(extra.fiber, Object.assign(Object.create(context), scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__4__', hook: {remove() {},destroy(vn) {w3.destroy();}}}); let pvnode = h('dummy', {key: '__4__', hook: {remove() {},destroy(vn) {w3.destroy();}}});
c1.push(pvnode); c1.push(pvnode);
w3.__owl__.pvnode = pvnode; w3.__owl__.pvnode = pvnode;
@@ -620,7 +579,7 @@ exports[`other directives with t-component t-on expression captured in t-foreach
c6.push(vn7); c6.push(vn7);
const otherState_8 = scope['otherState']; const otherState_8 = scope['otherState'];
const iter_8 = scope.iter; const iter_8 = scope.iter;
p7.on['click'] = function (e) {if (context.__owl__.status === 5){return}const res = (() => { return otherState_8.vals.push(iter_8+'_'+iter_8) })(); if (typeof res === 'function') { res(e) }}; p7.on['click'] = function (e) {if (!context.__owl__.isMounted){return}otherState_8.vals.push(iter_8+'_'+iter_8)};
c7.push({text: \`expr\`}); c7.push({text: \`expr\`});
utils.getScope(scope, 'iter').iter = scope.iter+1; utils.getScope(scope, 'iter').iter = scope.iter+1;
} }
@@ -671,7 +630,7 @@ exports[`other directives with t-component t-on expression in t-foreach 1`] = `
c6.push(vn9); c6.push(vn9);
const otherState_10 = scope['otherState']; const otherState_10 = scope['otherState'];
const val_10 = scope['val']; const val_10 = scope['val'];
p9.on['click'] = function (e) {if (context.__owl__.status === 5){return}const res = (() => { return otherState_10.vals.push(val_10) })(); if (typeof res === 'function') { res(e) }}; p9.on['click'] = function (e) {if (!context.__owl__.isMounted){return}otherState_10.vals.push(val_10)};
c9.push({text: \`Expr\`}); c9.push({text: \`Expr\`});
} }
scope = _origScope5; scope = _origScope5;
@@ -725,7 +684,7 @@ exports[`other directives with t-component t-on expression in t-foreach with t-s
const otherState_10 = scope['otherState']; const otherState_10 = scope['otherState'];
const val_10 = scope['val']; const val_10 = scope['val'];
const bossa_10 = scope.bossa; const bossa_10 = scope.bossa;
p9.on['click'] = function (e) {if (context.__owl__.status === 5){return}const res = (() => { return otherState_10.vals.push(val_10+'_'+bossa_10) })(); if (typeof res === 'function') { res(e) }}; p9.on['click'] = function (e) {if (!context.__owl__.isMounted){return}otherState_10.vals.push(val_10+'_'+bossa_10)};
c9.push({text: \`Expr\`}); c9.push({text: \`Expr\`});
} }
scope = _origScope5; scope = _origScope5;
@@ -775,7 +734,7 @@ exports[`other directives with t-component t-on method call in t-foreach 1`] = `
let vn9 = h('button', p9, c9); let vn9 = h('button', p9, c9);
c6.push(vn9); c6.push(vn9);
let args10 = [scope['val']]; let args10 = [scope['val']];
p9.on['click'] = function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['addVal'](...args10, e);}; p9.on['click'] = function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['addVal'](...args10, e);};
c9.push({text: \`meth call\`}); c9.push({text: \`meth call\`});
} }
scope = _origScope5; scope = _origScope5;
@@ -807,11 +766,11 @@ exports[`other directives with t-component t-on with .capture modifier 1`] = `
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey2 = \`Child\`; let componentKey2 = \`Child\`;
let W2 = scope['Child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2]; let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')} if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2); w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id; parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('click', function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['capture'](e);}, true);}});}); let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('click', function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['capture'](e);}, true);}});});
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}}); let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode); c1.push(pvnode);
w2.__owl__.pvnode = pvnode; w2.__owl__.pvnode = pvnode;
@@ -849,11 +808,11 @@ exports[`other directives with t-component t-on with getter as handler 1`] = `
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey3 = \`Child\`; let componentKey3 = \`Child\`;
let W3 = scope['Child'] || context.constructor.components[componentKey3] || QWeb.components[componentKey3]; let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| scope['Child'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')} if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3); w3 = new W3(parent, props3);
parent.__owl__.cmap['__4__'] = w3.__owl__.id; parent.__owl__.cmap['__4__'] = w3.__owl__.id;
let fiber = w3.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['handler'](e);});}});}); let fiber = w3.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['handler'](e);});}});});
let pvnode = h('dummy', {key: '__4__', hook: {remove() {},destroy(vn) {w3.destroy();}}}); let pvnode = h('dummy', {key: '__4__', hook: {remove() {},destroy(vn) {w3.destroy();}}});
c1.push(pvnode); c1.push(pvnode);
w3.__owl__.pvnode = pvnode; w3.__owl__.pvnode = pvnode;
@@ -888,11 +847,11 @@ exports[`other directives with t-component t-on with handler bound to argument 1
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey2 = \`child\`; let componentKey2 = \`child\`;
let W2 = scope['child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2]; let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')} if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2); w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id; parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['onEv'](...args4, e);});}});}); let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['onEv'](...args4, e);});}});});
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}}); let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode); c1.push(pvnode);
w2.__owl__.pvnode = pvnode; w2.__owl__.pvnode = pvnode;
@@ -927,11 +886,11 @@ exports[`other directives with t-component t-on with handler bound to empty obje
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey2 = \`child\`; let componentKey2 = \`child\`;
let W2 = scope['child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2]; let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')} if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2); w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id; parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['onEv'](...args4, e);});}});}); let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['onEv'](...args4, e);});}});});
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}}); let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode); c1.push(pvnode);
w2.__owl__.pvnode = pvnode; w2.__owl__.pvnode = pvnode;
@@ -966,11 +925,11 @@ exports[`other directives with t-component t-on with handler bound to empty obje
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey2 = \`child\`; let componentKey2 = \`child\`;
let W2 = scope['child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2]; let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')} if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2); w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id; parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['onEv'](...args4, e);});}});}); let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['onEv'](...args4, e);});}});});
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}}); let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode); c1.push(pvnode);
w2.__owl__.pvnode = pvnode; w2.__owl__.pvnode = pvnode;
@@ -1005,11 +964,11 @@ exports[`other directives with t-component t-on with handler bound to object 1`]
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey2 = \`child\`; let componentKey2 = \`child\`;
let W2 = scope['child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2]; let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')} if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2); w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id; parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['onEv'](...args4, e);});}});}); let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['onEv'](...args4, e);});}});});
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}}); let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode); c1.push(pvnode);
w2.__owl__.pvnode = pvnode; w2.__owl__.pvnode = pvnode;
@@ -1048,11 +1007,11 @@ exports[`other directives with t-component t-on with inline statement 1`] = `
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey3 = \`Child\`; let componentKey3 = \`Child\`;
let W3 = scope['Child'] || context.constructor.components[componentKey3] || QWeb.components[componentKey3]; let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| scope['Child'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')} if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3); w3 = new W3(parent, props3);
parent.__owl__.cmap['__4__'] = w3.__owl__.id; parent.__owl__.cmap['__4__'] = w3.__owl__.id;
let fiber = w3.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (context.__owl__.status === 5){return}const res = (() => { return state_5.counter++ })(); if (typeof res === 'function') { res(e) }});}});}); let fiber = w3.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (!context.__owl__.isMounted){return}state_5.counter++});}});});
let pvnode = h('dummy', {key: '__4__', hook: {remove() {},destroy(vn) {w3.destroy();}}}); let pvnode = h('dummy', {key: '__4__', hook: {remove() {},destroy(vn) {w3.destroy();}}});
c1.push(pvnode); c1.push(pvnode);
w3.__owl__.pvnode = pvnode; w3.__owl__.pvnode = pvnode;
@@ -1086,11 +1045,11 @@ exports[`other directives with t-component t-on with no handler (only modifiers)
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey2 = \`ComponentA\`; let componentKey2 = \`ComponentA\`;
let W2 = scope['ComponentA'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2]; let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['ComponentA'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')} if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2); w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id; parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['onEv'](e);});}});}); let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['onEv'](e);});}});});
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}}); let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode); c1.push(pvnode);
w2.__owl__.pvnode = pvnode; w2.__owl__.pvnode = pvnode;
@@ -1124,11 +1083,11 @@ exports[`other directives with t-component t-on with prevent and self modifiers
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey2 = \`Child\`; let componentKey2 = \`Child\`;
let W2 = scope['Child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2]; let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')} if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2); w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id; parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (context.__owl__.status === 5){return}e.preventDefault();if (e.target !== vn.elm) {return}utils.getComponent(context)['onEv'](e);});}});}); let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (!context.__owl__.isMounted){return}e.preventDefault();if (e.target !== vn.elm) {return}utils.getComponent(context)['onEv'](e);});}});});
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}}); let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode); c1.push(pvnode);
w2.__owl__.pvnode = pvnode; w2.__owl__.pvnode = pvnode;
@@ -1162,11 +1121,11 @@ exports[`other directives with t-component t-on with self and prevent modifiers
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey2 = \`child\`; let componentKey2 = \`child\`;
let W2 = scope['child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2]; let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')} if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2); w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id; parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (context.__owl__.status === 5){return}if (e.target !== vn.elm) {return}e.preventDefault();utils.getComponent(context)['onEv'](e);});}});}); let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (!context.__owl__.isMounted){return}if (e.target !== vn.elm) {return}e.preventDefault();utils.getComponent(context)['onEv'](e);});}});});
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}}); let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode); c1.push(pvnode);
w2.__owl__.pvnode = pvnode; w2.__owl__.pvnode = pvnode;
@@ -1200,11 +1159,11 @@ exports[`other directives with t-component t-on with self modifier 1`] = `
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey2 = \`child\`; let componentKey2 = \`child\`;
let W2 = scope['child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2]; let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')} if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2); w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id; parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev-1', function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['onEv1'](e);});vn.elm.addEventListener('ev-2', function (e) {if (context.__owl__.status === 5){return}if (e.target !== vn.elm) {return}utils.getComponent(context)['onEv2'](e);});}});}); let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev-1', function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['onEv1'](e);});vn.elm.addEventListener('ev-2', function (e) {if (!context.__owl__.isMounted){return}if (e.target !== vn.elm) {return}utils.getComponent(context)['onEv2'](e);});}});});
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}}); let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode); c1.push(pvnode);
w2.__owl__.pvnode = pvnode; w2.__owl__.pvnode = pvnode;
@@ -1238,11 +1197,11 @@ exports[`other directives with t-component t-on with stop and/or prevent modifie
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey2 = \`child\`; let componentKey2 = \`child\`;
let W2 = scope['child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2]; let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')} if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2); w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id; parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev-1', function (e) {if (context.__owl__.status === 5){return}e.stopPropagation();utils.getComponent(context)['onEv1'](e);});vn.elm.addEventListener('ev-2', function (e) {if (context.__owl__.status === 5){return}e.preventDefault();utils.getComponent(context)['onEv2'](e);});vn.elm.addEventListener('ev-3', function (e) {if (context.__owl__.status === 5){return}e.stopPropagation();e.preventDefault();utils.getComponent(context)['onEv3'](e);});}});}); let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev-1', function (e) {if (!context.__owl__.isMounted){return}e.stopPropagation();utils.getComponent(context)['onEv1'](e);});vn.elm.addEventListener('ev-2', function (e) {if (!context.__owl__.isMounted){return}e.preventDefault();utils.getComponent(context)['onEv2'](e);});vn.elm.addEventListener('ev-3', function (e) {if (!context.__owl__.isMounted){return}e.stopPropagation();e.preventDefault();utils.getComponent(context)['onEv3'](e);});}});});
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}}); let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode); c1.push(pvnode);
w2.__owl__.pvnode = pvnode; w2.__owl__.pvnode = pvnode;
@@ -1378,7 +1337,7 @@ exports[`other directives with t-component t-set not altered by child widget 1`]
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey3 = \`ChildWidget\`; let componentKey3 = \`ChildWidget\`;
let W3 = scope['ChildWidget'] || context.constructor.components[componentKey3] || QWeb.components[componentKey3]; let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| scope['ChildWidget'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')} if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3); w3 = new W3(parent, props3);
parent.__owl__.cmap['__4__'] = w3.__owl__.id; parent.__owl__.cmap['__4__'] = w3.__owl__.id;
@@ -1473,7 +1432,7 @@ exports[`props evaluation t-set with a body expression can be used as textual p
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey3 = \`Child\`; let componentKey3 = \`Child\`;
let W3 = scope['Child'] || context.constructor.components[componentKey3] || QWeb.components[componentKey3]; let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| scope['Child'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')} if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3); w3 = new W3(parent, props3);
parent.__owl__.cmap['__4__'] = w3.__owl__.id; parent.__owl__.cmap['__4__'] = w3.__owl__.id;
@@ -1532,7 +1491,7 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey2 = \`child\`; let componentKey2 = \`child\`;
let W2 = scope['child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2]; let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')} if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2); w2 = new W2(parent, props2);
parent.__owl__.cmap[k3] = w2.__owl__.id; parent.__owl__.cmap[k3] = w2.__owl__.id;
@@ -1592,11 +1551,11 @@ exports[`random stuff/miscellaneous t-on with handler bound to dynamic argument
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey6 = \`Child\`; let componentKey6 = \`Child\`;
let W6 = scope['Child'] || context.constructor.components[componentKey6] || QWeb.components[componentKey6]; let W6 = context.constructor.components[componentKey6] || QWeb.components[componentKey6]|| scope['Child'];
if (!W6) {throw new Error('Cannot find the definition of component \\"' + componentKey6 + '\\"')} if (!W6) {throw new Error('Cannot find the definition of component \\"' + componentKey6 + '\\"')}
w6 = new W6(parent, props6); w6 = new W6(parent, props6);
parent.__owl__.cmap[k7] = w6.__owl__.id; parent.__owl__.cmap[k7] = w6.__owl__.id;
let fiber = w6.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['onEv'](...args8, e);});}});}); let fiber = w6.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['onEv'](...args8, e);});}});});
let pvnode = h('dummy', {key: k7, hook: {remove() {},destroy(vn) {w6.destroy();}}}); let pvnode = h('dummy', {key: k7, hook: {remove() {},destroy(vn) {w6.destroy();}}});
c1.push(pvnode); c1.push(pvnode);
w6.__owl__.pvnode = pvnode; w6.__owl__.pvnode = pvnode;
@@ -1614,7 +1573,6 @@ exports[`t-call handlers are properly bound through a t-call 1`] = `
) { ) {
// Template name: \\"sub\\" // Template name: \\"sub\\"
let utils = this.constructor.utils; let utils = this.constructor.utils;
let parent = extra.parent;
let h = this.h; let h = this.h;
let c2 = extra.parentNode; let c2 = extra.parentNode;
let key0 = extra.key || \\"\\"; let key0 = extra.key || \\"\\";
@@ -1622,7 +1580,7 @@ exports[`t-call handlers are properly bound through a t-call 1`] = `
let vn3 = h('p', p3, c3); let vn3 = h('p', p3, c3);
c2.push(vn3); c2.push(vn3);
let k4 = \`click__4__\${key0}__\`; let k4 = \`click__4__\${key0}__\`;
extra.handlers[k4] = extra.handlers[k4] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['update'](e);}; extra.handlers[k4] = extra.handlers[k4] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['update'](e);};
p3.on['click'] = extra.handlers[k4]; p3.on['click'] = extra.handlers[k4];
c3.push({text: \`lucas\`}); c3.push({text: \`lucas\`});
}" }"
@@ -1633,7 +1591,6 @@ exports[`t-call handlers with arguments are properly bound through a t-call 1`]
) { ) {
// Template name: \\"sub\\" // Template name: \\"sub\\"
let utils = this.constructor.utils; let utils = this.constructor.utils;
let parent = extra.parent;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let c2 = extra.parentNode; let c2 = extra.parentNode;
@@ -1642,7 +1599,7 @@ exports[`t-call handlers with arguments are properly bound through a t-call 1`]
let vn3 = h('p', p3, c3); let vn3 = h('p', p3, c3);
c2.push(vn3); c2.push(vn3);
let args4 = [scope['a']]; let args4 = [scope['a']];
p3.on['click'] = function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['update'](...args4, e);}; p3.on['click'] = function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['update'](...args4, e);};
c3.push({text: \`lucas\`}); c3.push({text: \`lucas\`});
}" }"
`; `;
@@ -1672,7 +1629,7 @@ exports[`t-call parent is set within t-call 1`] = `
c2.push(pvnode); c2.push(pvnode);
} else { } else {
let componentKey3 = \`Child\`; let componentKey3 = \`Child\`;
let W3 = scope['Child'] || context.constructor.components[componentKey3] || QWeb.components[componentKey3]; let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| scope['Child'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')} if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3); w3 = new W3(parent, props3);
parent.__owl__.cmap[k4] = w3.__owl__.id; parent.__owl__.cmap[k4] = w3.__owl__.id;
@@ -1710,7 +1667,7 @@ exports[`t-call parent is set within t-call with no parentNode 1`] = `
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey2 = \`Child\`; let componentKey2 = \`Child\`;
let W2 = scope['Child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2]; let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')} if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2); w2 = new W2(parent, props2);
parent.__owl__.cmap[k3] = w2.__owl__.id; parent.__owl__.cmap[k3] = w2.__owl__.id;
@@ -2105,7 +2062,7 @@ exports[`top level sub widgets basic use 1`] = `
utils.defineProxy(vn3, pvnode); utils.defineProxy(vn3, pvnode);
} else { } else {
let componentKey1 = \`Child\`; let componentKey1 = \`Child\`;
let W1 = scope['Child'] || context.constructor.components[componentKey1] || QWeb.components[componentKey1]; let W1 = context.constructor.components[componentKey1] || QWeb.components[componentKey1]|| scope['Child'];
if (!W1) {throw new Error('Cannot find the definition of component \\"' + componentKey1 + '\\"')} if (!W1) {throw new Error('Cannot find the definition of component \\"' + componentKey1 + '\\"')}
w1 = new W1(parent, props1); w1 = new W1(parent, props1);
parent.__owl__.cmap['__2__'] = w1.__owl__.id; parent.__owl__.cmap['__2__'] = w1.__owl__.id;
@@ -2145,7 +2102,7 @@ exports[`top level sub widgets can select a sub widget 1`] = `
utils.defineProxy(vn3, pvnode); utils.defineProxy(vn3, pvnode);
} else { } else {
let componentKey1 = \`Child\`; let componentKey1 = \`Child\`;
let W1 = scope['Child'] || context.constructor.components[componentKey1] || QWeb.components[componentKey1]; let W1 = context.constructor.components[componentKey1] || QWeb.components[componentKey1]|| scope['Child'];
if (!W1) {throw new Error('Cannot find the definition of component \\"' + componentKey1 + '\\"')} if (!W1) {throw new Error('Cannot find the definition of component \\"' + componentKey1 + '\\"')}
w1 = new W1(parent, props1); w1 = new W1(parent, props1);
parent.__owl__.cmap['__2__'] = w1.__owl__.id; parent.__owl__.cmap['__2__'] = w1.__owl__.id;
@@ -2172,7 +2129,7 @@ exports[`top level sub widgets can select a sub widget 1`] = `
utils.defineProxy(vn6, pvnode); utils.defineProxy(vn6, pvnode);
} else { } else {
let componentKey4 = \`OtherChild\`; let componentKey4 = \`OtherChild\`;
let W4 = scope['OtherChild'] || context.constructor.components[componentKey4] || QWeb.components[componentKey4]; let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4]|| scope['OtherChild'];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')} if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(parent, props4); w4 = new W4(parent, props4);
parent.__owl__.cmap['__5__'] = w4.__owl__.id; parent.__owl__.cmap['__5__'] = w4.__owl__.id;
@@ -24,7 +24,7 @@ exports[`props validation props are validated in dev mode (code snapshot) 1`] =
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey2 = \`Child\`; let componentKey2 = \`Child\`;
let W2 = scope['Child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2]; let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')} if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2); w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id; parent.__owl__.cmap['__3__'] = w2.__owl__.id;
+51 -191
View File
@@ -19,17 +19,17 @@ exports[`t-slot directive can define and call slots 1`] = `
w2 = false; w2 = false;
} }
if (w2) { if (w2) {
w2.__updateProps(props2, extra.fiber, utils.combine(context, scope)); w2.__updateProps(props2, extra.fiber, Object.assign(Object.create(context), scope));
let pvnode = w2.__owl__.pvnode; let pvnode = w2.__owl__.pvnode;
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey2 = \`Dialog\`; let componentKey2 = \`Dialog\`;
let W2 = scope['Dialog'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2]; let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Dialog'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')} if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2); w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id; parent.__owl__.cmap['__3__'] = w2.__owl__.id;
w2.__owl__.slotId = 1; w2.__owl__.slotId = 1;
let fiber = w2.__prepare(extra.fiber, utils.combine(context, scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); let fiber = w2.__prepare(extra.fiber, Object.assign(Object.create(context), scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}}); let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode); c1.push(pvnode);
w2.__owl__.pvnode = pvnode; w2.__owl__.pvnode = pvnode;
@@ -44,23 +44,23 @@ exports[`t-slot directive can define and call slots 2`] = `
) { ) {
// Template name: \\"Dialog\\" // Template name: \\"Dialog\\"
let h = this.h; let h = this.h;
let c8 = [], p8 = {key:8};
let vn8 = h('div', p8, c8);
let c9 = [], p9 = {key:9}; let c9 = [], p9 = {key:9};
let vn9 = h('div', p9, c9); let vn9 = h('div', p9, c9);
c8.push(vn9); let c10 = [], p10 = {key:10};
const slot10 = this.constructor.slots[context.__owl__.slotId + '_' + 'header']; let vn10 = h('div', p10, c10);
if (slot10) { c9.push(vn10);
slot10.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c9, parent: extra.parent || context})); const slot11 = this.constructor.slots[context.__owl__.slotId + '_' + 'header'];
if (slot11) {
slot11.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c10, parent: extra.parent || context}));
} }
let c11 = [], p11 = {key:11}; let c12 = [], p12 = {key:12};
let vn11 = h('div', p11, c11); let vn12 = h('div', p12, c12);
c8.push(vn11); c9.push(vn12);
const slot12 = this.constructor.slots[context.__owl__.slotId + '_' + 'footer']; const slot13 = this.constructor.slots[context.__owl__.slotId + '_' + 'footer'];
if (slot12) { if (slot13) {
slot12.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c11, parent: extra.parent || context})); slot13.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c12, parent: extra.parent || context}));
} }
return vn8; return vn9;
}" }"
`; `;
@@ -68,7 +68,6 @@ exports[`t-slot directive can define and call slots 3`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"slot_header_template\\" // Template name: \\"slot_header_template\\"
let parent = extra.parent;
let h = this.h; let h = this.h;
let c4 = extra.parentNode; let c4 = extra.parentNode;
let c5 = [], p5 = {key:5}; let c5 = [], p5 = {key:5};
@@ -82,7 +81,6 @@ exports[`t-slot directive can define and call slots 4`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"slot_footer_template\\" // Template name: \\"slot_footer_template\\"
let parent = extra.parent;
let h = this.h; let h = this.h;
let c6 = extra.parentNode; let c6 = extra.parentNode;
let c7 = [], p7 = {key:7}; let c7 = [], p7 = {key:7};
@@ -111,17 +109,17 @@ exports[`t-slot directive can define and call slots using old t-set keyword 1`]
w2 = false; w2 = false;
} }
if (w2) { if (w2) {
w2.__updateProps(props2, extra.fiber, utils.combine(context, scope)); w2.__updateProps(props2, extra.fiber, Object.assign(Object.create(context), scope));
let pvnode = w2.__owl__.pvnode; let pvnode = w2.__owl__.pvnode;
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey2 = \`Dialog\`; let componentKey2 = \`Dialog\`;
let W2 = scope['Dialog'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2]; let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Dialog'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')} if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2); w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id; parent.__owl__.cmap['__3__'] = w2.__owl__.id;
w2.__owl__.slotId = 1; w2.__owl__.slotId = 1;
let fiber = w2.__prepare(extra.fiber, utils.combine(context, scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); let fiber = w2.__prepare(extra.fiber, Object.assign(Object.create(context), scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}}); let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode); c1.push(pvnode);
w2.__owl__.pvnode = pvnode; w2.__owl__.pvnode = pvnode;
@@ -136,23 +134,23 @@ exports[`t-slot directive can define and call slots using old t-set keyword 2`]
) { ) {
// Template name: \\"__template__1\\" // Template name: \\"__template__1\\"
let h = this.h; let h = this.h;
let c8 = [], p8 = {key:8};
let vn8 = h('div', p8, c8);
let c9 = [], p9 = {key:9}; let c9 = [], p9 = {key:9};
let vn9 = h('div', p9, c9); let vn9 = h('div', p9, c9);
c8.push(vn9); let c10 = [], p10 = {key:10};
const slot10 = this.constructor.slots[context.__owl__.slotId + '_' + 'header']; let vn10 = h('div', p10, c10);
if (slot10) { c9.push(vn10);
slot10.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c9, parent: extra.parent || context})); const slot11 = this.constructor.slots[context.__owl__.slotId + '_' + 'header'];
if (slot11) {
slot11.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c10, parent: extra.parent || context}));
} }
let c11 = [], p11 = {key:11}; let c12 = [], p12 = {key:12};
let vn11 = h('div', p11, c11); let vn12 = h('div', p12, c12);
c8.push(vn11); c9.push(vn12);
const slot12 = this.constructor.slots[context.__owl__.slotId + '_' + 'footer']; const slot13 = this.constructor.slots[context.__owl__.slotId + '_' + 'footer'];
if (slot12) { if (slot13) {
slot12.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c11, parent: extra.parent || context})); slot13.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c12, parent: extra.parent || context}));
} }
return vn8; return vn9;
}" }"
`; `;
@@ -160,7 +158,6 @@ exports[`t-slot directive can define and call slots using old t-set keyword 3`]
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"slot_header_template\\" // Template name: \\"slot_header_template\\"
let parent = extra.parent;
let h = this.h; let h = this.h;
let c4 = extra.parentNode; let c4 = extra.parentNode;
let c5 = [], p5 = {key:5}; let c5 = [], p5 = {key:5};
@@ -174,7 +171,6 @@ exports[`t-slot directive can define and call slots using old t-set keyword 4`]
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"slot_footer_template\\" // Template name: \\"slot_footer_template\\"
let parent = extra.parent;
let h = this.h; let h = this.h;
let c6 = extra.parentNode; let c6 = extra.parentNode;
let c7 = [], p7 = {key:7}; let c7 = [], p7 = {key:7};
@@ -188,7 +184,6 @@ exports[`t-slot directive content is the default slot 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"slot_default_template\\" // Template name: \\"slot_default_template\\"
let parent = extra.parent;
let h = this.h; let h = this.h;
let c4 = extra.parentNode; let c4 = extra.parentNode;
let c5 = [], p5 = {key:5}; let c5 = [], p5 = {key:5};
@@ -215,50 +210,10 @@ exports[`t-slot directive dafault slots can define a default content 1`] = `
}" }"
`; `;
exports[`t-slot directive default slot next to named slot, with default content 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__2\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
// Component 'Dialog'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = {};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, utils.combine(context, scope));
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`Dialog\`;
let W2 = scope['Dialog'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
w2.__owl__.slotId = 1;
let fiber = w2.__prepare(extra.fiber, utils.combine(context, scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
}
w2.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
exports[`t-slot directive default slot work with text nodes 1`] = ` exports[`t-slot directive default slot work with text nodes 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"slot_default_template\\" // Template name: \\"slot_default_template\\"
let parent = extra.parent;
let h = this.h; let h = this.h;
let c4 = extra.parentNode; let c4 = extra.parentNode;
c4.push({text: \`sts rocks\`}); c4.push({text: \`sts rocks\`});
@@ -272,15 +227,15 @@ exports[`t-slot directive dynamic t-slot call 1`] = `
let utils = this.constructor.utils; let utils = this.constructor.utils;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let c9 = [], p9 = {key:9,on:{}}; let c10 = [], p10 = {key:10,on:{}};
let vn9 = h('button', p9, c9); let vn10 = h('button', p10, c10);
extra.handlers['click__10__'] = extra.handlers['click__10__'] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['toggle'](e);}; extra.handlers['click__11__'] = extra.handlers['click__11__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['toggle'](e);};
p9.on['click'] = extra.handlers['click__10__']; p10.on['click'] = extra.handlers['click__11__'];
const slot11 = this.constructor.slots[context.__owl__.slotId + '_' + (scope['current'].slot)]; const slot12 = this.constructor.slots[context.__owl__.slotId + '_' + (scope['current'].slot)];
if (slot11) { if (slot12) {
slot11.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c9, parent: extra.parent || context})); slot12.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c10, parent: extra.parent || context}));
} }
return vn9; return vn10;
}" }"
`; `;
@@ -288,7 +243,6 @@ exports[`t-slot directive multiple roots are allowed in a default slot 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"slot_default_template\\" // Template name: \\"slot_default_template\\"
let parent = extra.parent;
let h = this.h; let h = this.h;
let c4 = extra.parentNode; let c4 = extra.parentNode;
let c5 = [], p5 = {key:5}; let c5 = [], p5 = {key:5};
@@ -306,7 +260,6 @@ exports[`t-slot directive multiple roots are allowed in a named slot 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"slot_content_template\\" // Template name: \\"slot_content_template\\"
let parent = extra.parent;
let h = this.h; let h = this.h;
let c4 = extra.parentNode; let c4 = extra.parentNode;
let c5 = [], p5 = {key:5}; let c5 = [], p5 = {key:5};
@@ -342,14 +295,13 @@ exports[`t-slot directive refs are properly bound in slots 1`] = `
) { ) {
// Template name: \\"slot_footer_template\\" // Template name: \\"slot_footer_template\\"
let utils = this.constructor.utils; let utils = this.constructor.utils;
let parent = extra.parent;
context.__owl__.refs = context.__owl__.refs || {}; context.__owl__.refs = context.__owl__.refs || {};
let h = this.h; let h = this.h;
let c8 = extra.parentNode; let c8 = extra.parentNode;
let c9 = [], p9 = {key:9,on:{}}; let c9 = [], p9 = {key:9,on:{}};
let vn9 = h('button', p9, c9); let vn9 = h('button', p9, c9);
c8.push(vn9); c8.push(vn9);
extra.handlers['click__10__'] = extra.handlers['click__10__'] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['doSomething'](e);}; extra.handlers['click__10__'] = extra.handlers['click__10__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['doSomething'](e);};
p9.on['click'] = extra.handlers['click__10__']; p9.on['click'] = extra.handlers['click__10__'];
const ref11 = \`myButton\`; const ref11 = \`myButton\`;
p9.hook = { p9.hook = {
@@ -369,13 +321,12 @@ exports[`t-slot directive slots are rendered with proper context 1`] = `
) { ) {
// Template name: \\"slot_footer_template\\" // Template name: \\"slot_footer_template\\"
let utils = this.constructor.utils; let utils = this.constructor.utils;
let parent = extra.parent;
let h = this.h; let h = this.h;
let c8 = extra.parentNode; let c8 = extra.parentNode;
let c9 = [], p9 = {key:9,on:{}}; let c9 = [], p9 = {key:9,on:{}};
let vn9 = h('button', p9, c9); let vn9 = h('button', p9, c9);
c8.push(vn9); c8.push(vn9);
extra.handlers['click__10__'] = extra.handlers['click__10__'] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['doSomething'](e);}; extra.handlers['click__10__'] = extra.handlers['click__10__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['doSomething'](e);};
p9.on['click'] = extra.handlers['click__10__']; p9.on['click'] = extra.handlers['click__10__'];
c9.push({text: \`do something\`}); c9.push({text: \`do something\`});
}" }"
@@ -441,17 +392,17 @@ exports[`t-slot directive slots are rendered with proper context, part 2 2`] = `
w8 = false; w8 = false;
} }
if (w8) { if (w8) {
w8.__updateProps(props8, extra.fiber, utils.combine(context, scope)); w8.__updateProps(props8, extra.fiber, Object.assign(Object.create(context), scope));
let pvnode = w8.__owl__.pvnode; let pvnode = w8.__owl__.pvnode;
c7.push(pvnode); c7.push(pvnode);
} else { } else {
let componentKey8 = \`Link\`; let componentKey8 = \`Link\`;
let W8 = scope['Link'] || context.constructor.components[componentKey8] || QWeb.components[componentKey8]; let W8 = context.constructor.components[componentKey8] || QWeb.components[componentKey8]|| scope['Link'];
if (!W8) {throw new Error('Cannot find the definition of component \\"' + componentKey8 + '\\"')} if (!W8) {throw new Error('Cannot find the definition of component \\"' + componentKey8 + '\\"')}
w8 = new W8(parent, props8); w8 = new W8(parent, props8);
parent.__owl__.cmap[k9] = w8.__owl__.id; parent.__owl__.cmap[k9] = w8.__owl__.id;
w8.__owl__.slotId = 1; w8.__owl__.slotId = 1;
let fiber = w8.__prepare(extra.fiber, utils.combine(context, scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); let fiber = w8.__prepare(extra.fiber, Object.assign(Object.create(context), scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k9, hook: {remove() {},destroy(vn) {w8.destroy();}}}); let pvnode = h('dummy', {key: k9, hook: {remove() {},destroy(vn) {w8.destroy();}}});
c7.push(pvnode); c7.push(pvnode);
w8.__owl__.pvnode = pvnode; w8.__owl__.pvnode = pvnode;
@@ -467,7 +418,6 @@ exports[`t-slot directive slots are rendered with proper context, part 2 3`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"slot_default_template\\" // Template name: \\"slot_default_template\\"
let parent = extra.parent;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let c10 = extra.parentNode; let c10 = extra.parentNode;
@@ -540,17 +490,17 @@ exports[`t-slot directive slots are rendered with proper context, part 3 2`] = `
w8 = false; w8 = false;
} }
if (w8) { if (w8) {
w8.__updateProps(props8, extra.fiber, utils.combine(context, scope)); w8.__updateProps(props8, extra.fiber, Object.assign(Object.create(context), scope));
let pvnode = w8.__owl__.pvnode; let pvnode = w8.__owl__.pvnode;
c7.push(pvnode); c7.push(pvnode);
} else { } else {
let componentKey8 = \`Link\`; let componentKey8 = \`Link\`;
let W8 = scope['Link'] || context.constructor.components[componentKey8] || QWeb.components[componentKey8]; let W8 = context.constructor.components[componentKey8] || QWeb.components[componentKey8]|| scope['Link'];
if (!W8) {throw new Error('Cannot find the definition of component \\"' + componentKey8 + '\\"')} if (!W8) {throw new Error('Cannot find the definition of component \\"' + componentKey8 + '\\"')}
w8 = new W8(parent, props8); w8 = new W8(parent, props8);
parent.__owl__.cmap[k9] = w8.__owl__.id; parent.__owl__.cmap[k9] = w8.__owl__.id;
w8.__owl__.slotId = 1; w8.__owl__.slotId = 1;
let fiber = w8.__prepare(extra.fiber, utils.combine(context, scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); let fiber = w8.__prepare(extra.fiber, Object.assign(Object.create(context), scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k9, hook: {remove() {},destroy(vn) {w8.destroy();}}}); let pvnode = h('dummy', {key: k9, hook: {remove() {},destroy(vn) {w8.destroy();}}});
c7.push(pvnode); c7.push(pvnode);
w8.__owl__.pvnode = pvnode; w8.__owl__.pvnode = pvnode;
@@ -566,7 +516,6 @@ exports[`t-slot directive slots are rendered with proper context, part 3 3`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"slot_default_template\\" // Template name: \\"slot_default_template\\"
let parent = extra.parent;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let c10 = extra.parentNode; let c10 = extra.parentNode;
@@ -597,17 +546,17 @@ exports[`t-slot directive slots are rendered with proper context, part 4 1`] = `
w2 = false; w2 = false;
} }
if (w2) { if (w2) {
w2.__updateProps(props2, extra.fiber, utils.combine(context, scope)); w2.__updateProps(props2, extra.fiber, Object.assign(Object.create(context), scope));
let pvnode = w2.__owl__.pvnode; let pvnode = w2.__owl__.pvnode;
c1.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey2 = \`Link\`; let componentKey2 = \`Link\`;
let W2 = scope['Link'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2]; let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Link'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')} if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2); w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id; parent.__owl__.cmap['__3__'] = w2.__owl__.id;
w2.__owl__.slotId = 1; w2.__owl__.slotId = 1;
let fiber = w2.__prepare(extra.fiber, utils.combine(context, scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); let fiber = w2.__prepare(extra.fiber, Object.assign(Object.create(context), scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}}); let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode); c1.push(pvnode);
w2.__owl__.pvnode = pvnode; w2.__owl__.pvnode = pvnode;
@@ -621,7 +570,6 @@ exports[`t-slot directive slots are rendered with proper context, part 4 2`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"slot_default_template\\" // Template name: \\"slot_default_template\\"
let parent = extra.parent;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let c4 = extra.parentNode; let c4 = extra.parentNode;
@@ -632,94 +580,6 @@ exports[`t-slot directive slots are rendered with proper context, part 4 2`] = `
}" }"
`; `;
exports[`t-slot directive slots in t-foreach in t-foreach 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__2\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
let _2 = scope['tree'];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
}
let _length3 = _3.length;
let _origScope5 = scope;
scope = Object.create(scope);
for (let i1 = 0; i1 < _length3; i1++) {
scope.node1_first = i1 === 0
scope.node1_last = i1 === _length3 - 1
scope.node1_index = i1
scope.node1 = _3[i1]
scope.node1_value = _4[i1]
let key1 = scope['node1'].key;
let c6 = [], p6 = {key:\`\${key1}_6\`};
let vn6 = h('div', p6, c6);
c1.push(vn6);
let _7 = scope['node1'].value;
if (_7 != null) {
c6.push({text: _7});
}
let c8 = [], p8 = {key:\`\${key1}_8\`};
let vn8 = h('ul', p8, c8);
c1.push(vn8);
let _9 = scope['node1'].nodes;
if (!_9) { throw new Error('QWeb error: Invalid loop expression')}
let _10 = _11 = _9;
if (!(_9 instanceof Array)) {
_10 = Object.keys(_9);
_11 = Object.values(_9);
}
let _length10 = _10.length;
let _origScope12 = scope;
scope = Object.create(scope);
for (let i2 = 0; i2 < _length10; i2++) {
scope.node2_first = i2 === 0
scope.node2_last = i2 === _length10 - 1
scope.node2_index = i2
scope.node2 = _10[i2]
scope.node2_value = _11[i2]
let key2 = scope['node2'].key;
// Component 'Child'
let k14 = \`__14__\${key1}__\${key2}__\`;
let w13 = k14 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k14]] : false;
let props13 = {};
if (w13 && w13.__owl__.currentFiber && !w13.__owl__.vnode) {
w13.destroy();
w13 = false;
}
if (w13) {
w13.__updateProps(props13, extra.fiber, utils.combine(context, scope));
let pvnode = w13.__owl__.pvnode;
c8.push(pvnode);
} else {
let componentKey13 = \`Child\`;
let W13 = scope['Child'] || context.constructor.components[componentKey13] || QWeb.components[componentKey13];
if (!W13) {throw new Error('Cannot find the definition of component \\"' + componentKey13 + '\\"')}
w13 = new W13(parent, props13);
parent.__owl__.cmap[k14] = w13.__owl__.id;
w13.__owl__.slotId = 1;
let fiber = w13.__prepare(extra.fiber, utils.combine(context, scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k14, hook: {remove() {},destroy(vn) {w13.destroy();}}});
c8.push(pvnode);
w13.__owl__.pvnode = pvnode;
}
w13.__owl__.parentLastFiberId = extra.fiber.id;
}
scope = _origScope12;
}
scope = _origScope5;
return vn1;
}"
`;
exports[`t-slot directive t-set t-value in a slot 1`] = ` exports[`t-slot directive t-set t-value in a slot 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
+5 -161
View File
@@ -1,4 +1,4 @@
import { Component, Env, STATUS } from "../../src/component/component"; import { Component, Env } from "../../src/component/component";
import { useState } from "../../src/hooks"; import { useState } from "../../src/hooks";
import { xml } from "../../src/tags"; import { xml } from "../../src/tags";
import { makeDeferred, makeTestEnv, makeTestFixture, nextMicroTick, nextTick } from "../helpers"; import { makeDeferred, makeTestEnv, makeTestFixture, nextMicroTick, nextTick } from "../helpers";
@@ -40,14 +40,14 @@ describe("async rendering", () => {
} }
} }
const w = new W(); const w = new W();
expect(w.__owl__.status).toBe(STATUS.CREATED);
w.mount(fixture); w.mount(fixture);
expect(w.__owl__.status).toBe(STATUS.WILLSTARTED); expect(w.__owl__.isDestroyed).toBe(false);
expect(w.__owl__.isMounted).toBe(false);
w.destroy(); w.destroy();
expect(w.__owl__.status).toBe(STATUS.DESTROYED);
def.resolve(); def.resolve();
await nextTick(); await nextTick();
expect(w.__owl__.status).toBe(STATUS.DESTROYED); expect(w.__owl__.isDestroyed).toBe(true);
expect(w.__owl__.isMounted).toBe(false);
}); });
test("destroying/recreating a subwidget with different props (if start is not over)", async () => { test("destroying/recreating a subwidget with different props (if start is not over)", async () => {
@@ -1405,160 +1405,4 @@ describe("async rendering", () => {
expect(fixture.innerHTML).toBe("<div>2</div>"); expect(fixture.innerHTML).toBe("<div>2</div>");
expect(Widget.prototype.__render).toHaveBeenCalledTimes(2); expect(Widget.prototype.__render).toHaveBeenCalledTimes(2);
}); });
test("components with shouldUpdate=false", async () => {
const state = { p: 1, cc: 10 };
class ChildChild extends Component {
static template = xml`
<div>
child child: <t t-esc="state.cc"/>
</div>`;
state = state;
shouldUpdate() {
return false;
}
}
class Child extends Component {
static components = { ChildChild };
static template = xml`
<div>
child
<ChildChild/>
</div>`;
shouldUpdate() {
return false;
}
}
let parent: any;
class Parent extends Component {
static components = { Child };
static template = xml`
<div>
parent: <t t-esc="state.p"/>
<Child/>
</div>`;
state = state;
constructor(a, b) {
super(a, b);
parent = this;
}
shouldUpdate() {
return false;
}
}
class App extends Component {
static components = { Parent };
static template = xml`
<div>
<Parent/>
</div>`;
}
var div = document.createElement("div");
fixture.appendChild(div);
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
"<div></div><div><div> parent: 1<div> child <div> child child: 10</div></div></div></div>"
);
app.mount(div);
// wait for rendering from second mount to go through parent
await Promise.resolve();
await Promise.resolve();
state.cc++;
state.p++;
parent.render();
await nextTick();
expect(fixture.innerHTML).toBe(
"<div><div><div> parent: 2<div> child <div> child child: 11</div></div></div></div></div>"
);
});
test("components with shouldUpdate=false, part 2", async () => {
const state = { p: 1, cc: 10 };
let shouldUpdate = true;
class ChildChild extends Component {
static template = xml`
<div>
child child: <t t-esc="state.cc"/>
</div>`;
state = state;
shouldUpdate() {
return shouldUpdate;
}
}
class Child extends Component {
static components = { ChildChild };
static template = xml`
<div>
child
<ChildChild/>
</div>`;
shouldUpdate() {
return shouldUpdate;
}
}
let parent: any;
class Parent extends Component {
static components = { Child };
static template = xml`
<div>
parent: <t t-esc="state.p"/>
<Child/>
</div>`;
state = state;
constructor(a, b) {
super(a, b);
parent = this;
}
shouldUpdate() {
return shouldUpdate;
}
}
class App extends Component {
static components = { Parent };
static template = xml`
<div>
<Parent/>
</div>`;
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
"<div><div> parent: 1<div> child <div> child child: 10</div></div></div></div>"
);
state.cc++;
state.p++;
app.render();
// wait for rendering to go through child
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
shouldUpdate = false;
parent.render();
await nextTick();
expect(fixture.innerHTML).toBe(
"<div><div> parent: 2<div> child <div> child child: 11</div></div></div></div>"
);
});
}); });
+1 -2
View File
@@ -265,8 +265,7 @@ describe("class and style attributes with t-component", () => {
error = e; error = e;
} }
expect(error).toBeDefined(); expect(error).toBeDefined();
const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g; expect(error.message).toBe("Cannot read property 'crash' of undefined");
expect(error.message).toMatch(regexp);
expect(fixture.innerHTML).toBe(""); expect(fixture.innerHTML).toBe("");
}); });
}); });
+28 -188
View File
@@ -1,4 +1,4 @@
import { Component, Env, mount, STATUS } from "../../src/component/component"; import { Component, Env, mount } from "../../src/component/component";
import { EventBus } from "../../src/core/event_bus"; import { EventBus } from "../../src/core/event_bus";
import { useRef, useState } from "../../src/hooks"; import { useRef, useState } from "../../src/hooks";
import { QWeb } from "../../src/qweb/qweb"; import { QWeb } from "../../src/qweb/qweb";
@@ -149,24 +149,6 @@ describe("basic widget properties", () => {
expect(fixture.innerHTML).toBe("<div>1<button>Inc</button></div>"); expect(fixture.innerHTML).toBe("<div>1<button>Inc</button></div>");
}); });
test("support for callable expression in event handler", async () => {
class Counter extends Component {
static template = xml`
<div><t t-esc="state.value"/><input type="text" t-on-input="obj.onInput"/></div>`;
state = useState({ value: "" });
obj = { onInput: (ev) => (this.state.value = ev.target.value) };
}
const counter = await mount(Counter, { target: fixture });
await nextTick();
expect(fixture.innerHTML).toBe(`<div><input type="text"></div>`);
const input = (<HTMLElement>counter.el).getElementsByTagName("input")[0];
input.value = "test";
input.dispatchEvent(new Event("input"));
await nextTick();
expect(fixture.innerHTML).toBe(`<div>test<input type="text"></div>`);
});
test("can handle empty props", async () => { test("can handle empty props", async () => {
class Child extends Component { class Child extends Component {
static template = xml`<span><t t-esc="props.val"/></span>`; static template = xml`<span><t t-esc="props.val"/></span>`;
@@ -181,7 +163,7 @@ describe("basic widget properties", () => {
expect(fixture.innerHTML).toBe("<div><span></span></div>"); expect(fixture.innerHTML).toBe("<div><span></span></div>");
}); });
test("can be clicked on and updated if not in DOM", async () => { test("cannot be clicked on and updated if not in DOM", async () => {
class Counter extends Component { class Counter extends Component {
static template = xml` static template = xml`
<div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`; <div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`;
@@ -196,8 +178,8 @@ describe("basic widget properties", () => {
const button = (<HTMLElement>counter.el).getElementsByTagName("button")[0]; const button = (<HTMLElement>counter.el).getElementsByTagName("button")[0];
button.click(); button.click();
await nextTick(); await nextTick();
expect(target.innerHTML).toBe("<div>1<button>Inc</button></div>"); expect(target.innerHTML).toBe("<div>0<button>Inc</button></div>");
expect(counter.state.counter).toBe(1); expect(counter.state.counter).toBe(0);
}); });
test("widget style and classname", async () => { test("widget style and classname", async () => {
@@ -679,8 +661,9 @@ describe("lifecycle hooks", () => {
class ChildWidget extends Component { class ChildWidget extends Component {
static template = xml`<div/>`; static template = xml`<div/>`;
setup() { constructor(parent) {
steps.push("setup"); super(parent);
steps.push("init");
} }
async willStart() { async willStart() {
steps.push("willstart"); steps.push("willstart");
@@ -704,10 +687,10 @@ describe("lifecycle hooks", () => {
const widget = new ParentWidget(); const widget = new ParentWidget();
await widget.mount(fixture); await widget.mount(fixture);
expect(steps).toEqual(["setup", "willstart", "mounted"]); expect(steps).toEqual(["init", "willstart", "mounted"]);
widget.state.ok = false; widget.state.ok = false;
await nextTick(); await nextTick();
expect(steps).toEqual(["setup", "willstart", "mounted", "willunmount"]); expect(steps).toEqual(["init", "willstart", "mounted", "willunmount"]);
}); });
test("components are unmounted and destroyed if no longer in DOM, even after updateprops", async () => { test("components are unmounted and destroyed if no longer in DOM, even after updateprops", async () => {
@@ -755,7 +738,8 @@ describe("lifecycle hooks", () => {
class ChildWidget extends Component { class ChildWidget extends Component {
static template = xml`<div/>`; static template = xml`<div/>`;
setup() { constructor(parent) {
super(parent);
steps.push("c init"); steps.push("c init");
} }
async willStart() { async willStart() {
@@ -771,7 +755,8 @@ describe("lifecycle hooks", () => {
class ParentWidget extends Component { class ParentWidget extends Component {
static template = xml`<div><t t-component="child"/></div>`; static template = xml`<div><t t-component="child"/></div>`;
static components = { child: ChildWidget }; static components = { child: ChildWidget };
setup() { constructor(parent?) {
super(parent);
steps.push("p init"); steps.push("p init");
} }
async willStart() { async willStart() {
@@ -977,69 +962,6 @@ describe("lifecycle hooks", () => {
"parent:patched", "parent:patched",
]); ]);
}); });
test("willPatch/patched hook is not called if not mounted in DOM", async () => {
const steps: string[] = [];
class ChildWidget extends Component {
static template = xml`<div/>`;
constructor(parent, props) {
super(parent, props);
steps.push("child:constructor");
}
mounted() {
steps.push("child:mounted");
}
willPatch() {
steps.push("child:willPatch");
}
patched() {
steps.push("child:patched");
}
}
class ParentWidget extends Component {
static template = xml`
<div>
<t t-component="child" v="state.n"/>
</div>
`;
static components = { child: ChildWidget };
state = useState({ n: 1 });
constructor() {
super();
steps.push("parent:constructor");
}
mounted() {
steps.push("parent:mounted");
}
willPatch() {
steps.push("parent:willPatch");
}
patched() {
steps.push("parent:patched");
}
}
const div = document.createElement("div");
const widget = new ParentWidget();
await widget.mount(div);
expect(steps).toEqual(["parent:constructor", "child:constructor"]);
widget.state.n = 2;
await nextTick();
expect(steps).toEqual(["parent:constructor", "child:constructor"]);
// then we remount the component in the dom
await widget.mount(fixture);
expect(steps).toEqual([
"parent:constructor",
"child:constructor",
"child:mounted",
"parent:mounted",
]);
});
}); });
describe("destroy method", () => { describe("destroy method", () => {
@@ -1053,7 +975,8 @@ describe("destroy method", () => {
expect(document.contains(widget.el)).toBe(true); expect(document.contains(widget.el)).toBe(true);
widget.destroy(); widget.destroy();
expect(document.contains(widget.el)).toBe(false); expect(document.contains(widget.el)).toBe(false);
expect(widget.__owl__.status).toBe(STATUS.DESTROYED); expect(widget.__owl__.isMounted).toBe(false);
expect(widget.__owl__.isDestroyed).toBe(true);
}); });
test("destroying a parent also destroys its children", async () => { test("destroying a parent also destroys its children", async () => {
@@ -1062,9 +985,9 @@ describe("destroy method", () => {
const child = children(parent)[0]; const child = children(parent)[0];
expect(child.__owl__.status).toBe(STATUS.MOUNTED); expect(child.__owl__.isDestroyed).toBe(false);
parent.destroy(); parent.destroy();
expect(child.__owl__.status).toBe(STATUS.DESTROYED); expect(child.__owl__.isDestroyed).toBe(true);
}); });
test("destroy remove the parent/children link", async () => { test("destroy remove the parent/children link", async () => {
@@ -1090,15 +1013,17 @@ describe("destroy method", () => {
} }
expect(fixture.innerHTML).toBe(""); expect(fixture.innerHTML).toBe("");
const widget = new DelayedWidget(); const widget = new DelayedWidget();
expect(widget.__owl__.status).toBe(STATUS.CREATED);
widget.mount(fixture); widget.mount(fixture);
expect(widget.__owl__.status).toBe(STATUS.WILLSTARTED); expect(widget.__owl__.isMounted).toBe(false);
expect(widget.__owl__.isDestroyed).toBe(false);
widget.destroy(); widget.destroy();
expect(widget.__owl__.status).toBe(STATUS.DESTROYED); expect(widget.__owl__.isMounted).toBe(false);
expect(widget.__owl__.isDestroyed).toBe(true);
def.resolve(); def.resolve();
await nextTick(); await nextTick();
expect(widget.__owl__.status).toBe(STATUS.DESTROYED); expect(widget.__owl__.isMounted).toBe(false);
expect(widget.__owl__.isDestroyed).toBe(true);
expect(widget.__owl__.vnode).toBe(undefined); expect(widget.__owl__.vnode).toBe(undefined);
expect(fixture.innerHTML).toBe(""); expect(fixture.innerHTML).toBe("");
expect(isRendered).toBe(false); expect(isRendered).toBe(false);
@@ -1246,30 +1171,6 @@ describe("composition", () => {
expect(fixture.innerHTML).toBe("<div>child b</div>"); expect(fixture.innerHTML).toBe("<div>child b</div>");
}); });
test("can switch between dynamic components without the need for a t-key", async () => {
class A extends Component {
static template = xml`<span>child a</span>`;
}
class B extends Component {
static template = xml`<span>child b</span>`;
}
class App extends Component {
static template = xml`
<div>
<t t-component="{{state.child}}"/>
</div>`;
static components = { A, B };
state = useState({ child: "A" });
}
const app = await mount(App, { target: fixture });
expect(fixture.innerHTML).toBe("<div><span>child a</span></div>");
app.state.child = "B";
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>child b</span></div>");
expect(QWeb.TEMPLATES[App.template].fn.toString()).toMatchSnapshot();
});
test("don't fallback to global registry if widget defined locally", async () => { test("don't fallback to global registry if widget defined locally", async () => {
QWeb.registerComponent("WidgetB", WidgetB); // should not use this widget QWeb.registerComponent("WidgetB", WidgetB); // should not use this widget
env.qweb.addTemplate("ParentWidget", `<div><t t-component="WidgetB"/></div>`); env.qweb.addTemplate("ParentWidget", `<div><t t-component="WidgetB"/></div>`);
@@ -1284,23 +1185,6 @@ describe("composition", () => {
delete QWeb.components["WidgetB"]; delete QWeb.components["WidgetB"];
}); });
test("don't fallback to global/component's registry if widget defined in the instance's context", async () => {
QWeb.registerComponent("WidgetB", WidgetB); // should not use this widget
env.qweb.addTemplate("ParentWidget", `<div><t t-component="WidgetB"/></div>`);
env.qweb.addTemplate("ComponentWidgetB", `<span>Belgium</span>`); // should not use this widget either
env.qweb.addTemplate("InstanceWidgetB", `<span>Chocolate</span>`); // should use this
class ComponentWidgetB extends Component {}
class InstanceWidgetB extends Component {}
class ParentWidget extends Component {
static components = { WidgetB: ComponentWidgetB };
WidgetB = InstanceWidgetB;
}
const widget = new ParentWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>Chocolate</span></div>");
delete QWeb.components["WidgetB"];
});
test("can define components in template without t-component", async () => { test("can define components in template without t-component", async () => {
env.qweb.addTemplates(` env.qweb.addTemplates(`
<templates> <templates>
@@ -1595,7 +1479,7 @@ describe("composition", () => {
parent.state.flag = true; parent.state.flag = true;
await nextTick(); await nextTick();
expect(children(parent)[0]).toBe(child); expect(children(parent)[0]).toBe(child);
expect(child.__owl__.status).toBe(STATUS.MOUNTED); expect(child.__owl__.isDestroyed).toBe(false);
expect(normalize(fixture.innerHTML)).toBe( expect(normalize(fixture.innerHTML)).toBe(
normalize(` normalize(`
<div> <div>
@@ -2343,29 +2227,9 @@ describe("other directives with t-component", () => {
el.click(); el.click();
expect(steps).toEqual(["click"]); expect(steps).toEqual(["click"]);
parent.unmount(); parent.unmount();
expect(child.__owl__.status).toBe(STATUS.UNMOUNTED); expect(child.__owl__.isMounted).toBe(false);
el.click(); el.click();
expect(steps).toEqual(["click", "click"]); expect(steps).toEqual(["click"]);
});
test("triggering custom event on mounted components", async () => {
let value = false;
class Child extends Component {
static template = xml`<div/>`;
mounted() {
this.trigger("coucou");
}
}
class Parent extends Component {
static template = xml`<Child t-on-coucou="doSomething"/>`;
static components = { Child };
doSomething() {
value = true;
}
}
const parent = new Parent();
await parent.mount(fixture);
expect(value).toBe(true);
}); });
test("t-on with .capture modifier", async () => { test("t-on with .capture modifier", async () => {
@@ -2417,7 +2281,7 @@ describe("other directives with t-component", () => {
expect(steps).toEqual(["click"]); expect(steps).toEqual(["click"]);
parent.state.flag = false; parent.state.flag = false;
await nextTick(); await nextTick();
expect(child.__owl__.status).toBe(STATUS.DESTROYED); expect(child.__owl__.isDestroyed).toBe(true);
el.click(); el.click();
expect(steps).toEqual(["click"]); expect(steps).toEqual(["click"]);
}); });
@@ -2452,7 +2316,7 @@ describe("other directives with t-component", () => {
expect(steps).toEqual(["click"]); expect(steps).toEqual(["click"]);
parent.state.flag = false; parent.state.flag = false;
await nextTick(); await nextTick();
expect(child.__owl__.status).toBe(STATUS.DESTROYED); expect(child.__owl__.isDestroyed).toBe(true);
el.click(); el.click();
expect(steps).toEqual(["click"]); expect(steps).toEqual(["click"]);
}); });
@@ -4064,30 +3928,6 @@ describe("dynamic t-props", () => {
expect(fixture.innerHTML).toBe("<div><span>3</span></div>"); expect(fixture.innerHTML).toBe("<div><span>3</span></div>");
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot(); expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
}); });
test("t-props with props", async () => {
expect.assertions(1);
class Child extends Component {
static template = xml`<div />`;
setup() {
expect(this.props).toEqual({ a: 1, b: 2, c: "c" });
}
}
class Parent extends Component {
static template = xml`
<div>
<Child t-props="props" a="1" b="2" />
</div>
`;
static components = { Child };
props = { a: "a", c: "c" };
}
const widget = new Parent();
await widget.mount(fixture);
});
}); });
describe("support svg components", () => { describe("support svg components", () => {
+7 -100
View File
@@ -1,4 +1,4 @@
import { Component, Env, mount, STATUS } from "../../src/component/component"; import { Component, Env } from "../../src/component/component";
import { useState } from "../../src/hooks"; import { useState } from "../../src/hooks";
import { xml } from "../../src/tags"; import { xml } from "../../src/tags";
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers"; import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
@@ -95,9 +95,7 @@ describe("component error handling (catchError)", () => {
try { try {
await super.render(); await super.render();
} catch (e) { } catch (e) {
expect(e.message).toMatch( expect(e.message).toBe("Cannot read property 'this' of undefined");
/Cannot read properties of undefined \(reading 'this'\)|Cannot read property 'this' of undefined/
);
} }
} }
} }
@@ -110,7 +108,7 @@ describe("component error handling (catchError)", () => {
expect(console.error).toBeCalledTimes(0); expect(console.error).toBeCalledTimes(0);
console.error = consoleError; console.error = consoleError;
expect(app.__owl__.status).toBe(STATUS.DESTROYED); expect(app.__owl__.isDestroyed).toBe(true);
expect(handler).toBeCalledTimes(1); expect(handler).toBeCalledTimes(1);
}); });
@@ -361,8 +359,7 @@ describe("component error handling (catchError)", () => {
error = e; error = e;
} }
expect(error).toBeDefined(); expect(error).toBeDefined();
const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g; expect(error.message).toBe("Cannot read property 'crash' of undefined");
expect(error.message).toMatch(regexp);
expect(console.error).toBeCalledTimes(0); expect(console.error).toBeCalledTimes(0);
console.error = consoleError; console.error = consoleError;
@@ -472,8 +469,7 @@ describe("component error handling (catchError)", () => {
error = e; error = e;
} }
expect(error).toBeDefined(); expect(error).toBeDefined();
const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g; expect(error.message).toBe("Cannot read property 'crash' of undefined");
expect(error.message).toMatch(regexp);
expect(console.error).toBeCalledTimes(0); expect(console.error).toBeCalledTimes(0);
console.error = consoleError; console.error = consoleError;
@@ -499,8 +495,7 @@ describe("component error handling (catchError)", () => {
error = e; error = e;
} }
expect(error).toBeDefined(); expect(error).toBeDefined();
const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g; expect(error.message).toBe("Cannot read property 'crash' of undefined");
expect(error.message).toMatch(regexp);
expect(console.error).toBeCalledTimes(0); expect(console.error).toBeCalledTimes(0);
console.error = consoleError; console.error = consoleError;
@@ -523,94 +518,6 @@ describe("component error handling (catchError)", () => {
error = e; error = e;
} }
expect(error).toBeDefined(); expect(error).toBeDefined();
const regexp = /Cannot read properties of undefined \(reading 'y'\)|Cannot read property 'y' of undefined/g; expect(error.message).toBe("Cannot read property 'y' of undefined");
expect(error.message).toMatch(regexp);
});
test("simple catchError", async () => {
class Boom extends Component {
static template = xml`<div t-esc="a.b.c"/>`;
}
class Parent extends Component {
static template = xml`
<div>
<t t-if="error">Error</t>
<t t-else="">
<Boom />
</t>
</div>`;
static components = { Boom };
error = false;
catchError(error) {
this.error = error;
this.render();
}
}
await mount(Parent, { target: fixture });
expect(fixture.innerHTML).toBe("<div>Error</div>");
});
test("catchError in catchError", async () => {
class Boom extends Component {
static template = xml`<div t-esc="a.b.c"/>`;
}
class Child extends Component {
static template = xml`
<div>
<Boom />
</div>`;
static components = { Boom };
catchError(error) {
throw error;
}
}
class Parent extends Component {
static template = xml`
<div>
<t t-if="error">Error</t>
<t t-else="">
<Child />
</t>
</div>`;
static components = { Child };
error = false;
catchError(error) {
this.error = error;
this.render();
}
}
await mount(Parent, { target: fixture });
expect(fixture.innerHTML).toBe("<div>Error</div>");
});
test("errors in mounted and in willUnmount", async () => {
expect.assertions(1);
class Example extends Component {
static template = xml`<div/>`;
val;
mounted() {
throw new Error("Error in mounted");
this.val = { foo: "bar" };
}
willUnmount() {
console.log(this.val.foo);
}
}
try {
await mount(Example, { target: fixture });
} catch (e) {
expect(e.message).toBe("Error in mounted");
}
}); });
}); });
-252
View File
@@ -325,70 +325,6 @@ describe("t-slot directive", () => {
expect(QWeb.slots["1_default"].toString()).toMatchSnapshot(); expect(QWeb.slots["1_default"].toString()).toMatchSnapshot();
}); });
test("slots in t-foreach in t-foreach", async () => {
class Child extends Component {
static template = xml`
<div><t t-slot="default" /></div>
`;
}
class App extends Component {
static template = xml`
<div>
<t t-foreach="tree" t-as="node1" t-key="node1.key">
<div t-esc="node1.value" />
<ul>
<t t-foreach="node1.nodes" t-as="node2" t-key="node2.key">
<Child>
<li t-esc="node1.value" />
</Child>
</t>
</ul>
</t>
</div>`;
static components = { Child };
tree = [
{
key: "a",
value: "A",
nodes: [
{
key: "1",
value: "A-1",
},
{
key: "2",
value: "A-2",
},
],
},
{
key: "b",
value: "B",
nodes: [
{
key: "1",
value: "B-1",
},
{
key: "2",
value: "B-2",
},
],
},
];
}
await mount(App, { target: fixture });
expect(fixture.innerHTML).toBe(
"<div><div>A</div><ul><div><li>A</li></div><div><li>A</li></div></ul><div>B</div><ul><div><li>B</li></div><div><li>B</li></div></ul></div>"
);
expect(env.qweb.templates[App.template].fn.toString()).toMatchSnapshot();
});
test("refs are properly bound in slots", async () => { test("refs are properly bound in slots", async () => {
class Dialog extends Component { class Dialog extends Component {
static template = xml`<span><t t-slot="footer"/></span>`; static template = xml`<span><t t-slot="footer"/></span>`;
@@ -467,48 +403,6 @@ describe("t-slot directive", () => {
expect(QWeb.slots["1_default"].toString()).toMatchSnapshot(); expect(QWeb.slots["1_default"].toString()).toMatchSnapshot();
}); });
test("default slot next to named slot, with default content", async () => {
class Dialog extends Component {
// We're using 2 slots here: a "default" one and a "footer",
// both having default children nodes.
static template = xml`
<div class="Dialog">
<div class="content">
<t t-slot="default">
Default content
</t>
</div>
<div class="footer">
<t t-slot="footer">
Default footer
</t>
</div>
</div>
`;
}
class App extends Component {
// Here we're trying to assign the "footer" slot with some content
static components = { Dialog };
static template = xml`
<div>
<Dialog>
<t t-set-slot="footer">
Overridden footer
</t>
</Dialog>
</div>
`;
}
await mount(App, { target: fixture });
expect(fixture.innerHTML).toBe(
'<div><div class="Dialog"><div class="content"> Default content </div><div class="footer"> Overridden footer </div></div></div>'
);
expect(QWeb.TEMPLATES[App.template].fn.toString()).toMatchSnapshot();
});
test("multiple roots are allowed in a named slot", async () => { test("multiple roots are allowed in a named slot", async () => {
env.qweb.addTemplates(` env.qweb.addTemplates(`
<templates> <templates>
@@ -1224,150 +1118,4 @@ describe("t-slot directive", () => {
expect(env.qweb.templates[Toggler.template].fn.toString()).toMatchSnapshot(); expect(env.qweb.templates[Toggler.template].fn.toString()).toMatchSnapshot();
}); });
test("t-slot within dynamic t-call", async () => {
let child;
class Child extends Component {
static template = xml`<div class="child"/>`;
constructor(...args) {
super(...args);
child = this;
}
}
class Slotted extends Component {
static template = xml`<div class="slotted"><t t-slot="default" /></div>`;
}
class UsingTcallInSlotted extends Component {
tcallTemplate = xml`<div class="slot"><Child/></div>`;
static template = xml`
<div>
<Slotted>
<t t-call="{{ tcallTemplate }}"/>
</Slotted>
</div>`;
static components = { Slotted, Child };
}
await mount(UsingTcallInSlotted, { target: fixture });
expect(child.__owl__.parent).toBeInstanceOf(Slotted);
expect(fixture.innerHTML).toBe(
`<div><div class="slotted"><div class="slot"><div class="child"></div></div></div></div>`
);
});
test("t-slot scope context", async () => {
expect.assertions(1);
class Wrapper extends Component {
static template = xml`<t t-slot="default"/>`;
}
let dialog;
class Dialog extends Component {
static template = xml`
<Wrapper>
<div t-on-click="onClick">
<t t-slot="default" />
</div>
</Wrapper>
`;
static components = { Wrapper };
setup() {
dialog = this;
}
onClick(ev) {
// we do not use expect(this).toBe(dialog) here because if it fails, it
// may blow up jest because it then tries to compute a diff, which is
// infinite if there is a cycle
expect(this === dialog).toBe(true);
}
}
class Parent extends Component {
static template = xml`
<Dialog>
<button>The Button</button>
</Dialog>`;
static components = { Dialog };
}
await mount(Parent, { target: fixture });
document.querySelector("button").click();
await nextTick();
});
test("t-slot in recursive templates", async () => {
QWeb.registerTemplate(
"_test_recursive_template",
`
<Wrapper>
<t t-esc="name" />
<t t-foreach="items" t-as="item">
<t t-if="!item.children.length">
<t t-esc="item.name" />
</t>
<t t-else="" t-call="_test_recursive_template">
<t t-set="name" t-value="item.name" />
<t t-set="items" t-value="item.children" />
</t>
</t>
</Wrapper>`
);
class Wrapper extends Component {
static template = xml`
<wrapper>
<t t-slot="default"/>
</wrapper>`;
}
class Parent extends Component {
static template = "_test_recursive_template";
static components = { Wrapper };
name = "foo";
items = [
{
name: "foo-0",
children: [
{ name: "foo-00", children: [] },
{
name: "foo-01",
children: [
{ name: "foo-010", children: [] },
{ name: "foo-011", children: [] },
{
name: "foo-012",
children: [
{ name: "foo-0120", children: [] },
{ name: "foo-0121", children: [] },
{ name: "foo-0122", children: [] },
],
},
],
},
{ name: "foo-02", children: [] },
],
},
{ name: "foo-1", children: [] },
{ name: "foo-2", children: [] },
];
}
await mount(Parent, { target: fixture });
expect(fixture.innerHTML).toBe(
"<wrapper>foo<wrapper>foo-0foo-00<wrapper>foo-01foo-010foo-011<wrapper>foo-012foo-0120foo-0121foo-0122</wrapper></wrapper>foo-02</wrapper>foo-1foo-2</wrapper>"
);
});
}); });
+4 -133
View File
@@ -323,54 +323,6 @@ describe("unmounting and remounting", () => {
expect(steps).toEqual([2, 2, 3]); expect(steps).toEqual([2, 2, 3]);
}); });
test("change state and render while mounted in detached dom", async () => {
class App extends Component {
static template = xml`<div><t t-esc="state.val"/></div>`;
state = useState({ val: 1 });
}
const detachedDiv = document.createElement("div");
const app = await mount(App, { target: detachedDiv });
expect(detachedDiv.innerHTML).toBe("<div>1</div>");
app.state.val = 2;
await nextTick();
expect(detachedDiv.innerHTML).toBe("<div>2</div>");
});
test("change state and render while not mounted ", async () => {
class App extends Component {
static template = xml`<div><t t-esc="state.val"/></div>`;
state = useState({ val: 1 });
}
const app = new App(null);
app.state.val = 2; // will call the render method (before being mounted)
await nextTick();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>2</div>");
});
test("destroy and change state after mounted in detached dom", async () => {
class App extends Component {
static template = xml`<div><t t-esc="state.val"/></div>`;
state = useState({ val: 1 });
}
const detachedDiv = document.createElement("div");
const app = await mount(App, { target: detachedDiv });
expect(detachedDiv.innerHTML).toBe("<div>1</div>");
app.destroy();
app.state.val = 2;
await nextTick();
expect(detachedDiv.innerHTML).toBe("");
});
test("change state while component is unmounted", async () => { test("change state while component is unmounted", async () => {
let child; let child;
class Child extends Component { class Child extends Component {
@@ -404,40 +356,6 @@ describe("unmounting and remounting", () => {
expect(fixture.innerHTML).toBe("<div>P2<span>C2</span></div>"); expect(fixture.innerHTML).toBe("<div>P2<span>C2</span></div>");
}); });
test("change state while component is mounted in a fragment", async () => {
class Child1 extends Component {
static template = xml`<span>C1</span>`;
}
class Child2 extends Component {
static template = xml`<span>C2</span>`;
}
class Parent extends Component {
static components = { Child1, Child2 };
static template = xml`
<div>
<Child1 t-if="child == 'c1'"/>
<Child2 t-if="child == 'c2'"/>
</div>`;
child: string | false = false;
}
const fragment = document.createDocumentFragment();
const parent = new Parent();
await parent.mount(fragment);
expect(parent.el.outerHTML).toBe("<div></div>");
parent.child = "c1";
parent.render();
await Promise.resolve();
parent.child = "c2";
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>C2</span></div>");
});
test("unmount component during a re-rendering", async () => { test("unmount component during a re-rendering", async () => {
const def = makeDeferred(); const def = makeDeferred();
class Child extends Component { class Child extends Component {
@@ -524,17 +442,17 @@ describe("unmounting and remounting", () => {
// one full tick. // one full tick.
await nextMicroTick(); await nextMicroTick();
await nextMicroTick(); await nextMicroTick();
expect(steps).toEqual([]); expect(steps).toEqual(["1 catch"]);
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe("<div></div><span></span>"); expect(fixture.innerHTML).toBe("<div></div><span></span>");
def.resolve(); def.resolve();
await nextTick(); await nextTick();
expect(steps).toEqual(["2 resolved"]); expect(steps).toEqual(["1 catch", "2 resolved"]);
expect(fixture.innerHTML).toBe("<div></div><span><div>Hey</div></span>"); expect(fixture.innerHTML).toBe("<div></div><span><div>Hey</div></span>");
}); });
test("component can be mounted on same target, another situation", async () => { test("widget can be mounted on same target, another situation", async () => {
const def = makeDeferred(); const def = makeDeferred();
const steps: string[] = []; const steps: string[] = [];
@@ -562,8 +480,8 @@ describe("unmounting and remounting", () => {
def.resolve(); def.resolve();
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe("<div>Hey</div>");
expect(steps).toEqual(["1 resolved", "2 resolved"]); expect(steps).toEqual(["1 resolved", "2 resolved"]);
expect(fixture.innerHTML).toBe("<div>Hey</div>");
}); });
test("mounting a destroyed widget", async () => { test("mounting a destroyed widget", async () => {
@@ -682,51 +600,4 @@ describe("unmounting and remounting", () => {
await parent.render(); await parent.render();
expect(fixture.textContent).toBe("fixedsome text"); expect(fixture.textContent).toBe("fixedsome text");
}); });
test("remounting component tree where a component implement shouldupdate", async () => {
let state: any;
const steps = [];
class Child extends Component {
static template = xml`<div><t t-esc="state.word"/><t t-esc="props.name"/></div>`;
state = useState({ word: "hello" });
constructor(parent, props) {
super(parent, props);
state = this.state;
}
patched() {
steps.push("patched");
}
mounted() {
steps.push("mounted");
}
willUnmount() {
steps.push("willUnmount");
}
shouldUpdate() {
return false;
}
}
class Parent extends Component {
static template = xml`<div><Child name="state.name"/></div>`;
static components = { Child };
state = useState({ name: "World" });
}
const parent = await mount(Parent, { target: fixture });
expect(fixture.innerHTML).toBe("<div><div>helloWorld</div></div>");
parent.unmount();
expect(fixture.innerHTML).toBe("");
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>helloWorld</div></div>");
state.word = "test";
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>testWorld</div></div>");
expect(steps).toEqual(["mounted", "willUnmount", "mounted", "patched"]);
});
}); });
+1 -20
View File
@@ -289,26 +289,7 @@ describe("Context", () => {
expect(testContext.subscriptions.update.length).toBe(0); expect(testContext.subscriptions.update.length).toBe(0);
}); });
test.skip("concurrent renderings", async () => { test("concurrent renderings", async () => {
/**
* Note: this test is interesting, but sadly just an incomplete attempt at
* protecting users against themselves. With the context API, it is not
* possible for the framework to protect completely against crashes. Maybe
* like in this case, when a component is in a simple hierarchy where all
* renderings come from the context changes, but in a real case, where some
* code can trigger a rendering independently, it is insufficient.
*
* The main problem is that the sub component depends on some external state,
* which may be modified, and then incompatible with the component actual
* state (for example, if the sub component has an id key related to some
* object that has been removed from the context).
*
* For now, sadly, the only solution is that components that depends on external
* state should guarantee their own integrity themselves. Then maybe this
* could be solved at the level of a state management solution that has a
* more advanced API, to let components determine if they should be updated
* or not (so, something slightly more advanced that the useStore hook).
*/
const testContext = new Context({ x: { n: 1 }, key: "x" }); const testContext = new Context({ x: { n: 1 }, key: "x" });
const def = makeDeferred(); const def = makeDeferred();
let stateC; let stateC;
+5 -6
View File
@@ -1,4 +1,4 @@
import { Env, Component, STATUS } from "../src/component/component"; import { Env, Component } from "../src/component/component";
import { scheduler } from "../src/component/scheduler"; import { scheduler } from "../src/component/scheduler";
import { EvalContext, QWeb } from "../src/qweb/qweb"; import { EvalContext, QWeb } from "../src/qweb/qweb";
import { CompilationContext } from "../src/qweb/compilation_context"; import { CompilationContext } from "../src/qweb/compilation_context";
@@ -41,10 +41,9 @@ export async function nextTick(): Promise<void> {
await new Promise((resolve) => scheduler.requestAnimationFrame(resolve)); await new Promise((resolve) => scheduler.requestAnimationFrame(resolve));
} }
export async function nextFrame(numberOfAnimationFrames: number = 2): Promise<void> { export async function nextFrame(): Promise<void> {
for (let i = 0; i < numberOfAnimationFrames; i++) { await new Promise((resolve) => scheduler.requestAnimationFrame(resolve));
await new Promise((resolve) => scheduler.requestAnimationFrame(resolve)); await new Promise((resolve) => scheduler.requestAnimationFrame(resolve));
}
} }
export function makeTestFixture() { export function makeTestFixture() {
@@ -93,7 +92,7 @@ export function renderToDOM(
if (!context.__owl__) { if (!context.__owl__) {
// we add `__owl__` to better simulate a component as context. This is // we add `__owl__` to better simulate a component as context. This is
// particularly important for event handlers added with the `t-on` directive. // particularly important for event handlers added with the `t-on` directive.
context.__owl__ = { status: STATUS.MOUNTED }; context.__owl__ = { isMounted: true };
} }
const vnode = qweb.render(template, context, extra); const vnode = qweb.render(template, context, extra);
+6 -28
View File
@@ -9,10 +9,8 @@ import {
onWillPatch, onWillPatch,
onWillStart, onWillStart,
onWillUpdateProps, onWillUpdateProps,
useEnv,
useSubEnv, useSubEnv,
useExternalListener, useExternalListener,
useComponent,
} from "../src/hooks"; } from "../src/hooks";
import { xml } from "../src/tags"; import { xml } from "../src/tags";
@@ -75,6 +73,8 @@ describe("hooks", () => {
} }
const component = new MyComponent(); const component = new MyComponent();
await component.mount(fixture); await component.mount(fixture);
expect(component).not.toHaveProperty("mounted");
expect(component).not.toHaveProperty("willUnmount");
expect(fixture.innerHTML).toBe("<div>hey</div>"); expect(fixture.innerHTML).toBe("<div>hey</div>");
expect(steps).toEqual(["mounted"]); expect(steps).toEqual(["mounted"]);
component.unmount(); component.unmount();
@@ -381,6 +381,8 @@ describe("hooks", () => {
const component = new MyComponent(); const component = new MyComponent();
await component.mount(fixture); await component.mount(fixture);
expect(component).not.toHaveProperty("patched");
expect(component).not.toHaveProperty("willPatch");
expect(steps).toEqual([]); expect(steps).toEqual([]);
expect(fixture.innerHTML).toBe("<div>hey</div>"); expect(fixture.innerHTML).toBe("<div>hey</div>");
@@ -518,19 +520,6 @@ describe("hooks", () => {
}); });
}); });
test("can use useEnv", async () => {
expect.assertions(1);
class TestComponent extends Component {
static template = xml`<div><t t-esc="env.val"/></div>`;
constructor() {
super();
expect(useEnv()).toBe(env);
}
}
const component = new TestComponent();
await component.mount(fixture);
});
test("can use sub env", async () => { test("can use sub env", async () => {
class TestComponent extends Component { class TestComponent extends Component {
static template = xml`<div><t t-esc="env.val"/></div>`; static template = xml`<div><t t-esc="env.val"/></div>`;
@@ -546,19 +535,6 @@ describe("hooks", () => {
expect(component.env).toHaveProperty("val"); expect(component.env).toHaveProperty("val");
}); });
test("can use useComponent", async () => {
expect.assertions(1);
class TestComponent extends Component {
static template = xml`<div></div>`;
constructor() {
super();
expect(useComponent()).toBe(this);
}
}
const component = new TestComponent();
await component.mount(fixture);
});
test("parent and child env", async () => { test("parent and child env", async () => {
class Child extends Component { class Child extends Component {
static template = xml`<div><t t-esc="env.val"/></div>`; static template = xml`<div><t t-esc="env.val"/></div>`;
@@ -626,6 +602,8 @@ describe("hooks", () => {
const app = new App(); const app = new App();
await app.mount(fixture); await app.mount(fixture);
expect(app).not.toHaveProperty("willStart");
expect(app).not.toHaveProperty("willUpdateProps");
expect(fixture.innerHTML).toBe("<div><span>1</span></div>"); expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
// NOTE: 'on2ndStart' appears first in the list even though // NOTE: 'on2ndStart' appears first in the list even though
+1 -2
View File
@@ -546,8 +546,7 @@ describe("Portal: Basic use and DOM placement", () => {
error = e; error = e;
} }
expect(error).toBeDefined(); expect(error).toBeDefined();
const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g; expect(error.message).toBe("Cannot read property 'crash' of undefined");
expect(error.message).toMatch(regexp);
}); });
test("portal manual unmount", async () => { test("portal manual unmount", async () => {
+37 -149
View File
@@ -7,7 +7,7 @@ exports[`attributes class and t-att-class should combine together 1`] = `
let utils = this.constructor.utils; let utils = this.constructor.utils;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let _1 = utils.toClassObj(scope['value']); let _1 = utils.toObj(scope['value']);
Object.assign(_1, {'hello':true}) Object.assign(_1, {'hello':true})
let c3 = [], p3 = {key:3,class:_1}; let c3 = [], p3 = {key:3,class:_1};
let vn3 = h('div', p3, c3); let vn3 = h('div', p3, c3);
@@ -74,7 +74,7 @@ exports[`attributes dynamic class attribute 1`] = `
let utils = this.constructor.utils; let utils = this.constructor.utils;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let _1 = utils.toClassObj(scope['c']); let _1 = utils.toObj(scope['c']);
let c2 = [], p2 = {key:2,class:_1}; let c2 = [], p2 = {key:2,class:_1};
let vn2 = h('div', p2, c2); let vn2 = h('div', p2, c2);
return vn2; return vn2;
@@ -88,7 +88,7 @@ exports[`attributes dynamic empty class attribute 1`] = `
let utils = this.constructor.utils; let utils = this.constructor.utils;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let _1 = utils.toClassObj(scope['c']); let _1 = utils.toObj(scope['c']);
let c2 = [], p2 = {key:2,class:_1}; let c2 = [], p2 = {key:2,class:_1};
let vn2 = h('div', p2, c2); let vn2 = h('div', p2, c2);
return vn2; return vn2;
@@ -195,7 +195,7 @@ exports[`attributes from object variables set previously 1`] = `
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1); let vn1 = h('div', p1, c1);
scope.o = {a:'b'}; scope.o = {a:'b'};
let _2 = utils.toClassObj(scope.o.a); let _2 = utils.toObj(scope.o.a);
let c3 = [], p3 = {key:3,class:_2}; let c3 = [], p3 = {key:3,class:_2};
let vn3 = h('span', p3, c3); let vn3 = h('span', p3, c3);
c1.push(vn3); c1.push(vn3);
@@ -213,7 +213,7 @@ exports[`attributes from variables set previously 1`] = `
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1); let vn1 = h('div', p1, c1);
scope.abc = 'def'; scope.abc = 'def';
let _2 = utils.toClassObj(scope.abc); let _2 = utils.toObj(scope.abc);
let c3 = [], p3 = {key:3,class:_2}; let c3 = [], p3 = {key:3,class:_2};
let vn3 = h('span', p3, c3); let vn3 = h('span', p3, c3);
c1.push(vn3); c1.push(vn3);
@@ -288,7 +288,7 @@ exports[`attributes t-att-class and class should combine together 1`] = `
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let _2 = {'hello':true}; let _2 = {'hello':true};
Object.assign(_2, utils.toClassObj(scope['value'])) Object.assign(_2, utils.toObj(scope['value']))
let c3 = [], p3 = {key:3,class:_2}; let c3 = [], p3 = {key:3,class:_2};
let vn3 = h('div', p3, c3); let vn3 = h('div', p3, c3);
return vn3; return vn3;
@@ -303,7 +303,7 @@ exports[`attributes t-att-class with object 1`] = `
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let _2 = {'static':true}; let _2 = {'static':true};
Object.assign(_2, utils.toClassObj({a:scope['b'],c:scope['d'],e:scope['f']})) Object.assign(_2, utils.toObj({a:scope['b'],c:scope['d'],e:scope['f']}))
let c3 = [], p3 = {key:3,class:_2}; let c3 = [], p3 = {key:3,class:_2};
let vn3 = h('div', p3, c3); let vn3 = h('div', p3, c3);
return vn3; return vn3;
@@ -1073,39 +1073,6 @@ exports[`special cases for some specific html attributes/properties input type=
}" }"
`; `;
exports[`special cases for some specific html attributes/properties select with t-att-value 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let scope = Object.create(context);
let h = this.h;
let expr1 = scope['value'];
let c3 = [], p3 = {key:3,attrs:{value: expr1},props:{value: expr1}};
let vn3 = h('select', p3, c3);
p3.hook = {
create: (_, n) => {
n.elm.value=expr1;
},
};
let _4 = 'potato';
let c5 = [], p5 = {key:5,attrs:{value: _4}};
let vn5 = h('option', p5, c5);
c3.push(vn5);
c5.push({text: \`Potato\`});
let _6 = 'tomato';
let c7 = [], p7 = {key:7,attrs:{value: _6}};
let vn7 = h('option', p7, c7);
c3.push(vn7);
c7.push({text: \`Tomato\`});
let _8 = 'onion';
let c9 = [], p9 = {key:9,attrs:{value: _8}};
let vn9 = h('option', p9, c9);
c3.push(vn9);
c9.push({text: \`Onion\`});
return vn3;
}"
`;
exports[`special cases for some specific html attributes/properties various boolean html attributes 1`] = ` exports[`special cases for some specific html attributes/properties various boolean html attributes 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
@@ -1236,55 +1203,6 @@ exports[`static templates empty div 1`] = `
}" }"
`; `;
exports[`static templates inline template string in t-esc 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let scope = Object.create(context);
let result;
let h = this.h;
let _1 = \`text\`;
if (_1 != null) {
let vn2 = {text: _1};
result = vn2
}
return result;
}"
`;
exports[`static templates inline template string with content in t-esc 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let scope = Object.create(context);
let result;
let h = this.h;
scope.v = 1;
let _1 = \`text\${scope.v}\`;
if (_1 != null) {
let vn2 = {text: _1};
result = vn2
}
return result;
}"
`;
exports[`static templates inline template string with variable in context 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let scope = Object.create(context);
let result;
let h = this.h;
let _1 = \`text \${scope['v']}\`;
if (_1 != null) {
let vn2 = {text: _1};
result = vn2
}
return result;
}"
`;
exports[`static templates properly handle comments 1`] = ` exports[`static templates properly handle comments 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
@@ -1596,7 +1514,6 @@ exports[`t-call (template calling recursive template, part 1 2`] = `
) { ) {
// Template name: \\"recursive\\" // Template name: \\"recursive\\"
let utils = this.constructor.utils; let utils = this.constructor.utils;
let parent = extra.parent;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let c3 = extra.parentNode; let c3 = extra.parentNode;
@@ -1613,7 +1530,7 @@ exports[`t-call (template calling recursive template, part 1 2`] = `
scope = Object.create(scope); scope = Object.create(scope);
scope.__access_mode__ = 'ro'; scope.__access_mode__ = 'ro';
let k7 = \`__7__\${key0}__\`; let k7 = \`__7__\${key0}__\`;
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c4, parent: parent, key: k7})); this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c4, parent: utils.getComponent(context), key: k7}));
scope = _origScope6; scope = _origScope6;
} }
}" }"
@@ -1649,7 +1566,6 @@ exports[`t-call (template calling recursive template, part 2 2`] = `
) { ) {
// Template name: \\"nodeTemplate\\" // Template name: \\"nodeTemplate\\"
let utils = this.constructor.utils; let utils = this.constructor.utils;
let parent = extra.parent;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let c2 = extra.parentNode; let c2 = extra.parentNode;
@@ -1691,7 +1607,7 @@ exports[`t-call (template calling recursive template, part 2 2`] = `
scope[utils.zero] = c__0; scope[utils.zero] = c__0;
} }
let k11 = \`__11__\${key0}__\${key1}__\`; let k11 = \`__11__\${key0}__\${key1}__\`;
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c3, parent: parent, key: k11})); this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c3, parent: utils.getComponent(context), key: k11}));
} }
scope = _origScope10; scope = _origScope10;
} }
@@ -1729,7 +1645,6 @@ exports[`t-call (template calling recursive template, part 3 2`] = `
) { ) {
// Template name: \\"nodeTemplate\\" // Template name: \\"nodeTemplate\\"
let utils = this.constructor.utils; let utils = this.constructor.utils;
let parent = extra.parent;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let c2 = extra.parentNode; let c2 = extra.parentNode;
@@ -1771,7 +1686,7 @@ exports[`t-call (template calling recursive template, part 3 2`] = `
scope[utils.zero] = c__0; scope[utils.zero] = c__0;
} }
let k11 = \`__11__\${key0}__\${key1}__\`; let k11 = \`__11__\${key0}__\${key1}__\`;
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c3, parent: parent, key: k11})); this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c3, parent: utils.getComponent(context), key: k11}));
} }
scope = _origScope10; scope = _origScope10;
} }
@@ -1810,7 +1725,6 @@ exports[`t-call (template calling recursive template, part 4: with t-set recursi
) { ) {
// Template name: \\"nodeTemplate\\" // Template name: \\"nodeTemplate\\"
let utils = this.constructor.utils; let utils = this.constructor.utils;
let parent = extra.parent;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let c2 = extra.parentNode; let c2 = extra.parentNode;
@@ -1857,7 +1771,7 @@ exports[`t-call (template calling recursive template, part 4: with t-set recursi
scope[utils.zero] = c__0; scope[utils.zero] = c__0;
} }
let k11 = \`__11__\${key0}__\${key1}__\`; let k11 = \`__11__\${key0}__\${key1}__\`;
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c3, parent: parent, key: k11})); this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c3, parent: utils.getComponent(context), key: k11}));
} }
scope = _origScope10; scope = _origScope10;
} }
@@ -2813,7 +2727,7 @@ exports[`t-on can bind event handler 1`] = `
let h = this.h; let h = this.h;
let c1 = [], p1 = {key:1,on:{}}; let c1 = [], p1 = {key:1,on:{}};
let vn1 = h('button', p1, c1); let vn1 = h('button', p1, c1);
extra.handlers['click__2__'] = extra.handlers['click__2__'] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['add'](e);}; extra.handlers['click__2__'] = extra.handlers['click__2__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['add'](e);};
p1.on['click'] = extra.handlers['click__2__']; p1.on['click'] = extra.handlers['click__2__'];
c1.push({text: \`Click\`}); c1.push({text: \`Click\`});
return vn1; return vn1;
@@ -2830,7 +2744,7 @@ exports[`t-on can bind handlers with arguments 1`] = `
let c1 = [], p1 = {key:1,on:{}}; let c1 = [], p1 = {key:1,on:{}};
let vn1 = h('button', p1, c1); let vn1 = h('button', p1, c1);
let args2 = [5]; let args2 = [5];
p1.on['click'] = function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['add'](...args2, e);}; p1.on['click'] = function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['add'](...args2, e);};
c1.push({text: \`Click\`}); c1.push({text: \`Click\`});
return vn1; return vn1;
}" }"
@@ -2846,7 +2760,7 @@ exports[`t-on can bind handlers with empty object (with non empty inner string)
let c1 = [], p1 = {key:1,on:{}}; let c1 = [], p1 = {key:1,on:{}};
let vn1 = h('button', p1, c1); let vn1 = h('button', p1, c1);
let args2 = [{}]; let args2 = [{}];
p1.on['click'] = function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['doSomething'](...args2, e);}; p1.on['click'] = function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['doSomething'](...args2, e);};
c1.push({text: \`Click\`}); c1.push({text: \`Click\`});
return vn1; return vn1;
}" }"
@@ -2862,7 +2776,7 @@ exports[`t-on can bind handlers with empty object 1`] = `
let c1 = [], p1 = {key:1,on:{}}; let c1 = [], p1 = {key:1,on:{}};
let vn1 = h('button', p1, c1); let vn1 = h('button', p1, c1);
let args2 = [{}]; let args2 = [{}];
p1.on['click'] = function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['doSomething'](...args2, e);}; p1.on['click'] = function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['doSomething'](...args2, e);};
c1.push({text: \`Click\`}); c1.push({text: \`Click\`});
return vn1; return vn1;
}" }"
@@ -2901,7 +2815,7 @@ exports[`t-on can bind handlers with loop variable as argument 1`] = `
let vn7 = h('a', p7, c7); let vn7 = h('a', p7, c7);
c6.push(vn7); c6.push(vn7);
let args8 = [scope['action']]; let args8 = [scope['action']];
p7.on['click'] = function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['activate'](...args8, e);}; p7.on['click'] = function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['activate'](...args8, e);};
c7.push({text: \`link\`}); c7.push({text: \`link\`});
} }
scope = _origScope5; scope = _origScope5;
@@ -2919,7 +2833,7 @@ exports[`t-on can bind handlers with object arguments 1`] = `
let c1 = [], p1 = {key:1,on:{}}; let c1 = [], p1 = {key:1,on:{}};
let vn1 = h('button', p1, c1); let vn1 = h('button', p1, c1);
let args2 = [{val:5}]; let args2 = [{val:5}];
p1.on['click'] = function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['add'](...args2, e);}; p1.on['click'] = function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['add'](...args2, e);};
c1.push({text: \`Click\`}); c1.push({text: \`Click\`});
return vn1; return vn1;
}" }"
@@ -2933,9 +2847,9 @@ exports[`t-on can bind two event handlers 1`] = `
let h = this.h; let h = this.h;
let c1 = [], p1 = {key:1,on:{}}; let c1 = [], p1 = {key:1,on:{}};
let vn1 = h('button', p1, c1); let vn1 = h('button', p1, c1);
extra.handlers['click__2__'] = extra.handlers['click__2__'] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['handleClick'](e);}; extra.handlers['click__2__'] = extra.handlers['click__2__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['handleClick'](e);};
p1.on['click'] = extra.handlers['click__2__']; p1.on['click'] = extra.handlers['click__2__'];
extra.handlers['dblclick__3__'] = extra.handlers['dblclick__3__'] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['handleDblClick'](e);}; extra.handlers['dblclick__3__'] = extra.handlers['dblclick__3__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['handleDblClick'](e);};
p1.on['dblclick'] = extra.handlers['dblclick__3__']; p1.on['dblclick'] = extra.handlers['dblclick__3__'];
c1.push({text: \`Click\`}); c1.push({text: \`Click\`});
return vn1; return vn1;
@@ -2950,7 +2864,7 @@ exports[`t-on handler is bound to proper owner 1`] = `
let h = this.h; let h = this.h;
let c1 = [], p1 = {key:1,on:{}}; let c1 = [], p1 = {key:1,on:{}};
let vn1 = h('button', p1, c1); let vn1 = h('button', p1, c1);
extra.handlers['click__2__'] = extra.handlers['click__2__'] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['add'](e);}; extra.handlers['click__2__'] = extra.handlers['click__2__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['add'](e);};
p1.on['click'] = extra.handlers['click__2__']; p1.on['click'] = extra.handlers['click__2__'];
c1.push({text: \`Click\`}); c1.push({text: \`Click\`});
return vn1; return vn1;
@@ -2969,7 +2883,7 @@ exports[`t-on t-on combined with t-esc 1`] = `
let c2 = [], p2 = {key:2,on:{}}; let c2 = [], p2 = {key:2,on:{}};
let vn2 = h('button', p2, c2); let vn2 = h('button', p2, c2);
c1.push(vn2); c1.push(vn2);
extra.handlers['click__3__'] = extra.handlers['click__3__'] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['onClick'](e);}; extra.handlers['click__3__'] = extra.handlers['click__3__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['onClick'](e);};
p2.on['click'] = extra.handlers['click__3__']; p2.on['click'] = extra.handlers['click__3__'];
let _4 = scope['text']; let _4 = scope['text'];
if (_4 != null) { if (_4 != null) {
@@ -2991,7 +2905,7 @@ exports[`t-on t-on combined with t-raw 1`] = `
let c2 = [], p2 = {key:2,on:{}}; let c2 = [], p2 = {key:2,on:{}};
let vn2 = h('button', p2, c2); let vn2 = h('button', p2, c2);
c1.push(vn2); c1.push(vn2);
extra.handlers['click__3__'] = extra.handlers['click__3__'] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['onClick'](e);}; extra.handlers['click__3__'] = extra.handlers['click__3__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['onClick'](e);};
p2.on['click'] = extra.handlers['click__3__']; p2.on['click'] = extra.handlers['click__3__'];
let _4 = scope['html']; let _4 = scope['html'];
if (_4 != null) { if (_4 != null) {
@@ -3009,12 +2923,12 @@ exports[`t-on t-on with .capture modifier 1`] = `
let h = this.h; let h = this.h;
let c1 = [], p1 = {key:1,on:{}}; let c1 = [], p1 = {key:1,on:{}};
let vn1 = h('div', p1, c1); let vn1 = h('div', p1, c1);
extra.handlers['!click__2__'] = extra.handlers['!click__2__'] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['onCapture'](e);}; extra.handlers['!click__2__'] = extra.handlers['!click__2__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['onCapture'](e);};
p1.on['!click'] = extra.handlers['!click__2__']; p1.on['!click'] = extra.handlers['!click__2__'];
let c3 = [], p3 = {key:3,on:{}}; let c3 = [], p3 = {key:3,on:{}};
let vn3 = h('button', p3, c3); let vn3 = h('button', p3, c3);
c1.push(vn3); c1.push(vn3);
extra.handlers['click__4__'] = extra.handlers['click__4__'] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['doSomething'](e);}; extra.handlers['click__4__'] = extra.handlers['click__4__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['doSomething'](e);};
p3.on['click'] = extra.handlers['click__4__']; p3.on['click'] = extra.handlers['click__4__'];
c3.push({text: \`Button\`}); c3.push({text: \`Button\`});
return vn1; return vn1;
@@ -3032,7 +2946,7 @@ exports[`t-on t-on with empty handler (only modifiers) 1`] = `
let c2 = [], p2 = {key:2,on:{}}; let c2 = [], p2 = {key:2,on:{}};
let vn2 = h('button', p2, c2); let vn2 = h('button', p2, c2);
c1.push(vn2); c1.push(vn2);
p2.on['click'] = function (e) {if (context.__owl__.status === 5){return}e.preventDefault();const res = (() => { return })(); if (typeof res === 'function') { res(e) }}; p2.on['click'] = function (e) {if (!context.__owl__.isMounted){return}e.preventDefault();};
c2.push({text: \`Button\`}); c2.push({text: \`Button\`});
return vn1; return vn1;
}" }"
@@ -3047,7 +2961,7 @@ exports[`t-on t-on with inline statement (function call) 1`] = `
let c1 = [], p1 = {key:1,on:{}}; let c1 = [], p1 = {key:1,on:{}};
let vn1 = h('button', p1, c1); let vn1 = h('button', p1, c1);
const state_2 = scope['state']; const state_2 = scope['state'];
p1.on['click'] = function (e) {if (context.__owl__.status === 5){return}const res = (() => { return state_2.incrementCounter(2) })(); if (typeof res === 'function') { res(e) }}; p1.on['click'] = function (e) {if (!context.__owl__.isMounted){return}state_2.incrementCounter(2)};
c1.push({text: \`Click\`}); c1.push({text: \`Click\`});
return vn1; return vn1;
}" }"
@@ -3062,7 +2976,7 @@ exports[`t-on t-on with inline statement 1`] = `
let c1 = [], p1 = {key:1,on:{}}; let c1 = [], p1 = {key:1,on:{}};
let vn1 = h('button', p1, c1); let vn1 = h('button', p1, c1);
const state_2 = scope['state']; const state_2 = scope['state'];
p1.on['click'] = function (e) {if (context.__owl__.status === 5){return}const res = (() => { return state_2.counter++ })(); if (typeof res === 'function') { res(e) }}; p1.on['click'] = function (e) {if (!context.__owl__.isMounted){return}state_2.counter++};
c1.push({text: \`Click\`}); c1.push({text: \`Click\`});
return vn1; return vn1;
}" }"
@@ -3077,7 +2991,7 @@ exports[`t-on t-on with inline statement, part 2 1`] = `
let c1 = [], p1 = {key:1,on:{}}; let c1 = [], p1 = {key:1,on:{}};
let vn1 = h('button', p1, c1); let vn1 = h('button', p1, c1);
const state_2 = scope['state']; const state_2 = scope['state'];
p1.on['click'] = function (e) {if (context.__owl__.status === 5){return}const res = (() => { return state_2.flag=!state_2.flag })(); if (typeof res === 'function') { res(e) }}; p1.on['click'] = function (e) {if (!context.__owl__.isMounted){return}state_2.flag=!state_2.flag};
c1.push({text: \`Toggle\`}); c1.push({text: \`Toggle\`});
return vn1; return vn1;
}" }"
@@ -3093,7 +3007,7 @@ exports[`t-on t-on with inline statement, part 3 1`] = `
let vn1 = h('button', p1, c1); let vn1 = h('button', p1, c1);
const state_2 = scope['state']; const state_2 = scope['state'];
const someFunction_2 = scope['someFunction']; const someFunction_2 = scope['someFunction'];
p1.on['click'] = function (e) {if (context.__owl__.status === 5){return}const res = (() => { return state_2.n=someFunction_2(3) })(); if (typeof res === 'function') { res(e) }}; p1.on['click'] = function (e) {if (!context.__owl__.isMounted){return}state_2.n=someFunction_2(3)};
c1.push({text: \`Toggle\`}); c1.push({text: \`Toggle\`});
return vn1; return vn1;
}" }"
@@ -3110,7 +3024,7 @@ exports[`t-on t-on with prevent and self modifiers (order matters) 1`] = `
let c2 = [], p2 = {key:2,on:{}}; let c2 = [], p2 = {key:2,on:{}};
let vn2 = h('button', p2, c2); let vn2 = h('button', p2, c2);
c1.push(vn2); c1.push(vn2);
extra.handlers['click__3__'] = extra.handlers['click__3__'] || function (e) {if (context.__owl__.status === 5){return}e.preventDefault();if (e.target !== this.elm) {return}utils.getComponent(context)['onClick'](e);}; extra.handlers['click__3__'] = extra.handlers['click__3__'] || function (e) {if (!context.__owl__.isMounted){return}e.preventDefault();if (e.target !== this.elm) {return}utils.getComponent(context)['onClick'](e);};
p2.on['click'] = extra.handlers['click__3__']; p2.on['click'] = extra.handlers['click__3__'];
let c4 = [], p4 = {key:4}; let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4); let vn4 = h('span', p4, c4);
@@ -3131,19 +3045,19 @@ exports[`t-on t-on with prevent and/or stop modifiers 1`] = `
let c2 = [], p2 = {key:2,on:{}}; let c2 = [], p2 = {key:2,on:{}};
let vn2 = h('button', p2, c2); let vn2 = h('button', p2, c2);
c1.push(vn2); c1.push(vn2);
extra.handlers['click__3__'] = extra.handlers['click__3__'] || function (e) {if (context.__owl__.status === 5){return}e.preventDefault();utils.getComponent(context)['onClickPrevented'](e);}; extra.handlers['click__3__'] = extra.handlers['click__3__'] || function (e) {if (!context.__owl__.isMounted){return}e.preventDefault();utils.getComponent(context)['onClickPrevented'](e);};
p2.on['click'] = extra.handlers['click__3__']; p2.on['click'] = extra.handlers['click__3__'];
c2.push({text: \`Button 1\`}); c2.push({text: \`Button 1\`});
let c4 = [], p4 = {key:4,on:{}}; let c4 = [], p4 = {key:4,on:{}};
let vn4 = h('button', p4, c4); let vn4 = h('button', p4, c4);
c1.push(vn4); c1.push(vn4);
extra.handlers['click__5__'] = extra.handlers['click__5__'] || function (e) {if (context.__owl__.status === 5){return}e.stopPropagation();utils.getComponent(context)['onClickStopped'](e);}; extra.handlers['click__5__'] = extra.handlers['click__5__'] || function (e) {if (!context.__owl__.isMounted){return}e.stopPropagation();utils.getComponent(context)['onClickStopped'](e);};
p4.on['click'] = extra.handlers['click__5__']; p4.on['click'] = extra.handlers['click__5__'];
c4.push({text: \`Button 2\`}); c4.push({text: \`Button 2\`});
let c6 = [], p6 = {key:6,on:{}}; let c6 = [], p6 = {key:6,on:{}};
let vn6 = h('button', p6, c6); let vn6 = h('button', p6, c6);
c1.push(vn6); c1.push(vn6);
extra.handlers['click__7__'] = extra.handlers['click__7__'] || function (e) {if (context.__owl__.status === 5){return}e.preventDefault();e.stopPropagation();utils.getComponent(context)['onClickPreventedAndStopped'](e);}; extra.handlers['click__7__'] = extra.handlers['click__7__'] || function (e) {if (!context.__owl__.isMounted){return}e.preventDefault();e.stopPropagation();utils.getComponent(context)['onClickPreventedAndStopped'](e);};
p6.on['click'] = extra.handlers['click__7__']; p6.on['click'] = extra.handlers['click__7__'];
c6.push({text: \`Button 3\`}); c6.push({text: \`Button 3\`});
return vn1; return vn1;
@@ -3183,7 +3097,7 @@ exports[`t-on t-on with prevent modifier in t-foreach 1`] = `
let vn7 = h('a', p7, c7); let vn7 = h('a', p7, c7);
c1.push(vn7); c1.push(vn7);
let args8 = [scope['project'].id]; let args8 = [scope['project'].id];
p7.on['click'] = function (e) {if (context.__owl__.status === 5){return}e.preventDefault();utils.getComponent(context)['onEdit'](...args8, e);}; p7.on['click'] = function (e) {if (!context.__owl__.isMounted){return}e.preventDefault();utils.getComponent(context)['onEdit'](...args8, e);};
c7.push({text: \` Edit \`}); c7.push({text: \` Edit \`});
let _9 = scope['project'].name; let _9 = scope['project'].name;
if (_9 != null) { if (_9 != null) {
@@ -3207,7 +3121,7 @@ exports[`t-on t-on with self and prevent modifiers (order matters) 1`] = `
let c2 = [], p2 = {key:2,on:{}}; let c2 = [], p2 = {key:2,on:{}};
let vn2 = h('button', p2, c2); let vn2 = h('button', p2, c2);
c1.push(vn2); c1.push(vn2);
extra.handlers['click__3__'] = extra.handlers['click__3__'] || function (e) {if (context.__owl__.status === 5){return}if (e.target !== this.elm) {return}e.preventDefault();utils.getComponent(context)['onClick'](e);}; extra.handlers['click__3__'] = extra.handlers['click__3__'] || function (e) {if (!context.__owl__.isMounted){return}if (e.target !== this.elm) {return}e.preventDefault();utils.getComponent(context)['onClick'](e);};
p2.on['click'] = extra.handlers['click__3__']; p2.on['click'] = extra.handlers['click__3__'];
let c4 = [], p4 = {key:4}; let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4); let vn4 = h('span', p4, c4);
@@ -3228,7 +3142,7 @@ exports[`t-on t-on with self modifier 1`] = `
let c2 = [], p2 = {key:2,on:{}}; let c2 = [], p2 = {key:2,on:{}};
let vn2 = h('button', p2, c2); let vn2 = h('button', p2, c2);
c1.push(vn2); c1.push(vn2);
extra.handlers['click__3__'] = extra.handlers['click__3__'] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['onClick'](e);}; extra.handlers['click__3__'] = extra.handlers['click__3__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['onClick'](e);};
p2.on['click'] = extra.handlers['click__3__']; p2.on['click'] = extra.handlers['click__3__'];
let c4 = [], p4 = {key:4}; let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4); let vn4 = h('span', p4, c4);
@@ -3237,7 +3151,7 @@ exports[`t-on t-on with self modifier 1`] = `
let c5 = [], p5 = {key:5,on:{}}; let c5 = [], p5 = {key:5,on:{}};
let vn5 = h('button', p5, c5); let vn5 = h('button', p5, c5);
c1.push(vn5); c1.push(vn5);
extra.handlers['click__6__'] = extra.handlers['click__6__'] || function (e) {if (context.__owl__.status === 5){return}if (e.target !== this.elm) {return}utils.getComponent(context)['onClickSelf'](e);}; extra.handlers['click__6__'] = extra.handlers['click__6__'] || function (e) {if (!context.__owl__.isMounted){return}if (e.target !== this.elm) {return}utils.getComponent(context)['onClickSelf'](e);};
p5.on['click'] = extra.handlers['click__6__']; p5.on['click'] = extra.handlers['click__6__'];
let c7 = [], p7 = {key:7}; let c7 = [], p7 = {key:7};
let vn7 = h('span', p7, c7); let vn7 = h('span', p7, c7);
@@ -3943,20 +3857,6 @@ exports[`t-set value priority 1`] = `
}" }"
`; `;
exports[`translation support can add additional attributes to the list of translatable attributes 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let h = this.h;
let _1 = 'word';
let _2 = 'mot';
let c3 = [], p3 = {key:3,attrs:{tomato: _1,potato: _2}};
let vn3 = h('div', p3, c3);
c3.push({text: \`text\`});
return vn3;
}"
`;
exports[`translation support can translate node content 1`] = ` exports[`translation support can translate node content 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
@@ -4024,18 +3924,6 @@ exports[`translation support some attributes are translated 1`] = `
}" }"
`; `;
exports[`translation support translation is done on the trimmed text, with extra spaces readded after 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
c1.push({text: \` mot \`});
return vn1;
}"
`;
exports[`whitespace handling consecutives whitespaces are condensed into a single space 1`] = ` exports[`whitespace handling consecutives whitespaces are condensed into a single space 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
@@ -1,57 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`qweb t-att t-att-class with multiple classes 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let _1 = utils.toClassObj({'a b c':scope['value']});
let c2 = [], p2 = {key:2,class:_1};
let vn2 = h('div', p2, c2);
return vn2;
}"
`;
exports[`qweb t-att t-att-class with multiple classes 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let _3 = utils.toClassObj({['a b c']:scope['value']});
let c4 = [], p4 = {key:4,class:_3};
let vn4 = h('div', p4, c4);
return vn4;
}"
`;
exports[`qweb t-att t-att-class with multiple classes, some of which are duplicate 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let _1 = utils.toClassObj({'a b c':scope['value'],'a b d':!scope['value']});
let c2 = [], p2 = {key:2,class:_1};
let vn2 = h('div', p2, c2);
return vn2;
}"
`;
exports[`qweb t-att t-att-class with multiple classes, some of which are duplicate 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let _3 = utils.toClassObj({'a b c':scope['value'],'a b d':!scope['value']});
let c4 = [], p4 = {key:4,class:_3};
let vn4 = h('div', p4, c4);
return vn4;
}"
`;
@@ -1,71 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`qweb t-tag simple usecases 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let scope = Object.create(context);
let result;
let h = this.h;
let c1 = [], p1 = {key:1};
let tag2 = 'div';
let vn1 = h(tag2, p1, c1);
result = vn1;
return result;
}"
`;
exports[`qweb t-tag simple usecases 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let scope = Object.create(context);
let result;
let h = this.h;
let c3 = [], p3 = {key:3};
let tag4 = scope['tag'];
let vn3 = h(tag4, p3, c3);
result = vn3;
c3.push({text: \`text\`});
return result;
}"
`;
exports[`qweb t-tag with multiple attributes 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let scope = Object.create(context);
let result;
let h = this.h;
let _2 = {'blueberry':true};
let _3 = 'raspberry';
let c4 = [], p4 = {key:4,attrs:{taste: _3},class:_2};
let tag5 = scope['tag'];
let vn4 = h(tag5, p4, c4);
result = vn4;
c4.push({text: \`gooseberry\`});
return result;
}"
`;
exports[`qweb t-tag with multiple child nodes 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let scope = Object.create(context);
let result;
let h = this.h;
let c1 = [], p1 = {key:1};
let tag2 = scope['tag'];
let vn1 = h(tag2, p1, c1);
result = vn1;
c1.push({text: \` pear \`});
let c3 = [], p3 = {key:3};
let vn3 = h('span', p3, c3);
c1.push(vn3);
c3.push({text: \`apple\`});
c1.push({text: \` strawberry \`});
return result;
}"
`;
+4 -84
View File
@@ -1,5 +1,4 @@
import { QWeb } from "../../src/qweb/index"; import { QWeb } from "../../src/qweb/index";
import { config } from "../../src/index";
import { nextTick, normalize, renderToDOM, renderToString, trim } from "../helpers"; import { nextTick, normalize, renderToDOM, renderToString, trim } from "../helpers";
import { patch } from "../../src/vdom"; import { patch } from "../../src/vdom";
@@ -14,7 +13,6 @@ let qweb: QWeb;
beforeEach(() => { beforeEach(() => {
QWeb.TEMPLATES = {}; QWeb.TEMPLATES = {};
QWeb.nextId = 1;
qweb = new QWeb(); qweb = new QWeb();
}); });
@@ -33,21 +31,6 @@ describe("static templates", () => {
expect(renderToString(qweb, "test", { text: "hello vdom" })).toBe("hello vdom"); expect(renderToString(qweb, "test", { text: "hello vdom" })).toBe("hello vdom");
}); });
test("inline template string in t-esc", () => {
qweb.addTemplate("test", '<t><t t-esc="`text`"/></t>');
expect(renderToString(qweb, "test")).toBe("text");
});
test("inline template string with content in t-esc", () => {
qweb.addTemplate("test", '<t><t t-set="v" t-value="1"/><t t-esc="`text${v}`"/></t>');
expect(renderToString(qweb, "test")).toBe("text1");
});
test("inline template string with variable in context", () => {
qweb.addTemplate("test", '<t><t t-esc="`text ${v}`"/></t>');
expect(renderToString(qweb, "test", { v: "from context" })).toBe("text from context");
});
test("simple string, with some dynamic value", () => { test("simple string, with some dynamic value", () => {
qweb.addTemplate("test", '<t>hello <t t-esc="text"/></t>'); qweb.addTemplate("test", '<t>hello <t t-esc="text"/></t>');
expect(renderToString(qweb, "test", { text: "vdom" })).toBe("hello vdom"); expect(renderToString(qweb, "test", { text: "vdom" })).toBe("hello vdom");
@@ -1864,7 +1847,8 @@ describe("t-ref", () => {
describe("loading templates", () => { describe("loading templates", () => {
test("can initialize qweb with a string", () => { test("can initialize qweb with a string", () => {
const templates = `<?xml version="1.0" encoding="UTF-8"?> const templates = `
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve"> <templates id="template" xml:space="preserve">
<div t-name="hey">jupiler</div> <div t-name="hey">jupiler</div>
</templates>`; </templates>`;
@@ -1873,7 +1857,8 @@ describe("loading templates", () => {
}); });
test("can load a few templates from a xml string", () => { test("can load a few templates from a xml string", () => {
const data = `<?xml version="1.0" encoding="UTF-8"?> const data = `
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve"> <templates id="template" xml:space="preserve">
<t t-name="items"><li>ok</li><li>foo</li></t> <t t-name="items"><li>ok</li><li>foo</li></t>
@@ -1946,49 +1931,6 @@ describe("special cases for some specific html attributes/properties", () => {
let elm = vnode2.elm as HTMLInputElement; let elm = vnode2.elm as HTMLInputElement;
expect(elm.indeterminate).toBe(true); expect(elm.indeterminate).toBe(true);
}); });
test("textarea with t-att-value", () => {
// render input with initial value
qweb.addTemplate("test", `<textarea t-att-value="v"/>`);
const vnode1 = qweb.render("test", { v: "zucchini" });
const vnode2 = patch(document.createElement("textarea"), vnode1);
let elm = vnode2.elm as HTMLInputElement;
expect(elm.value).toBe("zucchini");
// change value manually in textarea, to simulate user textarea
elm.value = "tomato";
expect(elm.value).toBe("tomato");
// rerender with a different value, and patch actual dom, to check that
// textarea value was properly reset by owl
const vnode3 = qweb.render("test", { v: "potato" });
patch(vnode2, vnode3);
expect(elm.value).toBe("potato");
});
test("select with t-att-value", () => {
const template = `
<select t-att-value="value">
<option value="potato">Potato</option>
<option value="tomato">Tomato</option>
<option value="onion">Onion</option>
</select>`;
qweb.addTemplate("test", template);
const vnode1 = qweb.render("test", { value: "tomato" });
const vnode2 = patch(document.createElement("select"), vnode1);
let elm = vnode2.elm as HTMLSelectElement;
expect(elm.value).toBe("tomato");
elm.value = "potato";
expect(elm.value).toBe("potato");
// rerender with a different value, and patch actual dom, to check that
// select value was properly reset by owl
const vnode3 = qweb.render("test", { value: "onion" });
patch(vnode2, vnode3);
expect(elm.value).toBe("onion");
expect(qweb.templates.test.fn.toString()).toMatchSnapshot();
});
}); });
describe("whitespace handling", () => { describe("whitespace handling", () => {
@@ -2212,28 +2154,6 @@ describe("translation support", () => {
'<div><p label="mot">mot</p><p title="mot">mot</p><p placeholder="mot">mot</p><p alt="mot">mot</p><p something="word">mot</p></div>' '<div><p label="mot">mot</p><p title="mot">mot</p><p placeholder="mot">mot</p><p alt="mot">mot</p><p something="word">mot</p></div>'
); );
}); });
test("can add additional attributes to the list of translatable attributes", () => {
const translations = {
word: "mot",
};
const translateFn = (expr) => translations[expr] || expr;
const qweb = new QWeb({ translateFn });
config.translatableAttributes.push("potato");
qweb.addTemplate("test", `<div tomato="word" potato="word">text</div>`);
expect(renderToString(qweb, "test")).toBe('<div tomato="word" potato="mot">text</div>');
});
test("translation is done on the trimmed text, with extra spaces readded after", () => {
const translations = {
word: "mot",
};
const translateFn = jest.fn((expr) => translations[expr] || expr);
const qweb = new QWeb({ translateFn });
qweb.addTemplate("test", "<div> word </div>");
expect(renderToString(qweb, "test")).toBe("<div> mot </div>");
expect(translateFn).toHaveBeenCalledWith("word");
});
}); });
describe("t-key tests", () => { describe("t-key tests", () => {
-27
View File
@@ -196,31 +196,4 @@ describe("expression evaluation", () => {
expect(compileExpr("f(...state.list)", {})).toBe("scope['f'](...scope['state'].list)"); expect(compileExpr("f(...state.list)", {})).toBe("scope['f'](...scope['state'].list)");
expect(compileExpr("f([...list])", {})).toBe("scope['f']([...scope['list']])"); expect(compileExpr("f([...list])", {})).toBe("scope['f']([...scope['list']])");
}); });
test("works with builtin properties", () => {
expect(compileExpr("state.constructor.name", {})).toBe("scope['state'].constructor.name");
});
test("works with shortcut object key description", () => {
expect(compileExpr("{a}", {})).toBe("{a:scope['a']}");
expect(compileExpr("{a,b}", {})).toBe("{a:scope['a'],b:scope['b']}");
expect(compileExpr("{a,b:3,c}", {})).toBe("{a:scope['a'],b:3,c:scope['c']}");
});
test("works with short object description and lists ", () => {
expect(compileExpr("[a, b]", {})).toBe("[scope['a'],scope['b']]");
expect(compileExpr("[a, b, c]", {})).toBe("[scope['a'],scope['b'],scope['c']]");
expect(compileExpr("[a, {b, c},d]", {})).toBe(
"[scope['a'],{b:scope['b'],c:scope['c']},scope['d']]"
);
expect(compileExpr("{a:[b, {c, d: e}]}", {})).toBe(
"{a:[scope['b'],{c:scope['c'],d:scope['e']}]}"
);
});
test("template strings", () => {
expect(compileExpr("`hey`", {})).toBe("`hey`");
expect(compileExpr("`hey ${you}`", {})).toBe("`hey ${scope['you']}`");
expect(compileExpr("`hey ${1 + 2}`", {})).toBe("`hey ${1+2}`");
});
}); });
-36
View File
@@ -1,36 +0,0 @@
import { QWeb } from "../../src/qweb/index";
import { renderToString } from "../helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
function render(template, context = {}) {
const qweb = new QWeb();
qweb.addTemplate("test", template);
return renderToString(qweb, "test", context);
}
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("qweb t-att", () => {
test("t-att-class with multiple classes", () => {
expect(render(`<div t-att-class="{'a b c': value}" />`, { value: true })).toBe(
'<div class="a b c"></div>'
);
expect(render(`<div t-att-class="{['a b c']: value}" />`, { value: true })).toBe(
'<div class="a b c"></div>'
);
});
test("t-att-class with multiple classes, some of which are duplicate", () => {
expect(render(`<div t-att-class="{'a b c': value, 'a b d': !value}" />`, { value: true })).toBe(
'<div class="a b c"></div>'
);
expect(
render(`<div t-att-class="{'a b c': value, 'a b d': !value}" />`, { value: false })
).toBe('<div class="a b d"></div>');
});
});
-42
View File
@@ -1,42 +0,0 @@
import { QWeb } from "../../src/qweb/index";
import { renderToString } from "../helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
function render(template, context = {}) {
const qweb = new QWeb();
qweb.addTemplate("test", template);
return renderToString(qweb, "test", context);
}
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("qweb t-tag", () => {
test("simple usecases", () => {
expect(render(`<t t-tag="'div'"></t>`)).toBe("<div></div>");
expect(render(`<t t-tag="tag">text</t>`, { tag: "span" })).toBe("<span>text</span>");
});
test("with multiple child nodes", () => {
const template = `
<t t-tag="tag">
pear
<span>apple</span>
strawberry
</t>`;
expect(render(template, { tag: "div" })).toBe(
"<div> pear <span>apple</span> strawberry </div>"
);
});
test("with multiple attributes", () => {
const template = `
<t t-tag="tag" class="blueberry" taste="raspberry">gooseberry</t>`;
const expected = `<div taste=\"raspberry\" class=\"blueberry\">gooseberry</div>`;
expect(render(template, { tag: "div" })).toBe(expected);
});
});
+2 -2
View File
@@ -7,11 +7,11 @@ exports[`Link component can render simple cases 1`] = `
let utils = this.constructor.utils; let utils = this.constructor.utils;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let _5 = utils.toClassObj({'router-link-active':scope['isActive']}); let _5 = utils.toObj({'router-link-active':scope['isActive']});
let _6 = scope['href']; let _6 = scope['href'];
let c7 = [], p7 = {key:7,attrs:{href: _6},class:_5,on:{}}; let c7 = [], p7 = {key:7,attrs:{href: _6},class:_5,on:{}};
let vn7 = h('a', p7, c7); let vn7 = h('a', p7, c7);
extra.handlers['click__8__'] = extra.handlers['click__8__'] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['navigate'](e);}; extra.handlers['click__8__'] = extra.handlers['click__8__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['navigate'](e);};
p7.on['click'] = extra.handlers['click__8__']; p7.on['click'] = extra.handlers['click__8__'];
const slot9 = this.constructor.slots[context.__owl__.slotId + '_' + 'default']; const slot9 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot9) { if (slot9) {
@@ -18,7 +18,7 @@ exports[`RouteComponent can render simple cases 1`] = `
let w4 = k5 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k5]] : false; let w4 = k5 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k5]] : false;
let vn6 = {}; let vn6 = {};
result = vn6; result = vn6;
let props4 = Object.assign({}, scope['env'].router.currentParams, {}); let props4 = Object.assign({}, scope['env'].router.currentParams);
if (w4 && w4.__owl__.currentFiber && !w4.__owl__.vnode) { if (w4 && w4.__owl__.currentFiber && !w4.__owl__.vnode) {
w4.destroy(); w4.destroy();
w4 = false; w4 = false;
@@ -29,7 +29,7 @@ exports[`RouteComponent can render simple cases 1`] = `
utils.defineProxy(vn6, pvnode); utils.defineProxy(vn6, pvnode);
} else { } else {
let componentKey4 = \`routeComponent\`; let componentKey4 = \`routeComponent\`;
let W4 = scope['routeComponent'] || context.constructor.components[componentKey4] || QWeb.components[componentKey4]; let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4]|| scope['routeComponent'];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')} if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(parent, props4); w4 = new W4(parent, props4);
parent.__owl__.cmap[k5] = w4.__owl__.id; parent.__owl__.cmap[k5] = w4.__owl__.id;
+1 -1
View File
@@ -80,7 +80,7 @@ describe("Link component", () => {
await app.mount(fixture); await app.mount(fixture);
expect(window.location.pathname).toBe("/users"); expect(window.location.pathname).toBe("/users");
var evt = new MouseEvent("contextmenu", { var evt = new MouseEvent("click", {
button: 1, button: 1,
}); });
-24
View File
@@ -103,28 +103,4 @@ describe("RouteComponent", () => {
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>Book 1984|124</span></div>"); expect(fixture.innerHTML).toBe("<div><span>Book 1984|124</span></div>");
}); });
test("can render parameterized route where params are not separated by slashes", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="App">
<RouteComponent />
</div>
<span t-name="Book">Book <t t-esc="props.title"/>|<t t-esc="props.val"/></span>
</templates>
`);
class Book extends Component {}
class App extends Component {
static components = { RouteComponent };
}
const routes = [
{ name: "book", path: "/#title={{title}}&val={{val.number}}", component: Book },
];
router = new TestRouter(env, routes, { mode: "hash" });
await router.navigate({ to: "book", params: { title: "1984", val: "123" } });
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>Book 1984|123</span></div>");
});
}); });
+23 -39
View File
@@ -1,6 +1,6 @@
import { Destination, RouterEnv, Route } from "../../src/router/router"; import { Destination, RouterEnv, Route } from "../../src/router/router";
import { makeTestEnv, nextTick } from "../helpers"; import { makeTestEnv, nextTick } from "../helpers";
import { TestRouter, getRouteParams } from "./test_router"; import { TestRouter } from "./test_router";
let env: RouterEnv; let env: RouterEnv;
let router: TestRouter | null = null; let router: TestRouter | null = null;
@@ -60,13 +60,6 @@ describe("router miscellaneous", () => {
await router.navigate({ to: "users", params: { id: 3 } }); await router.navigate({ to: "users", params: { id: 3 } });
expect(window.location.href).toBe("http://localhost/test.html#/users/3"); expect(window.location.href).toBe("http://localhost/test.html#/users/3");
}); });
test("navigate using path and query string should preserve query string", async () => {
router = new TestRouter(env, [{ name: "users", path: "/users/{{id}}" }]);
await router.navigate({ path: "/users/3?test=1" });
expect(window.location.pathname).toBe("/users/3");
expect(window.location.search).toBe("?test=1");
});
}); });
describe("routeToPath", () => { describe("routeToPath", () => {
@@ -114,69 +107,60 @@ describe("destToPath", () => {
describe("getRouteParams", () => { describe("getRouteParams", () => {
test("properly match simple routes", () => { test("properly match simple routes", () => {
router = new TestRouter(env, []);
// simple route // simple route
expect(getRouteParams({ path: "/home" }, "/home")).toEqual({}); expect(router["getRouteParams"]({ path: "/home" } as Route, "/home")).toEqual({});
// no match // no match
expect(getRouteParams({ path: "/home" }, "/otherpath")).toEqual(false); expect(router["getRouteParams"]({ path: "/home" } as Route, "/otherpath")).toEqual(false);
// fallback route // fallback route
expect(getRouteParams({ path: "*" }, "somepath")).toEqual({}); expect(router["getRouteParams"]({ path: "*" } as Route, "somepath")).toEqual({});
});
test("properly match routes with query params", () => {
expect(getRouteParams({ path: "/home" }, "/home?test=1")).toEqual({});
expect(getRouteParams({ path: "/home" }, "/home?test1=1&test2=2")).toEqual({});
}); });
test("properly match simple routes, mode hash", () => { test("properly match simple routes, mode hash", () => {
router = new TestRouter(env, [], { mode: "hash" });
// simple route // simple route
expect(getRouteParams({ path: "/home" }, "#/home")).toEqual({}); expect(router["getRouteParams"]({ path: "/home" } as Route, "#/home")).toEqual({});
// no match // no match
expect(getRouteParams({ path: "/home" }, "#/otherpath")).toEqual(false); expect(router["getRouteParams"]({ path: "/home" } as Route, "#/otherpath")).toEqual(false);
// fallback route // fallback route
expect(getRouteParams({ path: "*" }, "#/somepath")).toEqual({}); expect(router["getRouteParams"]({ path: "*" } as Route, "#/somepath")).toEqual({});
}); });
test("match some parameterized routes", () => { test("match some parameterized routes", () => {
expect(getRouteParams({ path: "/invoices/{{id}}" }, "/invoices/3")).toEqual({ router = new TestRouter(env, []);
expect(router["getRouteParams"]({ path: "/invoices/{{id}}" } as Route, "/invoices/3")).toEqual({
id: "3", id: "3",
}); });
}); });
test("match some parameterized routes, mode hash", () => { test("match some parameterized routes, mode hash", () => {
expect(getRouteParams({ path: "/invoices/{{id}}" }, "#/invoices/3")).toEqual({ router = new TestRouter(env, [], { mode: "hash" });
id: "3", expect(router["getRouteParams"]({ path: "/invoices/{{id}}" } as Route, "#/invoices/3")).toEqual(
}); {
id: "3",
}
);
}); });
test("can convert to number if needed", () => { test("can convert to number if needed", () => {
expect(getRouteParams({ path: "/invoices/{{id.number}}" }, "/invoices/3")).toEqual({ router = new TestRouter(env, []);
expect(
router["getRouteParams"]({ path: "/invoices/{{id.number}}" } as Route, "/invoices/3")
).toEqual({
id: 3, id: 3,
}); });
}); });
test("can convert to number if needed, mode: hash", () => { test("can convert to number if needed, mode: hash", () => {
expect(getRouteParams({ path: "/invoices/{{id.number}}" }, "#/invoices/3")).toEqual({ router = new TestRouter(env, [], { mode: "hash" });
id: 3,
});
});
test("can extract params not separated by slashes", () => {
expect(getRouteParams({ path: "/books/{{id.number}}-{{name}}" }, "/books/3-1984")).toEqual({
id: 3,
name: "1984",
});
});
test("can extract params not separated by slashes, mode: hash", () => {
expect( expect(
getRouteParams({ path: "books&id={{id.number}}&name={{name}}" }, "#books&id=3&name=1984") router["getRouteParams"]({ path: "/invoices/{{id.number}}" } as Route, "#/invoices/3")
).toEqual({ ).toEqual({
id: 3, id: 3,
name: "1984",
}); });
}); });
}); });
+1 -12
View File
@@ -1,5 +1,4 @@
import { Router, Route, RouterEnv } from "../../src/router/router"; import { Router } from "../../src/router/router";
import { makeTestEnv } from "../helpers";
import { QWeb } from "../../src/qweb/index"; import { QWeb } from "../../src/qweb/index";
export class TestRouter extends Router { export class TestRouter extends Router {
@@ -14,13 +13,3 @@ export class TestRouter extends Router {
} }
} }
} }
export function getRouteParams(route: Partial<Route>, path: string) {
const env = <RouterEnv>makeTestEnv();
const router = new TestRouter(env, [route]);
const {
routeIds: [routeId],
routes,
} = router;
return router["getRouteParams"](routes[routeId], path);
}
+3 -161
View File
@@ -1,4 +1,4 @@
import { Component, Env, mount } from "../src/component/component"; import { Component, Env } from "../src/component/component";
import { Store, useStore, useDispatch, useGetters, EnvWithStore } from "../src/store"; import { Store, useStore, useDispatch, useGetters, EnvWithStore } from "../src/store";
import { useState } from "../src/hooks"; import { useState } from "../src/hooks";
import { xml } from "../src/tags"; import { xml } from "../src/tags";
@@ -571,12 +571,12 @@ describe("connecting a component to store", () => {
app.state.beerId = 2; app.state.beerId = 2;
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe("<div><span>kwak</span></div>"); expect(fixture.innerHTML).toBe("<div><span>kwak</span></div>");
expect(counter).toBe(0); expect(counter).toBe(1);
store.dispatch("renameBeer", { id: 2, name: "orval" }); store.dispatch("renameBeer", { id: 2, name: "orval" });
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe("<div><span>orval</span></div>"); expect(fixture.innerHTML).toBe("<div><span>orval</span></div>");
expect(counter).toBe(1); expect(counter).toBe(2);
}); });
test("connected component is properly cleaned up on destroy", async () => { test("connected component is properly cleaned up on destroy", async () => {
@@ -1241,162 +1241,4 @@ describe("various scenarios", () => {
await nextTick(); await nextTick();
expect(fixture.innerHTML).toMatchSnapshot(); expect(fixture.innerHTML).toMatchSnapshot();
}); });
test("component with store, useState and shouldUpdate=false", async () => {
let state: any;
const store = new Store({ state: { rev: 0 } });
class Child extends Component {
static template = xml`<div><t t-esc="state.word"/><t t-esc="props.name"/></div>`;
state = useState({ word: "hello" });
constructor(parent, props) {
super(parent, props);
state = this.state;
useStore((props) => {
return 1;
});
}
shouldUpdate() {
return false;
}
}
class Parent extends Component {
static template = xml`<div><Child name="state.name"/></div>`;
static components = { Child };
state = useState({ name: "World" });
constructor(parent, props) {
super(parent, props);
useStore((props) => store.state.rev);
}
}
(env as any).store = store;
await mount(Parent, { target: fixture, env });
expect(fixture.innerHTML).toBe("<div><div>helloWorld</div></div>");
store.state.rev++;
// this is the key to the bug, it makes Parent be in "render" state but not
// yet rendered while the change of state happens
await Promise.resolve();
state.word = "test";
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>testWorld</div></div>");
});
test("component with store, useState, shouldUpdate=false and child with shouldupdate false", async () => {
let state: any;
const store = new Store({ state: { rev: 0 } });
class ChildChild extends Component {
static template = xml`<div><t t-esc="props.value"/></div>`;
shouldUpdate() {
return false;
}
}
class Child extends Component {
static template = xml`<div><t t-esc="state.word"/><t t-esc="props.name"/><ChildChild value="state.value"/></div>`;
static components = { ChildChild };
state = useState({ word: "hello", value: 3 });
constructor(parent, props) {
super(parent, props);
state = this.state;
useStore((props) => {
return 1;
});
}
shouldUpdate() {
return false;
}
}
class Parent extends Component {
static template = xml`<div><Child name="state.name"/></div>`;
static components = { Child };
state = useState({ name: "World" });
constructor(parent, props) {
super(parent, props);
useStore((props) => store.state.rev);
}
}
(env as any).store = store;
await mount(Parent, { target: fixture, env });
expect(fixture.innerHTML).toBe("<div><div>helloWorld<div>3</div></div></div>");
store.state.rev++;
// this is the key to the bug, it makes Parent be in "render" state but not
// yet rendered while the change of state happens
await Promise.resolve();
state.word = "test";
state.value = 44;
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>testWorld<div>3</div></div></div>");
});
test("parent/children with store, parent is remounted", async () => {
const store = new Store({ state: { a: 1, b: 1 } });
class Child extends Component {
static template = xml`<div><t t-esc="a"/></div>`;
a: any;
constructor(parent, props) {
super(parent, props);
this.a = useStore(
(state, props) => {
return state.a;
},
{
onUpdate: (a) => {
this.a = a;
},
}
);
}
}
class Parent extends Component {
static template = xml`
<div>
parent: <t t-esc="b"/>
<Child/>
</div>`;
static components = { Child };
b: any;
constructor(parent, props) {
super(parent, props);
this.b = useStore((state, props) => {
return state.b;
});
}
}
(env as any).store = store;
const div = document.createElement("div");
fixture.appendChild(div);
// initial mounting
const parent = await mount(Parent, { target: fixture, env });
expect(fixture.innerHTML).toBe("<div></div><div> parent: 1<div>1</div></div>");
// remounting component, then immediately update store.state
parent.mount(div);
store.state.a++;
await nextTick();
expect(fixture.innerHTML).toBe("<div><div> parent: 1<div>2</div></div></div>");
});
}); });
+2 -6
View File
@@ -8,7 +8,7 @@ import * as owl from "../../src/index";
import { Component, Env } from "../../src/component/component"; import { Component, Env } from "../../src/component/component";
import { xml } from "../../src/tags"; import { xml } from "../../src/tags";
import { makeTestFixture, makeTestEnv, nextTick } from "../helpers"; import { makeTestFixture, makeTestEnv } from "../helpers";
let fixture: HTMLElement = makeTestFixture(); let fixture: HTMLElement = makeTestFixture();
let env: Env = makeTestEnv(); let env: Env = makeTestEnv();
@@ -31,7 +31,6 @@ test("log a specific message for render method calls if component is not mounted
parent.unmount(); parent.unmount();
parent.state.value = 2; parent.state.value = 2;
await nextTick();
expect(steps).toEqual([ expect(steps).toEqual([
"[OWL_DEBUG] Parent<id=1> constructor, props={}", "[OWL_DEBUG] Parent<id=1> constructor, props={}",
"[OWL_DEBUG] Parent<id=1> mount", "[OWL_DEBUG] Parent<id=1> mount",
@@ -41,10 +40,7 @@ test("log a specific message for render method calls if component is not mounted
"[OWL_DEBUG] Parent<id=1> mounted", "[OWL_DEBUG] Parent<id=1> mounted",
"[OWL_DEBUG] scheduler: stop running tasks queue", "[OWL_DEBUG] scheduler: stop running tasks queue",
"[OWL_DEBUG] Parent<id=1> willUnmount", "[OWL_DEBUG] Parent<id=1> willUnmount",
"[OWL_DEBUG] Parent<id=1> render (warning: component is not mounted)", "[OWL_DEBUG] Parent<id=1> render (warning: component is not mounted, this render has no effect)",
"[OWL_DEBUG] scheduler: start running tasks queue",
"[OWL_DEBUG] Parent<id=1> rendering template",
"[OWL_DEBUG] scheduler: stop running tasks queue",
]); ]);
console.log = log; console.log = log;
}); });
+2 -2
View File
@@ -101,8 +101,8 @@
component.render = function(...args) { component.render = function(...args) {
const __owl__ = component.__owl__; const __owl__ = component.__owl__;
let msg = `render`; let msg = `render`;
if (__owl__.status !== 3 /* mounted */ && !__owl__.currentFiber) { if (!__owl__.isMounted && !__owl__.currentFiber) {
msg += ` (warning: component is not mounted)`; msg += ` (warning: component is not mounted, this render has no effect)`;
} }
log(msg); log(msg);
return render(...args); return render(...args);
+1 -1
View File
@@ -72,7 +72,7 @@ if __name__ == "__main__":
* Make an iframe, with all the js, css and xml properly injected. * Make an iframe, with all the js, css and xml properly injected.
*/ */
function makeCodeIframe(js, css, xml) { function makeCodeIframe(js, css, xml) {
const sanitizedXML = xml.replace(/<!--[\s\S]*?-->/g, "").replace(/`/g, '\\\`'); const sanitizedXML = xml.replace(/<!--[\s\S]*?-->/g, "");
// create iframe // create iframe
+41 -22
View File
@@ -2,7 +2,8 @@ const COMPONENTS = `// In this example, we show how components can be defined an
const { Component, useState, mount } = owl; const { Component, useState, mount } = owl;
class Greeter extends Component { class Greeter extends Component {
setup() { constructor() {
super(...arguments);
this.state = useState({ word: 'Hello' }); this.state = useState({ word: 'Hello' });
} }
@@ -13,9 +14,10 @@ class Greeter extends Component {
// Main root component // Main root component
class App extends Component { class App extends Component {
setup() { constructor() {
this.state = useState({ name: 'World'}); super(...arguments);
} this.state = useState({ name: 'World'});
}
} }
App.components = { Greeter }; App.components = { Greeter };
@@ -50,7 +52,8 @@ const ANIMATION = `// The goal of this component is to see how the t-transition
const { Component, useState, mount } = owl; const { Component, useState, mount } = owl;
class Counter extends Component { class Counter extends Component {
setup() { constructor() {
super(...arguments);
this.state = useState({ value: 0 }); this.state = useState({ value: 0 });
} }
@@ -60,7 +63,8 @@ class Counter extends Component {
} }
class App extends Component { class App extends Component {
setup() { constructor() {
super(...arguments);
this.state = useState({ flag: false, componentFlag: false, numbers: [] }); this.state = useState({ flag: false, componentFlag: false, numbers: [] });
} }
@@ -190,9 +194,10 @@ const LIFECYCLE_DEMO = `// This example shows all the possible lifecycle hooks
const { Component, useState, mount } = owl; const { Component, useState, mount } = owl;
class DemoComponent extends Component { class DemoComponent extends Component {
setup() { constructor() {
super(...arguments);
this.state = useState({ n: 0 }); this.state = useState({ n: 0 });
console.log("setup"); console.log("constructor");
} }
async willStart() { async willStart() {
console.log("willstart"); console.log("willstart");
@@ -218,7 +223,8 @@ class DemoComponent extends Component {
} }
class App extends Component { class App extends Component {
setup() { constructor() {
super(...arguments);
this.state = useState({ n: 0, flag: true }); this.state = useState({ n: 0, flag: true });
} }
@@ -289,7 +295,8 @@ function useMouse() {
// Main root component // Main root component
class App extends owl.Component { class App extends owl.Component {
setup() { constructor() {
super(...arguments);
// simple state hook (reactive object) // simple state hook (reactive object)
this.counter = useState({ value: 0 }); this.counter = useState({ value: 0 });
@@ -326,7 +333,8 @@ const { Component, Context, mount } = owl;
const { useContext } = owl.hooks; const { useContext } = owl.hooks;
class ToolbarButton extends Component { class ToolbarButton extends Component {
setup() { constructor() {
super(...arguments);
this.theme = useContext(this.env.themeContext); this.theme = useContext(this.env.themeContext);
} }
@@ -460,7 +468,8 @@ const actions = {
// TodoItem // TodoItem
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class TodoItem extends Component { class TodoItem extends Component {
setup() { constructor() {
super(...arguments);
useAutofocus("input"); useAutofocus("input");
this.state = useState({ isEditing: false }); this.state = useState({ isEditing: false });
this.dispatch = useDispatch(); this.dispatch = useDispatch();
@@ -490,7 +499,8 @@ class TodoItem extends Component {
// TodoApp // TodoApp
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class TodoApp extends Component { class TodoApp extends Component {
setup() { constructor() {
super(...arguments);
this.state = useState({ filter: "all" }); this.state = useState({ filter: "all" });
this.todos = useStore(state => state.todos); this.todos = useStore(state => state.todos);
this.dispatch = useDispatch(); this.dispatch = useDispatch();
@@ -1016,7 +1026,8 @@ class FormView extends owl.Component {}
FormView.components = { AdvancedComponent }; FormView.components = { AdvancedComponent };
class Chatter extends owl.Component { class Chatter extends owl.Component {
setup() { constructor() {
super(...arguments);
this.messages = Array.from(Array(100).keys()); this.messages = Array.from(Array(100).keys());
} }
} }
@@ -1159,7 +1170,8 @@ const SLOTS = `// We show here how slots can be used to create generic component
const { Component, useState, mount } = owl; const { Component, useState, mount } = owl;
class Card extends Component { class Card extends Component {
setup() { constructor() {
super(...arguments);
this.state = useState({ showContent: true }); this.state = useState({ showContent: true });
} }
@@ -1169,7 +1181,8 @@ class Card extends Component {
} }
class Counter extends Component { class Counter extends Component {
setup() { constructor() {
super(...arguments);
this.state = useState({val: 1}); this.state = useState({val: 1});
} }
@@ -1180,7 +1193,8 @@ class Counter extends Component {
// Main root component // Main root component
class App extends Component { class App extends Component {
setup() { constructor() {
super(...arguments);
this.state = useState({a: 1, b: 3}); this.state = useState({a: 1, b: 3});
} }
@@ -1292,7 +1306,8 @@ class SlowComponent extends Component {
class NotificationList extends Component {} class NotificationList extends Component {}
class App extends Component { class App extends Component {
setup() { constructor() {
super(...arguments);
this.state = useState({ value: 0, notifs: [] }); this.state = useState({ value: 0, notifs: [] });
} }
@@ -1366,7 +1381,8 @@ const FORM = `// This example illustrate how the t-model directive can be used t
const { Component, useState, mount } = owl; const { Component, useState, mount } = owl;
class Form extends Component { class Form extends Component {
setup() { constructor() {
super(...arguments);
this.state = useState({ this.state = useState({
text: "", text: "",
othertext: "", othertext: "",
@@ -1540,7 +1556,8 @@ const { useRef } = owl.hooks;
class HelloWorld extends Component {} class HelloWorld extends Component {}
class Counter extends Component { class Counter extends Component {
setup() { constructor() {
super(...arguments);
this.state = useState({ value: 0 }); this.state = useState({ value: 0 });
} }
@@ -1593,7 +1610,8 @@ class Window extends Component {
} }
class WindowManager extends Component { class WindowManager extends Component {
setup() { constructor() {
super(...arguments);
this.windows = []; this.windows = [];
this.nextId = 1; this.nextId = 1;
this.currentZindex = 1; this.currentZindex = 1;
@@ -1643,7 +1661,8 @@ class WindowManager extends Component {
WindowManager.components = { Window }; WindowManager.components = { Window };
class App extends Component { class App extends Component {
setup() { constructor() {
super(...arguments);
this.wmRef = useRef("wm"); this.wmRef = useRef("wm");
} }
+10 -66
View File
@@ -3,11 +3,9 @@ const readline = require("readline");
const fs = require("fs"); const fs = require("fs");
const exec = require("child_process").exec; const exec = require("child_process").exec;
const chalk = require("chalk"); const chalk = require("chalk");
const branchName = require('current-git-branch');
const REL_NOTES_FILE = `release-notes.md`; const REL_NOTES_FILE = `release-notes.md`;
const STEPS = 8; const STEPS = 8;
const branch = "master";
const rl = readline.createInterface({ const rl = readline.createInterface({
input: process.stdin, input: process.stdin,
@@ -23,19 +21,6 @@ startRelease().then(() => {
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
async function startRelease() { async function startRelease() {
// First check we are on master
if (branchName() !== branch) {
logError(`You shall not pass! You are not on the ${branch} branch!`)
return;
}
log("Check if code formatting is right...")
const checkFormatting = await execCommand("npm run check-formatting");
if (checkFormatting !== 0) {
logError("Prettier format validation failed. Aborting.");
return;
}
log(`*** Owl release script ***`); log(`*** Owl release script ***`);
log(`Current Version: ${package.version}`); log(`Current Version: ${package.version}`);
@@ -51,7 +36,7 @@ async function startRelease() {
content = await readFile("./" + file); content = await readFile("./" + file);
} catch (e) { } catch (e) {
logSubContent(e.message); logSubContent(e.message);
logError("Cannot find release notes... Aborting"); log("Cannot find release notes... Aborting");
return; return;
} }
let shouldBeDraft = await ask(`Should be a draft [y/n] ? (n)`); let shouldBeDraft = await ask(`Should be a draft [y/n] ? (n)`);
@@ -60,14 +45,12 @@ async function startRelease() {
{ {
draft = "--draft"; draft = "--draft";
} }
let shouldUploadPlayground = await ask(`Should this release be uploaded on the playground [y/n] ? (y)`);
shouldUploadPlayground = shouldUploadPlayground.toLowerCase() !== 'n';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
log(`Step 2/${STEPS}: running tests...`); log(`Step 2/${STEPS}: running tests...`);
const testsResult = await execCommand("npm run test"); const testsResult = await execCommand("npm run test");
if (testsResult !== 0) { if (testsResult !== 0) {
logError("Test suite does not pass. Aborting."); log("Test suite does not pass. Aborting.");
return; return;
} }
@@ -79,27 +62,26 @@ async function startRelease() {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
log(`Step 4/${STEPS}: creating git commit...`); log(`Step 4/${STEPS}: creating git commit...`);
const escapedContent = content.replace(/\"/g, '\\\"').replace(/\`/g, '\\\`'); const gitResult = await execCommand(`git commit -am "[REL] v${next}\n\n${content}"`);
const gitResult = await execCommand(`git commit -am "[REL] v${next}\n\n${escapedContent}"`);
if (gitResult !== 0) { if (gitResult !== 0) {
logError("Git commit failed. Aborting."); log("Git commit failed. Aborting.");
return; return;
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
log(`Step 5/${STEPS}: building owl...`); log(`Step 5/${STEPS}: building owl...`);
await execCommand("rm -rf dist/"); await execCommand("npm run prettier");
const buildResult = await execCommand("npm run build"); const buildResult = await execCommand("npm run build");
if (buildResult !== 0) { if (buildResult !== 0) {
logError("Build failed. Aborting."); log("Build failed. Aborting.");
return; return;
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
log(`Step 6/${STEPS}: pushing on github...`); log(`Step 6/${STEPS}: pushing on github...`);
const pushResult = await execCommand("git push origin " + branch); const pushResult = await execCommand("git push");
if (pushResult !== 0) { if (pushResult !== 0) {
logError("git push failed. Aborting."); log("git push failed. Aborting.");
return; return;
} }
@@ -108,51 +90,17 @@ async function startRelease() {
log(`Step 7/${STEPS}: Creating the release...`); log(`Step 7/${STEPS}: Creating the release...`);
const relaseResult = await execCommand(`gh release create v${next} dist/*.js ${draft} -F release-notes.md`); const relaseResult = await execCommand(`gh release create v${next} dist/*.js ${draft} -F release-notes.md`);
if (relaseResult !== 0) { if (relaseResult !== 0) {
logError("github release failed. Aborting."); log("github release failed. Aborting.");
return; return;
} }
log(`Step 8/${STEPS}: publishing module on npm...`); log(`Step 8/${STEPS}: publishing module on npm...`);
await execCommand("npm run publish"); await execCommand("npm run publish");
log("Owl Release process completed! Thank you for your patience"); log("Owl Release process completed! Thank you for your patience");
await execCommand(`gh release view`); await execCommand(`gh release view`);
await execCommand(`gh release view -w`); await execCommand(`gh release view -w`);
if (shouldUploadPlayground) {
log(`Bonus step: publishing new release on playground...`);
let owl_code = null;
status = 0
try {
owl_code = await readFile("dist/owl.iife.js");
} catch (e) {
logSubContent(e.message);
logError("Cannot read owl.iife.js... Aborting");
return;
}
status += await execCommand("git checkout gh-pages");
if (status !== 0) {
logError("Couldn't switch to gh-pages branch")
return;
}
try {
fs.writeFileSync('owl.js', owl_code)
} catch (err) {
logError(err)
return;
}
status += await execCommand(`git commit -am "[IMP] update owl to v${next}"`);
status += await execCommand(`git push origin gh-pages`);
status += await execCommand("git checkout -");
if (status !== 0) {
logError("Something went wrong for the playground update.")
}
}
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@@ -163,10 +111,6 @@ function log(text) {
console.log(chalk.yellow(formatLog(text))); console.log(chalk.yellow(formatLog(text)));
} }
function logError(text) {
console.log(chalk.red(formatLog(text)));
}
function formatLog(text) { function formatLog(text) {
return `[REL] ${text}`; return `[REL] ${text}`;
} }