Compare commits

...

25 Commits

Author SHA1 Message Date
Géry Debongnie fd6327b068 [REL] v1.0.5 2020-02-21 09:47:01 +01:00
Géry Debongnie d5098dd138 [MISC] set package.json license field to a valid SPDX expression
closes #646
2020-02-21 09:32:35 +01:00
Géry Debongnie ce052e0992 [FIX] transition: no crash if added/removed quickly
This commit should fix crashes coming from transition code on nodes.
This does not impact transitions on components.

closes #637
closes #641
2020-02-21 09:30:47 +01:00
Géry Debongnie b00188c1ce [IMP] qweb: add support for event capture
In this commit, we uses the "!" as suffix to designate an event that
should be captured.  This is inspired by the Vue source code.

This solves the issue of communicating additional information to the
underlying virtual dom.  However, this will most likely disappear when
we rewrite the vdom as a virtual block system.

closes #650
2020-02-21 09:22:43 +01:00
Géry Debongnie 2c01802b8e [FIX] qweb: support attributes with quotes
closes #651
2020-02-21 08:08:55 +01:00
Géry Debongnie 20eb848262 [FIX] component: merge hooks properly
Before this commit, creating a sub component with
t-att-style/t-att-class attributes or with t-on- event handlers would
override the *hook* object rendered by the sub component.

This is an issue for some directives, such as t-ref, which defines
hook functions.

This commit fixes the issue by checking for an override, and wrapping
the hooks in a function that calls each defined hook.

closes #638
2020-02-17 15:50:53 +01:00
Géry Debongnie 8b479749b7 [IMP] add yarn.lock to .gitignore 2020-02-17 13:06:23 +01:00
Géry Debongnie f7728b93bd [FIX] component: cancel previous mounting operations if necessary
closes #626
2020-02-17 12:49:32 +01:00
Géry Debongnie 03585d8fea [DOC] add dedicated page on error handling
closes #632
2020-02-17 09:11:04 +01:00
Géry Debongnie 047a9c8993 [DOC] add dedicated page on event handling
part of #632
2020-02-17 09:11:04 +01:00
Géry Debongnie 69d35abe4f [DOC] add a dedicated page on slots
closes #640
2020-02-17 09:11:04 +01:00
Géry Debongnie 1d7503913e [FIX] qweb: ignore comment nodes between t-if/t-elif/t-else
closes #636
2020-02-17 08:10:20 +01:00
Géry Debongnie ee956a1977 [FIX] component: resolve a subtle concurrency issue
Asynchronous rendering is subtle. Before this commit, it was possible
(though not easy) to get into a situation where the Owl rendering
pipeline decremented twice the counter of a fiber that indicates that
its work is complete.

The situation occurs in the `render` method, where we create a fiber,
wait for a micro tick before starting the actual rendering (the goal was
to batch all changes coming from a single call stack). However, in this
microtask tick, it was possible that the fiber was cancelled, and we did
not have a check for that.
2020-02-07 14:19:43 +01:00
Géry Debongnie 3fdc7a48f3 [IMP] types: make component generic types optional
In most cases, we just want Component<any, Env>. But since it was so
annoying to have always the type Env, we actually used
Component<any,any> everywhere.

With this commit, the generic types have a default (and their order is
swapped), so we can simply use Component in most cases, and
Component<Props> when we want to type the props.
2020-02-06 09:37:44 +01:00
Fabrice Henrion d212309f1b [FIX] typo
Courtesy of the en_US police
2020-02-04 09:03:02 +01:00
Géry Debongnie 8866905f33 [DOC] large refactoring, add section on starting project 2020-02-03 22:06:44 +01:00
Géry Debongnie 50b116c56d [DOC] add information on why odoo made owl 2020-02-03 22:06:44 +01:00
Géry Debongnie a823373220 [FIX] tool: properly wait in release script 2020-01-24 15:25:08 +01:00
Géry Debongnie e402ee62de [REL] v1.0.4 2020-01-21 10:28:50 +01:00
Géry Debongnie 5711eb065d [ADD] tooling: add release script
More work is needed to complete this, but the basics is here, and it is
difficult to test.

closes #612
2020-01-21 10:10:45 +01:00
Géry Debongnie 6739e79aac [ADD] tools: add single file component example to playground 2020-01-21 09:27:30 +01:00
Géry Debongnie 1c8dc97fca [FIX] component: can mount on different target without unmounting
closes #616
2020-01-21 09:26:38 +01:00
Géry Debongnie f9cac94dc7 [REF] tests: split component.test into multiple files 2020-01-21 09:26:38 +01:00
Géry Debongnie c62c3fea19 [FIX] types: improve type of useExternalListener
so it can work on window

closes #613
2020-01-21 08:11:40 +01:00
Géry Debongnie 3845607feb [DOC] store: clarify how a component can find a store
closes #617
2020-01-21 08:08:14 +01:00
69 changed files with 5344 additions and 3705 deletions
+4 -1
View File
@@ -15,6 +15,7 @@ yarn-debug.log*
yarn-error.log*
package-lock.json
yarn.lock
#ide's
.vscode
@@ -23,4 +24,6 @@ package-lock.json
node_modules
# Extras temp file
/tools/owl.js
/tools/owl.js
release-notes.md
+29 -158
View File
@@ -1,4 +1,4 @@
<h1 align="center">🦉 <a href="https://odoo.github.io/owl/">Odoo Web Library</a> 🦉</h1>
<h1 align="center">🦉 <a href="https://odoo.github.io/owl/">OWL Framework</a> 🦉</h1>
_Class based components with hooks, reactive state and concurrent mode_
@@ -11,18 +11,31 @@ simple and consistent way. Owl's main features are:
- a declarative component system,
- a reactivity system based on hooks,
- a store implementation (for state management),
- a small frontend router
- concurrent mode by default,
- a store and a frontend router
Owl components are defined with ES6 classes, they use QWeb templates, an underlying
virtual dom, integrates beautifully with hooks, and the rendering is asynchronous.
Owl components are defined with ES6 classes, they use QWeb templates, an
underlying virtual DOM, integrates beautifully with hooks, and the rendering is
asynchronous.
**Try it online!** An online playground is available at [https://odoo.github.io/owl/playground](https://odoo.github.io/owl/playground) to let you experiment with the Owl framework. There
are some code examples to showcase some interesting features.
**Try it online!** An online playground is available at
[https://odoo.github.io/owl/playground](https://odoo.github.io/owl/playground)
to let you experiment with the Owl framework. There are some code examples to
showcase some interesting features.
Owl is currently mostly stable. Possible future changes are explained in the
Owl is currently stable. Possible future changes are explained in the
[roadmap](roadmap.md).
## Why Owl?
Why did Odoo decide to make Yet Another Framework? This is really a question
that deserves [a long answer](doc/miscellaneous/why_owl.md). But in short, we believe that
while the current state of the art frameworks are excellent, they are not
optimized for our use case, and there is still room for something else.
If you are interested in a comparison with React or Vue, you will
find some more additional information [here](doc/miscellaneous/comparison.md).
## Example
Here is a short example to illustrate interactive components:
@@ -79,10 +92,9 @@ requirements are common and code needs to be maintained by large teams.
Owl is not designed to be fast nor small (even though it is quite good on those
two topics). It is a no nonsense framework to build applications. There is only
one way to define components (with classes).
one way to define components (with classes). There is no black magic. It just
works.
If you are interested in a comparison with React or Vue, you will
find some more information [here](doc/comparison.md).
## Documentation
@@ -90,19 +102,18 @@ A complete documentation for Owl can be found here:
- [Main documentation page](doc/readme.md).
The most important sections are:
Some of the most important pages are:
- [Tutorial: TodoList application](doc/learning/tutorial_todoapp.md)
- [How to start an Owl project](doc/learning/quick_start.md)
- [QWeb templating language](doc/reference/qweb_templating_language.md)
- [Component](doc/reference/component.md)
- [Hooks](doc/reference/hooks.md)
Found an issue in the documentation? A broken link? Some outdated information?
Submit a PR!
## Installing/Building
## Installing Owl
Owl can be installed with the following command:
Owl is available on `npm` and can be installed with the following command:
```
npm install @odoo/owl
@@ -110,148 +121,8 @@ npm install @odoo/owl
If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-1.0.3.js](https://github.com/odoo/owl/releases/download/v1.0.3/owl.js)
- [owl-1.0.3.min.js](https://github.com/odoo/owl/releases/download/v1.0.3/owl.min.js)
Some npm scripts are available:
| Command | Description |
| ---------------- | -------------------------------------------------- |
| `npm install` | install every dependency required for this project |
| `npm run build` | build a bundle of _owl_ in the _/dist/_ folder |
| `npm run minify` | minify the prebuilt owl.js file |
| `npm run test` | run all (owl) tests |
## Quick Overview
Owl components in an application are used to define a (dynamic) tree of components.
```
Root
/ \
A B
/ \
C D
```
**State:** each component can manage its own local state. It is a simple ES6
class, there are no special rules:
```js
class Counter extends Component {
static template = xml`
<button t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>`;
state = { value: 0 };
increment() {
this.state.value++;
this.render();
}
}
```
The example above shows a component with a local state. Note that since there
is nothing magical to the `state` object, we need to manually call the `render`
function whenever we update it. This can quickly become annoying (and not
efficient if we do it too much). There is a better way: using the `useState`
hook, which transforms an object into a reactive version of itself:
```js
const { useState } = owl.hooks;
class Counter extends Component {
static template = xml`
<button t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>`;
state = useState({ value: 0 });
increment() {
this.state.value++;
}
}
```
Note that the `t-on-click` handler can even be replaced by an inline statement:
```xml
<button t-on-click="state.value++">
```
**Props:** sub components often needs some information from their parents. This
is done by adding the required information to the template. This will then be
accessible by the sub component in the `props` object. Note that there is an
important rule here: the information contained in the `props` object is not
owned by the sub component, and should never be modified.
```js
class Child extends Component {
static template = xml`<div>Hello <t t-esc="props.name"/></div>`;
}
class Parent extends Component {
static template = xml`
<div>
<Child name="'Owl'" />
<Child name="'Framework'" />
</div>`;
static components = { Child };
}
```
**Communication:** there are multiple ways to communicate information between
components. However, the two most important ways are the following:
- from parent to children: by using `props`,
- from a children to one of its parent: by triggering events.
The following example illustrate both mechanisms:
```js
class OrderLine extends Component {
static template = xml`
<div t-on-click="add">
<div><t t-esc="props.line.name"/></div>
<div>Quantity: <t t-esc="props.line.quantity"/></div>
</div>`;
add() {
this.trigger("add-to-order", { line: props.line });
}
}
class Parent extends Component {
static template = xml`
<div t-on-add-to-order="addToOrder">
<OrderLine
t-foreach="orders"
t-as="line"
line="line" />
</div>`;
static components = { OrderLine };
orders = useState([{ id: 1, name: "Coffee", quantity: 0 }, { id: 2, name: "Tea", quantity: 0 }]);
addToOrder(event) {
const line = event.detail.line;
line.quantity++;
}
}
```
In this example, the `OrderLine` component trigger a `add-to-order` event. This
will generate a DOM event which will bubble along the DOM tree. It will then be
intercepted by the parent component, which will then get the line (from the
`detail` key) and then increment its quantity. See the section on [event handling](doc/reference/component.md#event-handling)
for more details on how events work.
Note that this example would have also worked if the `OrderLine` component
directly modifies the `line` object. However, this is not a good practice: this
only works because the `props` object received by the child component is reactive,
so the child component is then coupled to the parents implementation.
- [owl-1.0.5.js](https://github.com/odoo/owl/releases/download/v1.0.5/owl.js)
- [owl-1.0.5.min.js](https://github.com/odoo/owl/releases/download/v1.0.5/owl.min.js)
## License
+43
View File
@@ -0,0 +1,43 @@
# 🦉 How to debug Owl applications 🦉
Non trivial applications become quickly more difficult to understand. It is then
useful to have a solid understanding of what is going on. To help with that,
logging useful information is extremely valuable. There is a [javascript file](../../tools/debug.js) which can be evaluated in an application.
Once it is executed, it will log a lot of information on each component main hooks. The following code is a minified version to make it easier to copy/paste:
```
function debugOwl(t,e){let n,o="[OWL_DEBUG]";function r(t){let e;try{e=JSON.stringify(t||{})}catch(t){e="<JSON error>"}return e.length>200&&(e=e.slice(0,200)+"..."),e}if(Object.defineProperty(t.Component,"current",{get:()=>n,set(s){n=s;const i=s.constructor.name;if(e.componentBlackList&&e.componentBlackList.test(i))return;if(e.componentWhiteList&&!e.componentWhiteList.test(i))return;let l;Object.defineProperty(n,"__owl__",{get:()=>l,set(n){!function(n,s,i){let l=`${s}<id=${i}>`,c=t=>console.log(`${o} ${l} ${t}`),u=t=>(!e.methodBlackList||!e.methodBlackList.includes(t))&&!(e.methodWhiteList&&!e.methodWhiteList.includes(t));u("constructor")&&c(`constructor, props=${r(n.props)}`);u("willStart")&&t.hooks.onWillStart(()=>{c("willStart")});u("mounted")&&t.hooks.onMounted(()=>{c("mounted")});u("willUpdateProps")&&t.hooks.onWillUpdateProps(t=>{c(`willUpdateProps, nextprops=${r(t)}`)});u("willPatch")&&t.hooks.onWillPatch(()=>{c("willPatch")});u("patched")&&t.hooks.onPatched(()=>{c("patched")});u("willUnmount")&&t.hooks.onWillUnmount(()=>{c("willUnmount")});const d=n.__render.bind(n);n.__render=function(...t){c("rendering template"),d(...t)};const h=n.render.bind(n);n.render=function(...t){const e=n.__owl__;let o="render";return e.isMounted||e.currentFiber||(o+=" (warning: component is not mounted, this render has no effect)"),c(o),h(...t)};const p=n.mount.bind(n);n.mount=function(...t){return c("mount"),p(...t)}}(s,i,(l=n).id)}})}}),e.logScheduler){let e=t.Component.scheduler.start,n=t.Component.scheduler.stop;t.Component.scheduler.start=function(){this.isRunning||console.log(`${o} scheduler: start running tasks queue`),e.call(this)},t.Component.scheduler.stop=function(){this.isRunning&&console.log(`${o} scheduler: stop running tasks queue`),n.call(this)}}if(e.logStore){let e=t.Store.prototype.dispatch;t.Store.prototype.dispatch=function(t,...n){return console.log(`${o} store: action '${t}' dispatched. Payload: '${r(n)}'`),e.call(this,t,...n)}}}
debugOwl(owl, {
// componentBlackList: /App/, // regexp
// componentWhiteList: /SomeComponent/, // regexp
// methodBlackList: ["mounted"], // list of method names
// methodWhiteList: ["willStart"], // list of method names
logScheduler: false, // display/mute scheduler logs
logStore: true, // display/mute store logs
});
```
The above code, once pasted somewhere in the main javascript file of an owl
application, will log information looking like this:
```
[OWL_DEBUG] TodoApp<id=1> constructor, props={}
[OWL_DEBUG] TodoApp<id=1> mount
[OWL_DEBUG] TodoApp<id=1> willStart
[OWL_DEBUG] TodoApp<id=1> rendering template
[OWL_DEBUG] TodoItem<id=2> constructor, props={"id":2,"completed":false,"title":"hey"}
[OWL_DEBUG] TodoItem<id=2> willStart
[OWL_DEBUG] TodoItem<id=3> constructor, props={"id":4,"completed":false,"title":"aaa"}
[OWL_DEBUG] TodoItem<id=3> willStart
[OWL_DEBUG] TodoItem<id=2> rendering template
[OWL_DEBUG] TodoItem<id=3> rendering template
[OWL_DEBUG] TodoItem<id=3> mounted
[OWL_DEBUG] TodoItem<id=2> mounted
[OWL_DEBUG] TodoApp<id=1> mounted
```
Each component has an internal `id`, which is very useful when debugging.
Note that it is certainly useful to run this code at some point in an application,
just to get a feel of what each user action implies, for the framework.
@@ -1,4 +1,4 @@
# 🦉 Testing Owl components 🦉
# 🦉 How to test Components 🦉
## Content
@@ -11,8 +11,7 @@ It is a good practice to test applications and components to ensure that they
behave as expected. There are many ways to test a user interface: manual
testing, integration testing, unit testing, ...
In this section, we will discuss how to write unit tests for components, and
how to debug them if necessary.
In this section, we will discuss how to write unit tests for components.
## Unit Tests
+52
View File
@@ -0,0 +1,52 @@
# 🦉 How to write Single File Components 🦉
It is very useful to group code by feature instead of by type of file. It makes
it easier to scale application to larger size.
To do so, Owl has two small helpers that make it easy to define a
template or a stylesheet inside a javascript (or typescript) file: the
[`xml`](../reference/tags.md#xml-tag) and [`css`](../reference/tags.md#css-tag)
helper.
This means that the template, the style and the javascript code can be defined in
the same file. For example:
```js
const { Component } = owl;
const { xml, css } = owl.tags;
// -----------------------------------------------------------------------------
// TEMPLATE
// -----------------------------------------------------------------------------
const TEMPLATE = xml/* xml */ `
<div class="main">
<Sidebar/>
<Content />
</div>`;
// -----------------------------------------------------------------------------
// STYLE
// -----------------------------------------------------------------------------
const STYLE = css/* css */ `
.main {
display: grid;
grid-template-columns: 200px auto;
}
`;
// -----------------------------------------------------------------------------
// CODE
// -----------------------------------------------------------------------------
class Main extends Component {
static template = TEMPLATE;
static style = STYLE;
static components = { Sidebar, Content };
// rest of component...
}
```
Note that the above example has an inline xml comment, just after the `xml` call.
This is useful for some editor plugins, such as the VS Code addon
`Comment tagged template`, which, if installed, add syntax highlighting to the
content of the template string.
+133
View File
@@ -0,0 +1,133 @@
# 🦉 Quick Overview 🦉
Owl components in an application are used to define a (dynamic) tree of components.
```
Root
/ \
A B
/ \
C D
```
**State:** each component can manage its own local state. It is a simple ES6
class, there are no special rules:
```js
class Counter extends Component {
static template = xml`
<button t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>`;
state = { value: 0 };
increment() {
this.state.value++;
this.render();
}
}
```
The example above shows a component with a local state. Note that since there
is nothing magical to the `state` object, we need to manually call the `render`
function whenever we update it. This can quickly become annoying (and not
efficient if we do it too much). There is a better way: using the `useState`
hook, which transforms an object into a reactive version of itself:
```js
const { useState } = owl.hooks;
class Counter extends Component {
static template = xml`
<button t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>`;
state = useState({ value: 0 });
increment() {
this.state.value++;
}
}
```
Note that the `t-on-click` handler can even be replaced by an inline statement:
```xml
<button t-on-click="state.value++">
```
**Props:** sub components often needs some information from their parents. This
is done by adding the required information to the template. This will then be
accessible by the sub component in the `props` object. Note that there is an
important rule here: the information contained in the `props` object is not
owned by the sub component, and should never be modified.
```js
class Child extends Component {
static template = xml`<div>Hello <t t-esc="props.name"/></div>`;
}
class Parent extends Component {
static template = xml`
<div>
<Child name="'Owl'" />
<Child name="'Framework'" />
</div>`;
static components = { Child };
}
```
**Communication:** there are multiple ways to communicate information between
components. However, the two most important ways are the following:
- from parent to children: by using `props`,
- from a children to one of its parent: by triggering events.
The following example illustrate both mechanisms:
```js
class OrderLine extends Component {
static template = xml`
<div t-on-click="add">
<div><t t-esc="props.line.name"/></div>
<div>Quantity: <t t-esc="props.line.quantity"/></div>
</div>`;
add() {
this.trigger("add-to-order", { line: props.line });
}
}
class Parent extends Component {
static template = xml`
<div t-on-add-to-order="addToOrder">
<OrderLine
t-foreach="orders"
t-as="line"
line="line" />
</div>`;
static components = { OrderLine };
orders = useState([
{ id: 1, name: "Coffee", quantity: 0 },
{ id: 2, name: "Tea", quantity: 0 }
]);
addToOrder(event) {
const line = event.detail.line;
line.quantity++;
}
}
```
In this example, the `OrderLine` component trigger a `add-to-order` event. This
will generate a DOM event which will bubble along the DOM tree. It will then be
intercepted by the parent component, which will then get the line (from the
`detail` key) and then increment its quantity. See the page on [event handling](../reference/event_handling.md)
for more details on how events work.
Note that this example would have also worked if the `OrderLine` component
directly modifies the `line` object. However, this is not a good practice: this
only works because the `props` object received by the child component is reactive,
so the child component is then coupled to the parents implementation.
+374 -70
View File
@@ -1,107 +1,411 @@
# 🦉 Quick Start 🦉
# 🦉 How to start an Owl project 🦉
## Static Server
## Content
Let us assume that we have a static server running somewhere. Let us start by
adding an html page with a few extra files:
- [Overview](#overview)
- [Simple html file](#simple-html-file)
- [With a static server](#with-a-static-server)
- [Standard Javascript project](#standard-javascript-project)
## Overview
Each software project has its specific needs. Many of these needs can be solved
with some tooling: `webpack`, `gulp`, css preprocessor, bundlers, transpilers, ...
Because of that, it is usually not simple to just start a project. Some
frameworks provide their own tooling to help with that. But then, you have to
integrate and learn how these applications work.
Owl is designed to be used with no tooling at all. Because of that, Owl can
"easily" be integrated in a modern build toolchain. In this section, we will
discuss a few different setups to start a project. Each of these setups has
advantages and disadvantages in different situations.
## Simple html file
The simplest possible setup is the following: a simple javascript file with your
code. To do that, let us create the following file structure:
```
my-app/
index.html
app.css
app.js
owl-X.Y.Z.js
templates.xml
hello_owl/
index.html
owl.js
app.js
```
### HTML and CSS
The file `owl.js` can be downloaded from the last release published at
[https://github.com/odoo/owl/releases](https://github.com/odoo/owl/releases). It
is a single javascript file which export all Owl into the global `owl` object.
In a file `index.html`:
Now, `index.html` should contain the following:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>My OWL App</title>
<link href="app.css" rel="stylesheet" />
<script src="owl-X.Y.Z.js"></script>
<title>Hello Owl</title>
<script src="owl.js"></script>
<script src="app.js"></script>
</head>
<body>
<div id="main"></div>
<script src="app.js" type="module"></script>
</body>
<body></body>
</html>
```
In `app.css`:
And `app.js` should look like this:
```css
button {
color: darkred;
font-size: 30px;
width: 220px;
```js
const { Component } = owl;
const { xml } = owl.tags;
const { whenReady } = owl.utils;
// Owl Components
class App extends Component {
static template = xml`<div>Hello Owl</div>`;
}
// Setup code
function setup() {
const app = new App();
app.mount(document.body);
}
whenReady(setup);
```
Now, simply loading this html file in a browser should display a welcome message.
This setup is not fancy, but it is extremely simple. There are no tooling at
all required. It can be slightly optimized by using the minified build of Owl.
## With a static server
The previous setup has a big disadvantage: the application code is located in a
single file. Obviously, we could split it in several files and add multiple
`<script>` tags in the html page, but then we need to make sure the script are
inserted in the proper order, we need to export each file content in global
variables and we lose autocompletion across files.
There is a low tech solution to this issue: using native javascript modules.
This however has a requirement: for security reasons, browsers will not accept
modules on content served through the `file` protocol. This means that we need
to use a static server.
Let us start a new project with the following file structure:
```
hello_owl/
src/
app.js
index.html
main.js
owl.js
```
As previously, the file `owl.js` can be downloaded from the last release published at
[https://github.com/odoo/owl/releases](https://github.com/odoo/owl/releases).
Now, `index.html` should contain the following:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Hello Owl</title>
<script src="owl.js"></script>
<script src="main.js" type="module"></script>
</head>
<body></body>
</html>
```
Not that the `main.js` script tag has the `type="module"` attribute. This means
that the browser will parse the script as a module, and load all its dependencies.
Here is the content of `app.js` and `main.js`:
```js
// app.js ----------------------------------------------------------------------
const { Component } = owl;
const { xml } = owl.tags;
export class App extends Component {
static template = xml`<div>Hello Owl</div>`;
}
// main.js ---------------------------------------------------------------------
import { App } from "./app.js";
function setup() {
const app = new App();
app.mount(document.body);
}
owl.utils.whenReady(setup);
```
The `main.js` file import the `app.js` file. Note that the import statement has
a `.js` suffix, which is important. Most text editor can understand this syntax
and will provide autocompletion.
Now, to execute this code, we need to serve the `src` folder statically. A low
tech way to do that is to use for example the python `SimpleHTTPServer` feature:
```
$ cd src
$ python -m SimpleHTTPServer 8022 # now content is available at localhost:8022
```
Another more "javascripty" way to do it is to create a `npm` application. To do
that, we can add the following `package.json` file at the root of the project:
```json
{
"name": "hello_owl",
"version": "0.1.0",
"description": "Starting Owl app",
"main": "src/index.html",
"scripts": {
"serve": "serve src"
},
"author": "John",
"license": "ISC",
"devDependencies": {
"serve": "^11.3.0"
}
}
```
Also, let's not forget to add a release of OWL (`owl-X.Y.Z.js`)
We can now install the `serve` tool with the command `npm install`, and then,
start a static server with the simple `npm run serve` command.
### XML
## Standard Javascript project
In `templates.xml`:
The previous setup works, and is certainly good for some usecases, including
quick prototyping. However, it lacks some useful features, such as livereload,
a test suite, or bundling the code in a single file.
```xml
<templates>
<button t-name="clickcounter" t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>
</templates>
Each of these features, and many others, can be done in many different ways.
Since it is really not trivial to configure such a project, we provide here an
example that can be used as a starting point.
Our standard Owl project has the following file structure:
```
hello_owl/
public/
index.html
src/
components/
App.js
main.js
tests/
components/
App.test.js
helpers.js
.gitignore
package.json
webpack.config.js
```
### JS
This project as a `public` folder, meant to contain all static assets, such as
images and styles. The `src` folder has the javascript source code, and finally,
`tests` contains the test suite.
To build an application (or a sub-part of an application), we need two things:
Here is the content of `index.html`:
- an environment: it is the global context in which we are working. It needs to
contain a QWeb instance (preloaded with templates), and anything else that we
need. In practice, it could be used to contain some user session information, some
configuration keys (for example, isMobile = true/false if we are in mobile mode).
```html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Hello Owl</title>
</head>
<body></body>
</html>
```
- a description of the user interface: there should be a root component, which can
have sub components
Note that there are no `<script>` tag here. They will be injected by webpack.
Now, let's have a look at the javascript files:
Here are a few steps that we may take to get started:
```js
// src/components/App.js -------------------------------------------------------
import { Component, tags, useState } from "@odoo/owl";
- get the templates
- create a qweb engine, with the templates
- create an environment
- create an instance of the root component
- mount the root component to a DOM element
const { xml } = tags;
Let us now add the javascript to make it work, in `app.js`:
```javascript
const useState = owl.hooks.useState;
class ClickCounter extends owl.Component {
static template = "clickcounter";
state = useState({ value: 0 });
increment() {
this.state.value++;
export class App extends Component {
static template = xml`<div t-on-click="update">Hello <t t-esc="state.text"/></div>`;
state = useState({ text: "Owl" });
update() {
this.state.text = this.state.text === "Owl" ? "World" : "Owl";
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadFile("templates.xml");
ClickCounter.env = { qweb: new owl.QWeb({ templates }) };
const counter = new ClickCounter();
const target = document.getElementById("main");
await counter.mount(target);
// src/main.js -----------------------------------------------------------------
import { utils } from "@odoo/owl";
import { App } from "./components/App";
function setup() {
const app = new App();
app.mount(document.body);
}
start();
utils.whenReady(setup);
// tests/components/App.test.js ------------------------------------------------
import { App } from "../../src/components/App";
import { makeTestFixture, nextTick, click } from "../helpers";
let fixture;
beforeEach(() => {
fixture = makeTestFixture();
});
afterEach(() => {
fixture.remove();
});
describe("App", () => {
test("Works as expected...", async () => {
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>Hello Owl</div>");
click(fixture, "div");
await nextTick();
expect(fixture.innerHTML).toBe("<div>Hello World</div>");
});
});
// tests/helpers.js ------------------------------------------------------------
import { Component } from "@odoo/owl";
import "regenerator-runtime/runtime";
export async function nextTick() {
return new Promise(function(resolve) {
setTimeout(() => Component.scheduler.requestAnimationFrame(() => resolve()));
});
}
export function makeTestFixture() {
let fixture = document.createElement("div");
document.body.appendChild(fixture);
return fixture;
}
export function click(elem, selector) {
elem.querySelector(selector).dispatchEvent(new Event("click"));
}
```
Finally, here is the configuration files `.gitignore`, `package.json` and
`webpack.config.js`:
```
node_modules/
package-lock.json
dist/
```
```json
{
"name": "hello_owl",
"version": "0.1.0",
"description": "Demo app",
"main": "src/index.html",
"scripts": {
"test": "jest",
"build": "webpack --mode production",
"dev": "webpack-dev-server --mode development"
},
"author": "Someone",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.8.4",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"babel-jest": "^25.1.0",
"babel-loader": "^8.0.6",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
"html-webpack-plugin": "^3.2.0",
"jest": "^25.1.0",
"regenerator-runtime": "^0.13.3",
"serve": "^11.3.0",
"webpack": "^4.41.5",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.10.2"
},
"dependencies": {
"@odoo/owl": "^1.0.4"
},
"babel": {
"plugins": ["@babel/plugin-proposal-class-properties"],
"env": {
"test": {
"plugins": ["transform-es2015-modules-commonjs"]
}
}
},
"jest": {
"verbose": false,
"testRegex": "(/tests/.*(test|spec))\\.js?$",
"moduleFileExtensions": ["js"],
"transform": {
"^.+\\.[t|j]sx?$": "babel-jest"
}
}
}
```
```js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const host = process.env.HOST || "localhost";
module.exports = function(env, argv) {
const mode = argv.mode || "development";
return {
mode: mode,
entry: "./src/main.js",
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist")
},
module: {
rules: [
{
test: /\.jsx?$/,
loader: "babel-loader",
exclude: /node_modules/
}
]
},
resolve: {
extensions: [".js", ".jsx"]
},
devServer: {
contentBase: path.resolve(__dirname, "public/index.html"),
compress: true,
hot: true,
host,
port: 3000,
publicPath: "/"
},
plugins: [
new HtmlWebpackPlugin({
inject: true,
template: path.resolve(__dirname, "public/index.html")
})
]
};
};
```
With this setup, we can now use the following script commands:
```
npm run build # build the full application in prod mode in dist/
npm run dev # start a dev server with livereload
npm run test # run the jest test suite
```
+2 -2
View File
@@ -37,7 +37,7 @@ todoapp/
index.html
app.css
app.js
owl.min.js
owl.js
```
The entry point for this application is the file `index.html`, which should have
@@ -446,7 +446,7 @@ Now, this is an interesting situation: the task is displayed by the `Task`
component, but it is not the owner of its state, so it cannot modify it. Instead,
we want to communicate the request to toggle a task to the `App` component.
Since `App` is a parent of `Task`, we can
[trigger](../reference/component.md#event-handling) an event in `Task` and listen
[trigger](../reference/event_handling.md) an event in `Task` and listen
for it in `App`.
In `Task`, change the `input` to:
@@ -79,7 +79,7 @@ additional tools, we made a lot of effort to make the most of the web platform.
For example, Owl uses the standard `xml` parser that comes with every browser.
Because of that, Owl did not have to write its own template parser. Another
example is the [`xml`](reference/tags.md#xml-tag) tag helper function, which makes use of
example is the [`xml`](../reference/tags.md#xml-tag) tag helper function, which makes use of
native template literals to allow in a natural way to write `xml` templates
directly in the javascript code. This can be easily integrated with editor
plugins to have autocompletion inside the template.
@@ -127,7 +127,7 @@ structured than a template language. Note that the tooling is quite impressive:
there is a syntax highlighter for jsx here on github!
By comparison, here is the equivalent Owl component, written with the
[`xml`](reference/tags.md#xml-tag) tag helper:
[`xml`](../reference/tags.md#xml-tag) tag helper:
```js
class Clock extends Component {
@@ -251,7 +251,7 @@ keeps track of who get data, and retrigger a render when it was changed.
Owl store is a little bit like a mix of redux and vuex: it has actions (but not
mutations), and like VueX, it keeps track of the state changes. However, it does
not notify a component when the state changes. Instead, components need to connect
to the store like in redux, with the `useStore` hook (see the [store documentation](reference/store.md#connecting-a-component)).
to the store like in redux, with the `useStore` hook (see the [store documentation](../reference/store.md#connecting-a-component)).
```javascript
const actions = {
@@ -315,7 +315,7 @@ This work is based on the new ideas introduced by React hooks.
From the way React and Vue introduce their hooks, it may look like hooks are not
compatible with class components. However, this is not the case, as shown by
Owl [hooks](reference/hooks.md). They are inspired by both React and Vue. For example,
Owl [hooks](../reference/hooks.md). They are inspired by both React and Vue. For example,
the `useState` hook is named after React, but its API is closer to the `reactive`
Vue hook.
+180
View File
@@ -0,0 +1,180 @@
# 🦉 Why Owl ? 🦉
The common wisdom is that one should not reinvent the wheel, because that would
waste effort and resources. It is certainly true in many cases. A javascript
framework is a considerable investment, so it is quite logical to ask the question:
why did Odoo decide to make OWL instead of using a standard/well known framework,
such as React or Vue?
As you might expect, the answer to that question is not simple. But most of the
reasons discussed in this page are a consequence from a single fact: Odoo is
extremely modular.
This means, for example, that the core parts of Odoo are not aware, before runtime,
of what files will be loaded/executed, or what will be the state of the UI. Because
of that, Odoo cannot rely on a standard build toolchain. Also, this implies that
the core parts of Odoo need to be extremely generic. In other words, Odoo is not
really an application with a user interface. It is an application which generates
a dynamic user interface. And most frameworks are not up to the task.
Betting on Owl was not an easy choice to make, because there certainly are a lot
of conflicting needs that we want to carefully balance. Choosing anything other
than a well known framework is bound to be controversial. This page will explain
some of the reason why we still believe that building Owl is a worthwile
endeavour.
## Strategy
It is true that we want to keep control of our technology, in the sense that we
do not want to depend on Facebook or Google, or any other large (or small)
company. If they decide to change their license, or to go in a direction that
will not work for us, this may be a problem. This is even more true because
Odoo is not a conventional javascript application, and our needs are probably
quite different as most other applications.
## Class components
It is clear that the biggest frameworks are moving away from class components.
There is an implicit assumption that class components are terrible, and that
functional programming is the way to go. React even goes as far as to say that
classes are confusing for developers.
While there is some truth to that, and to the fact that composition is certainly
a good mechanism for code reuse, we believe that classes and inheritance are
important tools.
Sharing code between generic components with inheritance is the way Odoo built
its web client. And it is clear that inheritance is not the root of all evils.
It is often a perfectly simple and appropriate solution. What matter most is
the architectural decisions.
Also, Odoo has another specific use out of class components: each method of a
class provides an extension point for addons. This may not be a clean architecture
pattern, but it is a pragmatic decision that served Odoo well: classes are
sometimes monkey-patched to add behaviour from the outside. A little bit like
mixins, but from the outside.
Using React or Vue would make it significantly harder to monkey patch components,
because a lot of the state is hidden in their internals.
## Tooling
React or Vue have a huge community, and a lot of effort have been made into their
tooling. This is wonderful, but at the same time, a pretty big issue for Odoo:
since the assets are totally dynamic (and could change whenever the user install
or remove an addon), we need to have all that kind of tooling on the production
servers. This is certainly not ideal.
Also, this makes it very complicated to setup Vue or React tools: Odoo code is
not a simple file that import other files. It changes all the time, assets
are bundled differently in different contexts. This is the reason why Odoo has
its own module system, which are resolve at runtime, by the browser. The
dynamic nature of Odoo means that we often need to delay work as late as possible
(in other word, we want a JIT user interface!)
Our ideal framework has minimal (mandatory) tooling, which makes it easier to
deploy. Using React without JSX, or Vue without vue file is not very appealing.
At the same time, Owl is designed to solve this issue: it compiles templates
by the browser, it doesn't need much code for that, since we use the XML parser
built into each browser. Owl works with or without any additional tooling. It
can use template strings to write single file component, and is easy to integrate
in any html page, with a simple `<script>` tag.
## Template based
Odoo stores template as XML document in a database. This is very powerful, since
this allow the use of xpaths to customize other templates. This is a very
important feature of odoo, and one of the key to Odoo modularity.
Because of that, we still expect to write our templates in an XML document.
Weirdly enough, no major framework uses XML to store templates, even though it
is extremely convenient.
So, using React or Vue means that we need to make a template compiler. For React,
that would be a compiler that would take a QWeb template, and convert it to a
React render function. For Vue, it would convert it to a Vue template. Then
we need to bundle the vue template compiler as well.
Not only this would be complex (compiling a templating language into another is
not an easy task), but it would negatively impact the developer experience as
well. Writing Vue or React components in a QWeb template would certainly be
awkward, and very confusing.
## Developer Experience
This brings us to the following point: developer experience. We see this choice
as an investment for the future, and we want to make onboarding developer as
easy as possible.
While many javascript professionals clearly think that react/vue is not difficult
(which is true to some extent), it is alsy true that many non js specialists are
overwhelmed with the frontend world: functional component, hooks, and many other
fancy words. Also, what is available in the compilation context may be difficult,
there is a lot of black magic going on in pretty much every framework. Vue
somehow join various namespaces into one, under the hood, and add various internal
keys. Svelte transform the code. React require that state transformations are
deep, and not shallow.
Owl is trying very hard to have a simple and familiar API. It uses classes. Its
reactivity system is explicit, not implicit. The scoping rules are obvious. In
case of doubt, we err on the side of not implementing a feature.
It is certainly different from React or Vue, but at the same time, kind of
familiar for experienced developers.
## JIT compilation
There is also a clear trend in the frontend world to compile code
as much as possible ahead of time. Most frameworks will compile templates ahead
of time. And now Svelte is trying to compile the JS code away, so it can remove
itself from the bundle.
This is certainly reasonable for many usecases. However, this is not what Odoo
needs: Odoo will fetch templates from the database and need to compile them only
at the last possible moment, so we can apply all necessary xpaths.
Even more: Odoo needs to be able to generate (and compile) templates at runtime.
Currently, Odoo form views interpret a xml description. But the form view code
then needs to do a lot of complicated operations. With Owl, we will be able to
transform a view description into a QWeb template, then compile that and use it
immediately.
## Reactivity
There are other design choices that we feel are not optimal in other frameworks.
For example, the reactivity system. We like the way Vue did it, but it has a
flaw: it is not really optional. There is actually a way to opt out of the reactivity
system by freezing the state, but then, it is freezed.
And there certainly are situations where we need a state, which is not readonly,
and not observed. For example, imagine a spreadsheet component. It may have a
very large internal state, and it knows exactly when it needs to be rendered
(basically, whenever the user perform some action). Then, observing its state
is a net performance loss, both for the CPU and the memory.
## Concurrency
Many applications are happy to simply display a spinner whenever a new asynchronous
action is performed, but Odoo want a different user experience: most asynchronous
state changes are not displayed until ready. This is sometimes called a concurrent
mode: the UI is rendered in memory, and displayed only when it is ready (and
only if it has not been cancelled by subsequent user actions).
React has now an experimental concurrent mode, but it was not ready when Owl
started. Vue has not really an equivalent API (suspense is not what we need).
Also, React concurrent mode is complex to use. Concurrency was one of the rare
strong point of the former Odoo js framework (widgets), and we feel that Owl has
now a very strong concurrent mode, which is simple and powerful at the same time.
## Conclusion
This lengthy discussion showed that there are many small and not so small reasons
that current standard frameworks are not tailored to our needs. It is perfectly
fine, because they each chose a different set of tradeoffs.
However, we feel that there is still room in the framework world for something
that is different. For a framework that make choices compatible with Odoo.
And that is why we built Owl 🦉.
+28 -50
View File
@@ -1,14 +1,31 @@
# 🦉 OWL Documentation 🦉
## Learning Owl
Are you new to Owl? This is the place to start!
- [Tutorial: create a TodoList application](learning/tutorial_todoapp.md)
- [Quick Overview](learning/overview.md)
- [How to start an Owl project](learning/quick_start.md)
- [How to test Components](learning/how_to_test.md)
- [How to write Single File Components](learning/how_to_write_sfc.md)
- [How to write debug Owl applications](learning/how_to_debug.md)
## Reference
You will find here a complete reference of every feature, class or object
provided by Owl.
- [Animations](reference/animations.md)
- [Component](reference/component.md)
- [Content](reference/content.md)
- [Concurrency Model](reference/concurrency_model.md)
- [Configuration](reference/config.md)
- [Context](reference/context.md)
- [Environment](reference/environment.md)
- [Event Bus](reference/event_bus.md)
- [Event Handling](reference/event_handling.md)
- [Error Handling](reference/error_handling.md)
- [Hooks](reference/hooks.md)
- [Miscellaneous Components](reference/misc.md)
- [Observer](reference/observer.md)
@@ -18,60 +35,21 @@
- [QWeb Engine](reference/qweb_engine.md)
- [Router](reference/router.md)
- [Store](reference/store.md)
- [Slots](reference/slots.md)
- [Tags](reference/tags.md)
- [Utils](reference/utils.md)
## Learning Resources
## Other Topics
- [Quick Start: create an (almost) empty Owl application](learning/quick_start.md)
- [Tutorial: create a TodoList application](learning/tutorial_todoapp.md)
- [Testing Owl components](learning/testing_components.md)
This section provides miscellaneous document that explains some topics
which cannot be considered either a tutorial, or reference documentation.
## Miscellaneous
- [Owl architecture: the Virtual DOM](miscellaneous/vdom.md)
- [Owl architecture: the rendering pipeline](miscellaneous/rendering.md)
- [Comparison with React/Vue](miscellaneous/comparison.md)
- [Why did Odoo built Owl?](miscellaneous/why_owl.md)
- [Comparison with React/Vue](comparison.md)
- [Tooling](tooling.md)
- [Templates to start Owl applications (external link)](https://github.com/ged-odoo/owl-templates)
---
## Architecture
This section explains in more detail the inner workings of Owl. It is targeted
for developers working on Owl itself.
- [Virtual DOM](architecture/vdom.md)
- [Rendering](architecture/rendering.md)
## Owl Content
Here is a complete visual representation of everything exported by the `owl`
global object (so, for example, `Component` is available at `owl.Component`,
and `EventBus` is exported as `owl.core.EventBus`):
```
Component misc
Context AsyncRoot
QWeb Portal
Store router
useState Link
config RouteComponent
mode Router
core tags
EventBus css
Observer xml
hooks utils
onWillStart debounce
onMounted escape
onWillUpdateProps loadJS
onWillPatch loadFile
onPatched shallowEqual
onWillUnmount whenReady
useContext
useState
useRef
useSubEnv
useStore
useDispatch
useGetters
```
Note that for convenience, the `useState` hook is also exported at the root of the `owl` object.
Found an issue in the documentation? A broken link? Some outdated information?
Please open an issue or submit a PR!
+12 -265
View File
@@ -12,12 +12,9 @@
- [Lifecycle](#lifecycle)
- [Root Component](#root-component)
- [Composition](#composition)
- [Event Handling](#event-handling)
- [Form Input Bindings](#form-input-bindings)
- [References](#references)
- [Slots](#slots)
- [Dynamic sub components](#dynamic-sub-components)
- [Error Handling](#error-handling)
- [Functional Components](#functional-components)
- [SVG components](#svg-components)
@@ -308,16 +305,16 @@ A solid and robust component system needs useful hooks/methods to help
developers write components. Here is a complete description of the lifecycle of
a owl component:
| Method | Description |
| ------------------------------------------------ | ------------------------------------------------------------ |
| **[constructor](#constructorparent-props)** | constructor |
| **[willStart](#willstart)** | async, before first rendering |
| **[mounted](#mounted)** | just after component is rendered and added to the DOM |
| **[willUpdateProps](#willupdatepropsnextprops)** | async, before props update |
| **[willPatch](#willpatch)** | just before the DOM is patched |
| **[patched](#patchedsnapshot)** | just after the DOM is patched |
| **[willUnmount](#willunmount)** | just before removing component from DOM |
| **[catchError](#catcherrorerror)** | catch errors (see [error handling section](#error-handling)) |
| Method | Description |
| ------------------------------------------------ | ----------------------------------------------------------- |
| **[constructor](#constructorparent-props)** | constructor |
| **[willStart](#willstart)** | async, before first rendering |
| **[mounted](#mounted)** | just after component is rendered and added to the DOM |
| **[willUpdateProps](#willupdatepropsnextprops)** | async, before props update |
| **[willPatch](#willpatch)** | just before the DOM is patched |
| **[patched](#patchedsnapshot)** | just after the DOM is patched |
| **[willUnmount](#willunmount)** | just before removing component from DOM |
| **[catchError](#catcherrorerror)** | catch errors (see [error handling page](error_handling.md)) |
Notes:
@@ -452,8 +449,8 @@ This is the opposite method of `mounted`.
#### `catchError(error)`
The `catchError` method is useful when we need to intercept and properly react
to (rendering) errors that occur in some sub components. See the section on
[error handling](#error-handling).
to (rendering) errors that occur in some sub components. See the page on
[error handling](error_handling.md).
### Root Component
@@ -552,128 +549,6 @@ with a class object:
<MyComponent t-att-class="{a: state.flagA, b: state.flagB}" />
```
### Event Handling
In a component's template, it is useful to be able to register handlers on DOM
elements to some specific events. This is what makes a template _alive_. There
are four different use cases.
1. Register an event handler on a DOM node (_pure_ DOM event)
2. Register an event handler on a component (_pure_ DOM event)
3. Register an event handler on a DOM node (_business_ DOM event)
4. Register an event handler on a component (_business_ DOM event)
A _pure_ DOM event is directly triggered by a user interaction (e.g. a `click`).
```xml
<button t-on-click="someMethod">Do something</button>
```
This will be roughly translated in javascript like this:
```js
button.addEventListener("click", component.someMethod.bind(component));
```
The suffix (`click` in this example) is simply the name of the actual DOM
event.
A _business_ DOM event is triggered by a call to `trigger` on a component.
```xml
<MyComponent t-on-menu-loaded="someMethod" />
```
```js
class MyComponent {
someWhere() {
const payload = ...;
this.trigger('menu-loaded', payload);
}
}
```
The call to `trigger` generates an `OwlEvent`, a subclass of [_CustomEvent_](https://developer.mozilla.org/docs/Web/Guide/Events/Creating_and_triggering_events)
with an additional attribute `originalComponent` (the component that triggered
the event). The generated event is of type `menu-loaded` and dispatches it on
the component's DOM element (`this.el`). The event bubbles and is cancelable.
The parent component listening to event `menu-loaded` will receive the payload
in its `someMethod` handler (in the `detail` property of the event), whenever
the event is triggered.
```js
class ParentComponent {
someMethod(ev) {
const payload = ev.detail;
...
}
}
```
By convention, we use KebabCase for the name of _business_ events.
The `t-on` directive allows to prebind its arguments. For example,
```xml
<button t-on-click="someMethod(expr)">Do something</button>
```
Here, `expr` is a valid Owl expression, so it could be `true` or some variable
from the rendering context.
One can also directly specify inline statements. For example,
```xml
<button t-on-click="state.counter++">Increment counter</button>
```
Here, `state` must be defined in the rendering context (typically the component)
as it will be translated to:
```js
button.addEventListener("click", () => {
context.state.counter++;
});
```
Warning: inline expressions are evaluated in the context of the template. This
means that they can access the component methods and properties. But if they set
a key, the inline statement will actually not modify the component, but a key in
a sub scope.
```xml
<button t-on-click="value = 1">Set value to 1 (does not work!!!)</button>
<button t-on-click="state.value = 1">Set state.value to 1 (work as expected)</button>
```
In order to remove the DOM event details from the event handlers (like calls to
`event.preventDefault`) and let them focus on data logic, _modifiers_ can be
specified as additional suffixes of the `t-on` directive.
| Modifier | Description |
| ---------- | ----------------------------------------------------------------- |
| `.stop` | calls `event.stopPropagation()` before calling the method |
| `.prevent` | calls `event.preventDefault()` before calling the method |
| `.self` | calls the method only if the `event.target` is the element itself |
```xml
<button t-on-click.stop="someMethod">Do something</button>
```
Note that modifiers can be combined (ex: `t-on-click.stop.prevent`), and that
the order may matter. For instance `t-on-click.prevent.self` will prevent all
clicks while `t-on-click.self.prevent` will only prevent clicks on the element
itself.
Finally, empty handlers are tolerated as they could be defined only to apply
modifiers. For example,
```xml
<button t-on-click.stop="">Do something</button>
```
This will simply stop the propagation of the event.
### Form Input Bindings
It is very common to need to be able to read the value out of an html `input` (or
@@ -823,75 +698,6 @@ Note that these two examples uses the suffix `ref` to name the reference. This
is not mandatory, but it is a useful convention, so we do not forget to access
it with the `el` or `comp` suffix.
### Slots
To make generic components, it is useful to be able for a parent component to _inject_
some sub template, but still be the owner. For example, a generic dialog component
will need to render some content, some footer, but with the parent as the
rendering context.
This is what _slots_ are for.
```xml
<div t-name="Dialog" class="modal">
<div class="modal-title"><t t-esc="props.title"/></div>
<div class="modal-content">
<t t-slot="content"/>
</div>
<div class="modal-footer">
<t t-slot="footer"/>
</div>
</div>
```
Slots are defined by the caller, with the `t-set` directive:
```xml
<div t-name="SomeComponent">
<div>some component</div>
<Dialog title="Some Dialog">
<t t-set="content">
<div>hey</div>
</t>
<t t-set="footer">
<button t-on-click="doSomething">ok</button>
</t>
</Dialog>
</div>
```
In this example, the component `Dialog` will render the slots `content` and `footer`
with its parent as rendering context. This means that clicking on the button
will execute the `doSomething` method on the parent, not on the dialog.
Default slot: the first element inside the component which is not a named slot will
be considered the `default` slot. For example:
```xml
<div t-name="Parent">
<Child>
<span>some content</span>
</Child>
</div>
<div t-name="Child">
<t t-slot="default"/>
</div>
```
Slots can define a default content, in case the parent did not define them:
```xml
<div t-name="Parent">
<Child/>
</div>
<span t-name="Child">
<t t-slot="default">default content</t>
</span>
<!-- will be rendered as: <div><span>default content</span></div> -->
```
### Dynamic sub components
It is not common, but sometimes we need a dynamic component name. In this case,
@@ -937,65 +743,6 @@ component class.
Note that the `t-component` directive can only be used on `<t>` nodes.
### Error Handling
By default, whenever an error occurs in the rendering of an Owl application, we
destroy the whole application. Otherwise, we cannot offer any guarantee on the
state of the resulting component tree. It might be hopelessly corrupted, but
without any user-visible state.
Clearly, it sometimes is a little bit extreme to destroy the application. This
is why we have a builtin mechanism to handle rendering errors (and errors coming
from lifecycle hooks): the `catchError` hook.
Whenever the `catchError` lifecycle hook is implemented, all errors coming from
sub components rendering and/or lifecycle method calls will be caught and given
to the `catchError` method. This allows us to properly handle the error, and to
not break the application.
For example, here is how we could implement an `ErrorBoundary` component:
```xml
<div t-name="ErrorBoundary">
<t t-if="state.error">
Error handled
</t>
<t t-else="">
<t t-slot="default" />
</t>
</div>
```
```js
class ErrorBoundary extends Component {
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
```
Using the `ErrorBoundary` is then extremely simple:
```xml
<ErrorBoundary><SomeOtherComponent/></ErrorBoundary>
```
Note that we need to be careful here: the fallback UI should not throw any
error, otherwise we risk going into an infinite loop.
Also, it may be useful to know that whenever an error is caught, it is then
broadcasted to the application by an event on the `qweb` instance. It may be
useful, for example, to log the error somewhere.
```js
env.qweb.on("error", null, function(error) {
// do something
// react to the error
});
```
### Functional Components
Owl does not exactly have functional components. However, there is an extremely
+1 -1
View File
@@ -137,7 +137,7 @@ Here is what Owl will do:
Tags are very small helpers to make it easy to write inline templates. There is
only one currently available tag: `xml`, but we plan to add other tags later,
such as a `css` tag, which will be used to write [single file components](../tooling.md#single-file-component).
such as a `css` tag, which will be used to write [single file components](../learning/how_to_write_sfc.md).
### Asynchronous Rendering
+36
View File
@@ -0,0 +1,36 @@
# 🦉 Owl Content 🦉
Here is a complete visual representation of everything exported by the `owl`
global object.
For example, `Component` is available at `owl.Component` and `EventBus` is
exported as `owl.core.EventBus`.
```
Component misc
Context AsyncRoot
QWeb Portal
Store router
useState Link
config RouteComponent
mode Router
core tags
EventBus css
Observer xml
hooks utils
onWillStart debounce
onMounted escape
onWillUpdateProps loadJS
onWillPatch loadFile
onPatched shallowEqual
onWillUnmount whenReady
useContext
useState
useRef
useSubEnv
useStore
useDispatch
useGetters
```
Note that for convenience, the `useState` hook is also exported at the root of the `owl` object.
+81
View File
@@ -0,0 +1,81 @@
# 🦉 Error Handling 🦉
## Content
- [Overview](#overview)
- [Example](#example)
- [Reference](#reference)
## Overview
By default, whenever an error occurs in the rendering of an Owl application, we
destroy the whole application. Otherwise, we cannot offer any guarantee on the
state of the resulting component tree. It might be hopelessly corrupted, but
without any user-visible state.
Clearly, it sometimes is a little bit extreme to destroy the application. This
is why we have a builtin mechanism to handle rendering errors (and errors coming
from lifecycle hooks): the `catchError` hook.
## Example
For example, here is how we could implement an `ErrorBoundary` component:
```xml
<div t-name="ErrorBoundary">
<t t-if="state.error">
Error handled
</t>
<t t-else="">
<t t-slot="default" />
</t>
</div>
```
```js
class ErrorBoundary extends Component {
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
```
Using the `ErrorBoundary` is then extremely simple:
```xml
<ErrorBoundary><SomeOtherComponent/></ErrorBoundary>
```
Note that we need to be careful here: the fallback UI should not throw any
error, otherwise we risk going into an infinite loop (also, see the page on
[slots](slots.md) for more information on the `t-slot` directive).
## Reference
Whenever the `catchError` lifecycle hook is implemented, all errors coming from
sub components rendering and/or lifecycle method calls will be caught and given
to the `catchError` method. This allows us to properly handle the error, and to
not break the application.
There are important things to know:
- If an error that occured in the internal rendering cycle is not caught, then
Owl will destroy the full application. This is done on purpose, because Owl
cannot guarantee that the state is not corrupted from this point on.
- errors coming from event handlers are NOT managed by `catchError` or any other
owl mechanism. This is up to the application developer to properly recover
from an error
Also, it may be useful to know that whenever an error is caught, it is then
broadcasted to the application by an event on the `qweb` instance. It may be
useful, for example, to log the error somewhere.
```js
env.qweb.on("error", null, function(error) {
// do something
// react to the error
});
```
+137
View File
@@ -0,0 +1,137 @@
# 🦉 Event Handling 🦉
## Content
- [Event Handling](#event-handling)
- [Business DOM Events](#business-dom-events)
- [Inline Event Handlers](#inline-event-handlers)
- [Modifiers](#modifiers)
## Event Handling
In a component's template, it is useful to be able to register handlers on DOM
elements to some specific events. This is what makes a template _alive_. There
are four different use cases.
1. Register an event handler on a DOM node (_pure_ DOM event)
2. Register an event handler on a component (_pure_ DOM event)
3. Register an event handler on a DOM node (_business_ DOM event)
4. Register an event handler on a component (_business_ DOM event)
A _pure_ DOM event is directly triggered by a user interaction (e.g. a `click`).
```xml
<button t-on-click="someMethod">Do something</button>
```
This will be roughly translated in javascript like this:
```js
button.addEventListener("click", component.someMethod.bind(component));
```
The suffix (`click` in this example) is simply the name of the actual DOM
event.
## Business DOM Events
A _business_ DOM event is triggered by a call to `trigger` on a component.
```xml
<MyComponent t-on-menu-loaded="someMethod" />
```
```js
class MyComponent {
someWhere() {
const payload = ...;
this.trigger('menu-loaded', payload);
}
}
```
The call to `trigger` generates an `OwlEvent`, a subclass of [_CustomEvent_](https://developer.mozilla.org/docs/Web/Guide/Events/Creating_and_triggering_events)
with an additional attribute `originalComponent` (the component that triggered
the event). The generated event is of type `menu-loaded` and dispatches it on
the component's DOM element (`this.el`). The event bubbles and is cancelable.
The parent component listening to event `menu-loaded` will receive the payload
in its `someMethod` handler (in the `detail` property of the event), whenever
the event is triggered.
```js
class ParentComponent {
someMethod(ev) {
const payload = ev.detail;
...
}
}
```
By convention, we use KebabCase for the name of _business_ events.
The `t-on` directive allows to prebind its arguments. For example,
```xml
<button t-on-click="someMethod(expr)">Do something</button>
```
Here, `expr` is a valid Owl expression, so it could be `true` or some variable
from the rendering context.
## Inline Event Handlers
One can also directly specify inline statements. For example,
```xml
<button t-on-click="state.counter++">Increment counter</button>
```
Here, `state` must be defined in the rendering context (typically the component)
as it will be translated to:
```js
button.addEventListener("click", () => {
context.state.counter++;
});
```
Warning: inline expressions are evaluated in the context of the template. This
means that they can access the component methods and properties. But if they set
a key, the inline statement will actually not modify the component, but a key in
a sub scope.
```xml
<button t-on-click="value = 1">Set value to 1 (does not work!!!)</button>
<button t-on-click="state.value = 1">Set state.value to 1 (work as expected)</button>
```
## Modifiers
In order to remove the DOM event details from the event handlers (like calls to
`event.preventDefault`) and let them focus on data logic, _modifiers_ can be
specified as additional suffixes of the `t-on` directive.
| Modifier | Description |
| ---------- | ------------------------------------------------------------------------------------------------------------------------ |
| `.stop` | calls `event.stopPropagation()` before calling the method |
| `.prevent` | calls `event.preventDefault()` before calling the method |
| `.self` | calls the method only if the `event.target` is the element itself |
| `.capture` | bind the event handler in [capture](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) mode. |
```xml
<button t-on-click.stop="someMethod">Do something</button>
```
Note that modifiers can be combined (ex: `t-on-click.stop.prevent`), and that
the order may matter. For instance `t-on-click.prevent.self` will prevent all
clicks while `t-on-click.self.prevent` will only prevent clicks on the element
itself.
Finally, empty handlers are tolerated as they could be defined only to apply
modifiers. For example,
```xml
<button t-on-click.stop="">Do something</button>
```
This will simply stop the propagation of the event.
+2 -2
View File
@@ -18,7 +18,7 @@ for modals (as in `bootstrap-modal`).
### Usage
The content it will teleport is defined within the `<Portal>` node and
internally uses the `default` [Slot](component.md#slots).
internally uses the `default` [Slot](slots.md).
This slot must contain only **one** node, which in turn can have as many children as necessary.
@@ -95,7 +95,7 @@ The teleported piece is updated as any other `Component`'s DOM and in the same s
Namely the teleported piece will be updated in function of its parents components, and patched as
a normal child.
The [_business_ events](component.md#event-handling) triggered by a child component will be stopped
The [_business_ events](event_handling.md#business-dom-events) triggered by a child component will be stopped
to not bubble outside of the `target`. They will, on the other hand, be re-directed onto the
`Portal`'s root node and bubble up the DOM as if it were triggered by a regular child component.
+1 -1
View File
@@ -61,7 +61,7 @@ Its API is quite simple:
```
- **`render(name, context, extra)`**: renders a template. This returns a `vnode`,
which is a virtual representation of the DOM (see [vdom doc](../architecture/vdom.md)).
which is a virtual representation of the DOM (see [vdom doc](../miscellaneous/vdom.md)).
```js
const vnode = qweb.render("App", component);
+2 -2
View File
@@ -71,9 +71,9 @@ needs. Here is a list of all Owl specific directives:
| `t-component`, `t-props` | [Defining a sub component](component.md#composition) |
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#references) |
| `t-key` | [Defining a key (to help virtual dom reconciliation)](#loops) |
| `t-on-*` | [Event handling](component.md#event-handling) |
| `t-on-*` | [Event handling](event_handling.md) |
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
| `t-slot` | [Rendering a slot](component.md#slots) |
| `t-slot` | [Rendering a slot](slots.md) |
| `t-model` | [Form input bindings](component.md#form-input-bindings) |
## Reference
+93
View File
@@ -0,0 +1,93 @@
# 🦉 Slots 🦉
## Content
- [Overview](#overview)
- [Example](#example)
- [Reference](#reference)
## Overview
Owl is a template based component system. There is therefore a need to be able
to make generic components. For example, imagine a generic `Dialog`
component, which is able to display some arbitrary content.
Obviously, we want to use this component everywhere in our application, to
display various different content. The `Dialog` component is technically the
owner of its content, but is only a container. The user of the `Dialog` is
the component that want to _inject_ something inside the `Dialog`. This is
exactly what slots are for.
## Example
To make generic components, it is useful to be able for a parent component to _inject_
some sub template, but still be the owner. For example, a generic dialog component
will need to render some content, some footer, but with the parent as the
rendering context.
```xml
<div t-name="Dialog" class="modal">
<div class="modal-title"><t t-esc="props.title"/></div>
<div class="modal-content">
<t t-slot="content"/>
</div>
<div class="modal-footer">
<t t-slot="footer"/>
</div>
</div>
```
Slots are defined by the caller, with the `t-set` directive:
```xml
<div t-name="SomeComponent">
<div>some component</div>
<Dialog title="Some Dialog">
<t t-set="content">
<div>hey</div>
</t>
<t t-set="footer">
<button t-on-click="doSomething">ok</button>
</t>
</Dialog>
</div>
```
In this example, the component `Dialog` will render the slots `content` and `footer`
with its parent as rendering context. This means that clicking on the button
will execute the `doSomething` method on the parent, not on the dialog.
## Reference
Default slot: the first element inside the component which is not a named slot will
be considered the `default` slot. For example:
```xml
<div t-name="Parent">
<Child>
<span>some content</span>
</Child>
</div>
<div t-name="Child">
<t t-slot="default"/>
</div>
```
Default content: slots can define a default content, in case the parent did not define them:
```xml
<div t-name="Parent">
<Child/>
</div>
<span t-name="Child">
<t t-slot="default">default content</t>
</span>
<!-- will be rendered as: <div><span>default content</span></div> -->
```
Rendering context: the content of the slots is actually rendered with the
rendering context corresponding to where it was defined, not where it is
positioned. This allows the user to define event handlers that will be bound
to the correct component (usually, the grandparent of the slot content).
+13 -1
View File
@@ -208,7 +208,11 @@ Note that getters are not cached.
### Connecting a Component
At some point, we need a way to interact with the store from a component. This
can be done with the help of the three store hooks:
means that the component needs a reference to the store. By default, it looks
for it in the `env.store` key. However, this can be configured with the `useStore`
hook.
Every component-store interactions are done with the help of the three store hooks:
- [`useStore`](#usestore) to subscribe a component to some part of the store state,
- [`useDispatch`](#usedispatch) to get a reference to a dispatch function,
@@ -229,6 +233,14 @@ const state = {
const store = new owl.Store({ state, actions });
```
To make it accessible to the complete application, we will put it in the
environment:
```js
// in this example, the root component is App
App.env.store = store;
```
A counter component can then select this value and dispatch an action like this:
```js
+1 -1
View File
@@ -10,7 +10,7 @@
Tags are very small helpers intended to make it easy to write inline templates
or styles. There are currently two tags: `css` and `xml`. With these functions,
it is possible to write [single file components](../tooling.md#single-file-component).
it is possible to write [single file components](../learning/how_to_write_sfc.md).
## XML tag
-117
View File
@@ -1,117 +0,0 @@
# 🦉 Tooling 🦉
## Content
- [Overview](#overview)
- [Playground](#playground)
- [Benchmarks](#benchmarks)
- [Single File Component](#single-file-component)
- [Debugging Script](#debugging-script)
## Overview
To help work with/improve/learn OWL, there are a few extras tools/settings.
- development mode: enable better error reporting for the developer
- a playground application: a space to experiment and learn Owl.
- a benchmarks application: allow comparison with a few common frameworks
The two applications are available in the `tools/` folder, and can be accessed
by using a static http server. A simple python
server is available in `server.py`. There is also a npm script to start it:
`npm run tools` (and its version with a watcher: `npm run tools:watch`).
## Playground
The playground is an important application designed to help learning and
experimenting with Owl. The last published version of Owl can be tested [online](https://odoo.github.io/owl/playground/).
It is an application similar to `jsFiddle`, but specialized for Owl: there are
three tabs (`js`, `css` and `xml`), and a simple button `Run` to execute that
code in an iframe.
## Benchmarks
Note: This is more an internal tool, useful for people working on Owl.
The benchmarks application is a very small application, implemented in different
frameworks, and in different versions of Owl. This is a simple internal tool,
useful to compare various performance metrics on some tasks.
## Single File Component
It is very useful to group code by feature instead of by type of file. It makes
it easier to scale application to larger size.
To do so, Owl has two small helpers that make it easy to define a
template or a stylesheet inside a javascript (or typescript) file: the
[`xml`](reference/tags.md#xml-tag) and [`css`](reference/tags.md#css-tag)
helper.
This means that the template, the style and the javascript code can be defined in
the same file. For example:
```js
const { Component } = owl;
const { xml, css } = owl.tags;
// -----------------------------------------------------------------------------
// TEMPLATE
// -----------------------------------------------------------------------------
const TEMPLATE = xml/* xml */ `
<div class="main">
<Sidebar/>
<Content />
</div>`;
// -----------------------------------------------------------------------------
// STYLE
// -----------------------------------------------------------------------------
const STYLE = css/* css */ `
.main {
display: grid;
grid-template-columns: 200px auto;
}
`;
// -----------------------------------------------------------------------------
// CODE
// -----------------------------------------------------------------------------
class Main extends Component {
static template = TEMPLATE;
static style = STYLE;
static components = { Sidebar, Content };
// rest of component...
}
```
Note that the above example has an inline xml comment, just after the `xml` call.
This is useful for some editor plugins, such as the VS Code addon
`Comment tagged template`, which, if installed, add syntax highlighting to the
content of the template string.
## Debugging Script
## Debugging
Non trivial applications become quickly more difficult to understand. It is then
useful to have a solid understanding of what is going on. To help with that,
logging useful information is extremely valuable. There is a [javascript file](../tools/debug.js) which can be evaluated in an application.
Once it is executed, it will log a lot of information on each component main hooks. The following code is a minified version to make it easier to copy/paste:
```
function debugOwl(t,e){let n,o="[OWL_DEBUG]";function r(t){let e;try{e=JSON.stringify(t||{})}catch(t){e="<JSON error>"}return e.length>200&&(e=e.slice(0,200)+"..."),e}if(Object.defineProperty(t.Component,"current",{get:()=>n,set(s){n=s;const i=s.constructor.name;if(e.componentBlackList&&e.componentBlackList.test(i))return;if(e.componentWhiteList&&!e.componentWhiteList.test(i))return;let l;Object.defineProperty(n,"__owl__",{get:()=>l,set(n){!function(n,s,i){let l=`${s}<id=${i}>`,c=t=>console.log(`${o} ${l} ${t}`),u=t=>(!e.methodBlackList||!e.methodBlackList.includes(t))&&!(e.methodWhiteList&&!e.methodWhiteList.includes(t));u("constructor")&&c(`constructor, props=${r(n.props)}`);u("willStart")&&t.hooks.onWillStart(()=>{c("willStart")});u("mounted")&&t.hooks.onMounted(()=>{c("mounted")});u("willUpdateProps")&&t.hooks.onWillUpdateProps(t=>{c(`willUpdateProps, nextprops=${r(t)}`)});u("willPatch")&&t.hooks.onWillPatch(()=>{c("willPatch")});u("patched")&&t.hooks.onPatched(()=>{c("patched")});u("willUnmount")&&t.hooks.onWillUnmount(()=>{c("willUnmount")});const d=n.__render.bind(n);n.__render=function(...t){c("rendering template"),d(...t)};const h=n.render.bind(n);n.render=function(...t){const e=n.__owl__;let o="render";return e.isMounted||e.currentFiber||(o+=" (warning: component is not mounted, this render has no effect)"),c(o),h(...t)};const p=n.mount.bind(n);n.mount=function(...t){return c("mount"),p(...t)}}(s,i,(l=n).id)}})}}),e.logScheduler){let e=t.Component.scheduler.start,n=t.Component.scheduler.stop;t.Component.scheduler.start=function(){this.isRunning||console.log(`${o} scheduler: start running tasks queue`),e.call(this)},t.Component.scheduler.stop=function(){this.isRunning&&console.log(`${o} scheduler: stop running tasks queue`),n.call(this)}}if(e.logStore){let e=t.Store.prototype.dispatch;t.Store.prototype.dispatch=function(t,...n){return console.log(`${o} store: action '${t}' dispatched. Payload: '${r(n)}'`),e.call(this,t,...n)}}}
debugOwl(owl, {
// componentBlackList: /App/, // regexp
// componentWhiteList: /SomeComponent/, // regexp
// methodBlackList: ["mounted"], // list of method names
// methodWhiteList: ["willStart"], // list of method names
logScheduler: false, // display/mute scheduler logs
logStore: true, // display/mute store logs
});
```
Note that it is certainly useful to run this code at some point in an application,
just to get a feel of what each user action implies, for the framework.
+10 -4
View File
@@ -1,10 +1,13 @@
{
"name": "@odoo/owl",
"version": "1.0.3",
"version": "1.0.5",
"description": "Odoo Web Library (OWL)",
"main": "dist/owl.js",
"types": "dist/types/index.d.ts",
"files": ["dist/types/", "dist/owl.js"],
"files": [
"dist/types/",
"dist/owl.js"
],
"engines": {
"node": ">=10.15.3"
},
@@ -21,14 +24,15 @@
"pretools:watch": "npm run build",
"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",
"publish": "npm run buildcommonjs && npm publish"
"publish": "npm run buildcommonjs && npm publish",
"release": "node tools/release.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/odoo/owl.git"
},
"author": "Odoo",
"license": "LGPL",
"license": "LGPL-3.0-only",
"bugs": {
"url": "https://github.com/odoo/owl/issues"
},
@@ -36,8 +40,10 @@
"dependencies": {},
"devDependencies": {
"@types/jest": "^23.3.12",
"chalk": "^3.0.0",
"cpx": "^1.5.0",
"git-rev-sync": "^1.12.0",
"github-api": "^3.3.0",
"jest": "^23.6.0",
"jest-environment-jsdom": "^24.7.1",
"live-server": "^1.2.1",
+2 -17
View File
@@ -1,35 +1,20 @@
# 🦉 OWL Roadmap 🦉
- Current version: 1.0.3
- Current version: 1.0.5
- Status: stable
This roadmap is only an attempt at predicting Owl's future. Everything may
change!
### December 2019
If all goes well, Owl is upgraded to beta status. From now on, no API change,
even small, is expected (but it still could happen).
### End of 2019
Release v1.0
- API should be stable,
- we will use semantic versioning,
- we will maintain a changelog and an upgrade guide.
### 1.x
- add chrome and firefox devtools,
- add support for single file components,
- fix every bugs,
- improve documentation,
- small backward compatible improvements.
### 2.x (2021? 2022?)
### 2.x (2020? 2021? 2022?)
Maybe:
+38 -40
View File
@@ -37,8 +37,10 @@ export interface Env {
[key: string]: any;
}
export type MountPosition = "first-child" | "last-child" | "self";
interface MountOptions {
position?: "first-child" | "last-child" | "self";
position?: MountPosition;
}
/**
@@ -46,7 +48,7 @@ interface MountOptions {
* useful to typecheck and describe the internal keys used by Owl to manage the
* component tree.
*/
interface Internal<T extends Env, Props> {
interface Internal<T extends Env> {
// each component has a unique id, useful mostly to handle parent/child
// relationships
readonly id: number;
@@ -58,8 +60,8 @@ interface Internal<T extends Env, Props> {
// parent and children keys are obviously useful to setup the parent-children
// relationship.
parent: Component<T, any> | null;
children: { [key: number]: Component<T, any> };
parent: Component<any, T> | null;
children: { [key: number]: Component<any, T> };
// children mapping: from templateID to componentID. templateID identifies a
// place in a template. The t-component directive needs it to be able to get
// the component instance back whenever the template is rerendered.
@@ -85,7 +87,7 @@ interface Internal<T extends Env, Props> {
willStartCB: Function | null;
willUpdatePropsCB: Function | null;
classObj: { [key: string]: boolean } | null;
refs: { [key: string]: Component<T, any> | HTMLElement | undefined } | null;
refs: { [key: string]: Component<any, T> | HTMLElement | undefined } | null;
}
export const portalSymbol = Symbol("portal"); // FIXME
@@ -95,18 +97,17 @@ export const portalSymbol = Symbol("portal"); // FIXME
//------------------------------------------------------------------------------
let nextId = 1;
export class Component<T extends Env, Props extends {}> {
readonly __owl__: Internal<Env, Props>;
export class Component<Props extends {} = any, T extends Env = Env> {
readonly __owl__: Internal<T>;
static template?: string | null = null;
static _template?: string | null = null;
static current: Component<any, any> | null = null;
static current: Component | null = null;
static components = {};
static props?: any;
static defaultProps?: any;
static env: any = {};
// expose scheduler s.t. it can be mocked for testing purposes
static scheduler: Scheduler = scheduler;
__target: HTMLElement | undefined;
/**
* The `el` is the root element of the component. Note that it could be null:
@@ -130,7 +131,7 @@ export class Component<T extends Env, Props extends {}> {
* hand. Other components should be created automatically by the framework (with
* the t-component directive in a template)
*/
constructor(parent?: Component<T, any> | null, props?: Props) {
constructor(parent?: Component<any, T> | null, props?: Props) {
Component.current = this;
let constr = this.constructor as any;
@@ -302,23 +303,29 @@ export class Component<T extends Env, Props extends {}> {
const position = options.position || "last-child";
const __owl__ = this.__owl__;
if (__owl__.isMounted) {
return Promise.resolve();
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__.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)) {
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)`;
throw new Error(message);
}
let inserter =
position === "last-child"
? el => target.appendChild(el)
: position === "first-child"
? el => target.prepend(el)
: el => {};
if (position === "self") {
this.__target = target as HTMLElement;
}
const fiber = new Fiber(null, this, false, inserter);
const fiber = new Fiber(null, this, false, target, position);
fiber.shouldPatch = false;
if (!__owl__.vnode) {
this.__prepareAndRender(fiber, () => {});
@@ -363,9 +370,12 @@ export class Component<T extends Env, Props extends {}> {
// currentFiber that is already rendered (isRendered is true), so we are
// about to be mounted
const isMounted = __owl__.isMounted;
const fiber = new Fiber(null, this, force, null);
const fiber = new Fiber(null, this, force, null, null);
Promise.resolve().then(() => {
if (__owl__.isMounted || !isMounted) {
if (fiber.isCompleted) {
return;
}
// we are mounted (__owl__.isMounted), or if we are currently being
// mounted (!isMounted), so we call __render
this.__render(fiber);
@@ -434,7 +444,7 @@ export class Component<T extends Env, Props extends {}> {
* Note that it does not call the __callWillUnmount method to avoid visiting
* all children many times.
*/
__destroy(parent: Component<any, any> | null) {
__destroy(parent: Component | null) {
const __owl__ = this.__owl__;
const isMounted = __owl__.isMounted;
if (isMounted) {
@@ -494,7 +504,7 @@ export class Component<T extends Env, Props extends {}> {
* Private trigger method, allows to choose the component which triggered
* the event in the first place
*/
__trigger(component: Component<any, any>, eventType: string, payload?: any) {
__trigger(component: Component, eventType: string, payload?: any) {
if (this.el) {
const ev = new OwlEvent(component, eventType, {
bubbles: true,
@@ -517,7 +527,7 @@ export class Component<T extends Env, Props extends {}> {
const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps);
if (shouldUpdate) {
const __owl__ = this.__owl__;
const fiber = new Fiber(parentFiber, this, parentFiber.force, null);
const fiber = new Fiber(parentFiber, this, parentFiber.force, null, null);
if (!parentFiber.child) {
parentFiber.child = fiber;
} else {
@@ -549,20 +559,8 @@ export class Component<T extends Env, Props extends {}> {
* Main patching method. We call the virtual dom patch method here to convert
* a virtual dom vnode into some actual dom.
*/
__patch(vnode: VNode) {
const __owl__ = this.__owl__;
if (this.__target) {
if (this.__target.tagName.toLowerCase() !== vnode.sel) {
throw new Error(
`Cannot attach '${this.constructor.name}' to target node (not same tag name)`
);
}
__owl__.vnode = patch(this.__target, vnode);
delete this.__target;
} else {
const target = __owl__.vnode || document.createElement(vnode.sel!);
__owl__.vnode = patch(target, vnode);
}
__patch(target: HTMLElement | VNode | DocumentFragment, vnode: VNode) {
this.__owl__.vnode = patch(target as any, vnode);
}
/**
@@ -572,7 +570,7 @@ export class Component<T extends Env, Props extends {}> {
*/
__prepare(parentFiber: Fiber, scope: any, cb: CallableFunction): Fiber {
this.__owl__.scope = scope;
const fiber = new Fiber(parentFiber, this, parentFiber.force, null);
const fiber = new Fiber(parentFiber, this, parentFiber.force, null, null);
fiber.shouldPatch = false;
if (!parentFiber.child) {
parentFiber.child = fiber;
+27 -2
View File
@@ -23,6 +23,26 @@ QWeb.utils.defineProxy = function defineProxy(target, source) {
}
};
QWeb.utils.assignHooks = function assignHooks(dataObj, hooks) {
if ("hook" in dataObj) {
const hookObject = dataObj.hook;
for (let name in hooks) {
const current = hookObject[name];
const fn = hooks[name];
if (current) {
hookObject[name] = (...args) => {
current(...args);
fn(...args);
};
} else {
hookObject[name] = fn;
}
}
} else {
dataObj.hook = hooks;
}
};
/**
* The t-component directive is certainly a complicated and hard to maintain piece
* of code. To help you, fellow developer, if you have to maintain it, I offer
@@ -279,6 +299,8 @@ QWeb.addDirective({
}
let eventsCode = events
.map(function([name, value]) {
const capture = name.match(/\.capture/);
name = capture ? name.replace(/\.capture/, "") : name;
const { event, handler } = makeHandlerCode(
ctx,
name,
@@ -286,12 +308,15 @@ QWeb.addDirective({
false,
T_COMPONENT_MODS_CODE
);
if (capture) {
return `vn.elm.addEventListener('${event}', ${handler}, true);`;
}
return `vn.elm.addEventListener('${event}', ${handler});`;
})
.join("");
const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false);
const styleCode = styleExpr ? `vn.elm.style = ${styleExpr};` : "";
createHook = `vnode.data.hook = {create(_, vn){${styleCode}${eventsCode}}};`;
createHook = `utils.assignHooks(vnode.data, {create(_, vn){${styleCode}${eventsCode}}});`;
}
ctx.addLine(
@@ -368,7 +393,7 @@ QWeb.addDirective({
if (transition) {
ctx.addLine(`const __patch${componentID} = w${componentID}.__patch;`);
ctx.addLine(
`w${componentID}.__patch = fiber => {__patch${componentID}.call(w${componentID}, fiber); if(!w${componentID}.__owl__.transitionInserted){w${componentID}.__owl__.transitionInserted = true;utils.transitionInsert(w${componentID}.__owl__.vnode, '${transition}');}};`
`w${componentID}.__patch = (t, vn) => {__patch${componentID}.call(w${componentID}, t, vn); if(!w${componentID}.__owl__.transitionInserted){w${componentID}.__owl__.transitionInserted = true;utils.transitionInsert(w${componentID}.__owl__.vnode, '${transition}');}};`
);
}
ctx.addLine(`parent.__owl__.cmap[${templateKey}] = w${componentID}.__owl__.id;`);
+45 -13
View File
@@ -1,5 +1,5 @@
import { h, VNode } from "../vdom/index";
import { Component } from "./component";
import { Component, MountPosition } from "./component";
import { scheduler } from "./scheduler";
/**
@@ -46,11 +46,12 @@ export class Fiber {
// scheduler.
counter: number = 0;
inserter: (el: HTMLElement) => void | null;
target: HTMLElement | DocumentFragment | null;
position: MountPosition | null;
scope: any;
component: Component<any, any>;
component: Component;
vnode: VNode | null = null;
root: Fiber;
@@ -61,10 +62,17 @@ export class Fiber {
error?: Error;
constructor(parent: Fiber | null, component: Component<any, any>, force, inserter) {
constructor(
parent: Fiber | null,
component: Component,
force: boolean,
target: HTMLElement | DocumentFragment | null,
position: MountPosition | null
) {
this.component = component;
this.force = force;
this.inserter = inserter;
this.target = target;
this.position = position;
const __owl__ = component.__owl__;
this.scope = __owl__.scope;
@@ -179,7 +187,7 @@ export class Fiber {
complete() {
let component = this.component;
this.isCompleted = true;
if (!this.inserter && !component.__owl__.isMounted) {
if (!this.target && !component.__owl__.isMounted) {
return;
}
@@ -208,17 +216,41 @@ export class Fiber {
for (let i = patchLen - 1; i >= 0; i--) {
const fiber = patchQueue[i];
component = fiber.component;
component.__patch(fiber.vnode!);
if (!fiber.shouldPatch && (!fiber.inserter || i !== 0)) {
component.__owl__.pvnode!.elm = component.__owl__.vnode!.elm;
if (fiber.target && i === 0) {
let target;
if (fiber.position === "self") {
target = fiber.target;
if ((target as HTMLElement).tagName.toLowerCase() !== fiber.vnode!.sel) {
throw new Error(
`Cannot attach '${component.constructor.name}' to target node (not same tag name)`
);
}
} else {
target = component.__owl__.vnode || document.createElement(fiber.vnode!.sel!);
}
component.__patch(target!, fiber.vnode!);
} else {
if (fiber.shouldPatch) {
component.__patch(component.__owl__.vnode!, fiber.vnode!);
} else {
component.__patch(document.createElement(fiber.vnode!.sel!), fiber.vnode!);
component.__owl__.pvnode!.elm = component.__owl__.vnode!.elm;
}
}
component.__owl__.currentFiber = null;
}
// insert into the DOM (mount case)
let inDOM = false;
if (this.inserter) {
this.inserter(this.component.el!);
if (this.target) {
switch (this.position) {
case "first-child":
this.target.prepend(this.component.el!);
break;
case "last-child":
this.target.appendChild(this.component.el!);
break;
}
inDOM = document.body.contains(this.component.el);
this.component.env.qweb.trigger("dom-appended");
}
@@ -227,12 +259,12 @@ export class Fiber {
for (let i = patchLen - 1; i >= 0; i--) {
const fiber = patchQueue[i];
component = fiber.component;
if (fiber.shouldPatch && !this.inserter) {
if (fiber.shouldPatch && !this.target) {
component.patched();
if (component.__owl__.patchedCB) {
component.__owl__.patchedCB();
}
} else if (this.inserter ? inDOM : true) {
} else if (this.target ? inDOM : true) {
component.__callMounted();
}
}
+12 -1
View File
@@ -34,7 +34,7 @@ export class Scheduler {
this.isRunning = false;
}
addFiber(fiber): Promise<void> {
addFiber(fiber: Fiber): Promise<void> {
// if the fiber was remapped into a larger rendering fiber, it may not be a
// root fiber. But we only want to register root fibers
fiber = fiber.root;
@@ -57,6 +57,17 @@ export class Scheduler {
});
}
rejectFiber(fiber: Fiber, reason: string) {
fiber = fiber.root;
const index = this.tasks.findIndex(t => t.fiber === fiber);
if (index >= 0) {
const [task] = this.tasks.splice(index, 1);
fiber.cancel();
fiber.error = new Error(reason);
task.callback();
}
}
/**
* Process all current tasks. This only applies to the fibers that are ready.
* Other tasks are left unchanged.
+2 -2
View File
@@ -100,11 +100,11 @@ export class Context extends EventBus {
* to context state changes. The `useContext` method returns the context state
*/
export function useContext(ctx: Context): any {
const component: Component<any, any> = Component.current!;
const component: Component = Component.current!;
return useContextWithCB(ctx, component, component.render.bind(component));
}
export function useContextWithCB(ctx: Context, component: Component<any, any>, method): any {
export function useContextWithCB(ctx: Context, component: Component, method): any {
const __owl__ = component.__owl__;
const id = __owl__.id;
const mapping = ctx.mapping;
+1 -1
View File
@@ -7,7 +7,7 @@ import { Component } from "../component/component";
*/
export class OwlEvent<T> extends CustomEvent<T> {
originalComponent: Component<any, any>;
originalComponent: Component;
constructor(component, eventType, options) {
super(eventType, options);
this.originalComponent = component;
+12 -7
View File
@@ -22,7 +22,7 @@ import { Observer } from "./core/observer";
* trigger a rerendering of the current component.
*/
export function useState<T>(state: T): T {
const component: Component<any, any> = Component.current!;
const component: Component = Component.current!;
const __owl__ = component.__owl__;
if (!__owl__.observer) {
__owl__.observer = new Observer();
@@ -37,7 +37,7 @@ export function useState<T>(state: T): T {
function makeLifecycleHook(method: string, reverse: boolean = false) {
if (reverse) {
return function(cb) {
const component: Component<any, any> = Component.current!;
const component: Component = Component.current!;
if (component.__owl__[method]) {
const current = component.__owl__[method];
component.__owl__[method] = function() {
@@ -50,7 +50,7 @@ function makeLifecycleHook(method: string, reverse: boolean = false) {
};
} else {
return function(cb) {
const component: Component<any, any> = Component.current!;
const component: Component = Component.current!;
if (component.__owl__[method]) {
const current = component.__owl__[method];
component.__owl__[method] = function() {
@@ -66,7 +66,7 @@ function makeLifecycleHook(method: string, reverse: boolean = false) {
function makeAsyncHook(method: string) {
return function(cb) {
const component: Component<any, any> = Component.current!;
const component: Component = Component.current!;
if (component.__owl__[method]) {
const current = component.__owl__[method];
component.__owl__[method] = function(...args) {
@@ -96,7 +96,7 @@ export const onWillUpdateProps = makeAsyncHook("willUpdatePropsCB");
*/
interface Ref {
el: HTMLElement | null;
comp: Component<any, any> | null;
comp: Component | null;
}
export function useRef(name: string): Ref {
@@ -111,7 +111,7 @@ export function useRef(name: string): Ref {
}
return null;
},
get comp(): Component<any, any> | null {
get comp(): Component | null {
const val = __owl__.refs && __owl__.refs[name];
return val instanceof Component ? val : null;
}
@@ -149,7 +149,12 @@ export function useSubEnv(nextEnv) {
* in the constructor of the OWL component that needs to be notified,
* `useExternalListener(window, 'click', this._doSomething);`
* */
export function useExternalListener(target: HTMLElement, eventName: string, handler, eventParams?) {
export function useExternalListener(
target: HTMLElement | typeof window,
eventName: string,
handler,
eventParams?
) {
const boundHandler = handler.bind(Component.current);
onMounted(() => target.addEventListener(eventName, boundHandler, eventParams));
+1 -1
View File
@@ -10,7 +10,7 @@ import { xml } from "../tags";
* from this coordination. This is the goal of the AsyncRoot component.
*/
export class AsyncRoot extends Component<any, any> {
export class AsyncRoot extends Component {
static template = xml`<t t-slot="default"/>`;
async __updateProps(nextProps, parentFiber) {
+9 -5
View File
@@ -18,7 +18,11 @@ import { useSubEnv } from "../hooks";
* are re-triggered on an empty <portal> node located in the parent's DOM.
*/
export class Portal extends Component<any, any> {
interface Props {
target: string;
}
export class Portal extends Component<Props> {
static template = xml`<portal><t t-slot="default"/></portal>`;
static props = {
target: {
@@ -43,7 +47,7 @@ export class Portal extends Component<any, any> {
// represents the element that is moved somewhere else
portal: VNode | null = null;
// the target where we will move `portal`
target: HTMLElement | null = null;
target: Element | null = null;
constructor(parent, props) {
super(parent, props);
@@ -120,7 +124,7 @@ export class Portal extends Component<any, any> {
*
* @override
*/
__patch(vnode) {
__patch(target, vnode) {
if (this.doTargetLookUp) {
const target = document.querySelector(this.props.target);
if (!target) {
@@ -149,7 +153,7 @@ export class Portal extends Component<any, any> {
this.portal = patch(portalPatch, vnode.children![0] as VNode);
vnode.children = [];
super.__patch(vnode);
super.__patch(target, vnode);
if (shouldDeploy) {
this.__deployPortal();
@@ -158,7 +162,7 @@ export class Portal extends Component<any, any> {
/**
* Override to set the env
*/
__trigger(component: Component<any, any>, eventType: string, payload?: any) {
__trigger(component: Component, eventType: string, payload?: any) {
const env = this.env;
this.env = this.parentEnv;
super.__trigger(component, eventType, payload);
+11 -1
View File
@@ -40,7 +40,10 @@ export function makeHandlerCode(
putInCache: boolean,
modcodes = MODS_CODE
): HandlerInfo {
const [event, ...mods] = fullName.slice(5).split(".");
let [event, ...mods] = fullName.slice(5).split(".");
if (mods.includes("capture")) {
event = "!" + event;
}
if (!event) {
throw new Error("Missing event name with t-on directive");
}
@@ -180,6 +183,13 @@ function toMs(s: string): number {
}
function whenTransitionEnd(elm: HTMLElement, cb) {
if (!elm.parentNode) {
// if we get here, this means that the element was removed for some other
// reasons, and in that case, we don't want to work on animation since nothing
// will be displayed anyway.
return;
}
const styles = window.getComputedStyle(elm);
const delays: Array<string> = (styles.transitionDelay || "").split(", ");
const durations: Array<string> = (styles.transitionDuration || "").split(", ");
+9 -4
View File
@@ -174,6 +174,10 @@ function parseXML(xml: string): Document {
return doc;
}
function escapeQuotes(str: string): string {
return str.replace(/\'/g, "\\'");
}
//------------------------------------------------------------------------------
// QWeb rendering engine
//------------------------------------------------------------------------------
@@ -328,10 +332,11 @@ export class QWeb extends EventBus {
) {
throw new Error("Only one conditional branching directive is allowed per node");
}
// All text nodes between branch nodes are removed
// All text (with only spaces) and comment nodes (nodeType 8) between
// branch nodes are removed
let textNode;
while ((textNode = node.previousSibling) !== prevElem) {
if (textNode.nodeValue.trim().length) {
if (textNode.nodeValue.trim().length && textNode.nodeType !== 8) {
throw new Error("text is not allowed between branching directives");
}
textNode.remove();
@@ -693,13 +698,13 @@ export class QWeb extends EventBus {
if ((value = value.trim())) {
let classDef = value
.split(/\s+/)
.map(a => `'${a}':true`)
.map(a => `'${escapeQuotes(a)}':true`)
.join(",");
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = {${classDef}};`);
}
} else {
ctx.addLine(`let _${attID} = '${value}';`);
ctx.addLine(`let _${attID} = '${escapeQuotes(value)}';`);
if (!name.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
name = '"' + name + '"';
+1 -1
View File
@@ -4,7 +4,7 @@ import { Destination, RouterEnv } from "./router";
type Props = Destination;
export class Link<Env extends RouterEnv> extends Component<Env, Props> {
export class Link<Env extends RouterEnv> extends Component<Props, Env> {
static template = xml`
<a t-att-class="{'router-link-active': isActive }"
t-att-href="href"
+1 -1
View File
@@ -1,7 +1,7 @@
import { Component } from "../component/component";
import { xml } from "../tags";
export class RouteComponent extends Component<any, {}> {
export class RouteComponent extends Component {
static template = xml`
<t>
<t
+1 -1
View File
@@ -83,7 +83,7 @@ interface SelectorOptions {
const isStrictEqual = (a, b) => a === b;
export function useStore(selector, options: SelectorOptions = {}): any {
const component: Component<any, any> = Component.current!;
const component: Component = Component.current!;
const componentId = component.__owl__.id;
const store = options.store || (component.env.store as Store);
if (!(store instanceof Store)) {
+18 -6
View File
@@ -70,8 +70,12 @@ function handleEvent(event: Event, vnode: VNode) {
on = (vnode.data as VNodeData).on;
// call event handler(s) if exists
if (on && on[name]) {
invokeHandler(on[name], vnode, event);
if (on) {
if (on[name]) {
invokeHandler(on[name], vnode, event);
} else if (on["!" + name]) {
invokeHandler(on["!" + name], vnode, event);
}
}
}
@@ -100,13 +104,17 @@ function updateEventListeners(oldVnode: VNode, vnode?: VNode): void {
if (!on) {
for (name in oldOn) {
// remove listener if element was changed or existing listeners removed
oldElm.removeEventListener(name, oldListener, false);
const capture = name.charAt(0) === "!";
name = capture ? name.slice(1) : name;
oldElm.removeEventListener(name, oldListener, capture);
}
} else {
for (name in oldOn) {
// remove listener if existing listener removed
if (!on[name]) {
oldElm.removeEventListener(name, oldListener, false);
const capture = name.charAt(0) === "!";
name = capture ? name.slice(1) : name;
oldElm.removeEventListener(name, oldListener, capture);
}
}
}
@@ -123,13 +131,17 @@ function updateEventListeners(oldVnode: VNode, vnode?: VNode): void {
if (!oldOn) {
for (name in on) {
// add listener if element was changed or new listeners added
elm.addEventListener(name, listener, false);
const capture = name.charAt(0) === "!";
name = capture ? name.slice(1) : name;
elm.addEventListener(name, listener, capture);
}
} else {
for (name in on) {
// add listener if new listener added
if (!oldOn[name]) {
elm.addEventListener(name, listener, false);
const capture = name.charAt(0) === "!";
name = capture ? name.slice(1) : name;
elm.addEventListener(name, listener, capture);
}
}
}
+24 -3
View File
@@ -28,7 +28,7 @@ exports[`animations t-transition combined with component 1`] = `
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
const __patch2 = w2.__patch;
w2.__patch = fiber => {__patch2.call(w2, fiber); if(!w2.__owl__.transitionInserted){w2.__owl__.transitionInserted = true;utils.transitionInsert(w2.__owl__.vnode, 'chimay');}};
w2.__patch = (t, vn) => {__patch2.call(w2, t, vn); if(!w2.__owl__.transitionInserted){w2.__owl__.transitionInserted = true;utils.transitionInsert(w2.__owl__.vnode, 'chimay');}};
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {let finalize = () => {
@@ -73,7 +73,7 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
const __patch2 = w2.__patch;
w2.__patch = fiber => {__patch2.call(w2, fiber); if(!w2.__owl__.transitionInserted){w2.__owl__.transitionInserted = true;utils.transitionInsert(w2.__owl__.vnode, 'chimay');}};
w2.__patch = (t, vn) => {__patch2.call(w2, t, vn); if(!w2.__owl__.transitionInserted){w2.__owl__.transitionInserted = true;utils.transitionInsert(w2.__owl__.vnode, 'chimay');}};
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {let finalize = () => {
@@ -119,7 +119,7 @@ exports[`animations t-transition combined with t-component, remove and re-add be
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
const __patch2 = w2.__patch;
w2.__patch = fiber => {__patch2.call(w2, fiber); if(!w2.__owl__.transitionInserted){w2.__owl__.transitionInserted = true;utils.transitionInsert(w2.__owl__.vnode, 'chimay');}};
w2.__patch = (t, vn) => {__patch2.call(w2, t, vn); if(!w2.__owl__.transitionInserted){w2.__owl__.transitionInserted = true;utils.transitionInsert(w2.__owl__.vnode, 'chimay');}};
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {let finalize = () => {
@@ -177,3 +177,24 @@ exports[`animations t-transition, on a simple node (insert) 1`] = `
return vn1;
}"
`;
exports[`animations t-transition, on a simple node, not in the DOM 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('span', p1, c1);
p1.hook = {
insert: vn => {
utils.transitionInsert(vn, 'chimay');
},
remove: (vn, rm) => {
utils.transitionRemove(vn, 'chimay', rm);
},
};
c1.push({text: \`blue\`});
return vn1;
}"
`;
+23
View File
@@ -78,6 +78,7 @@ describe("animations", () => {
def.resolve();
});
let node: HTMLElement = <HTMLElement>renderToDOM(qweb, "test");
fixture.appendChild(node);
expect(node.className).toBe("chimay-enter chimay-enter-active");
await def; // wait for the mocked repaint to be done
@@ -85,6 +86,27 @@ describe("animations", () => {
expect(node.className).toBe("");
});
test("t-transition, on a simple node, not in the DOM", async () => {
expect.assertions(5);
qweb.addTemplate("test", `<span t-transition="chimay">blue</span>`);
let def = makeDeferred();
patchNextFrame(cb => {
expect(node.className).toBe("chimay-enter chimay-enter-active");
cb();
expect(node.className).toBe("chimay-enter-active chimay-enter-to");
def.resolve();
});
let node: HTMLElement = <HTMLElement>renderToDOM(qweb, "test");
expect(node.className).toBe("chimay-enter chimay-enter-active");
await def; // wait for the mocked repaint to be done
node.dispatchEvent(new Event("transitionend"));
// we check here that the css classes have not been removed, since the
// element is not in the dom, we actually do not want to do anything.
expect(node.className).toBe("chimay-enter-active chimay-enter-to");
});
test("t-transition with no delay/duration", async () => {
expect.assertions(4);
qweb.addTemplate("test", `<span t-transition="jupiler">blue</span>`);
@@ -97,6 +119,7 @@ describe("animations", () => {
def.resolve();
});
let node: HTMLElement = <HTMLElement>renderToDOM(qweb, "test");
fixture.appendChild(node);
expect(node.className).toBe("jupiler-enter jupiler-enter-active");
await def;
});
@@ -0,0 +1,156 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`class and style attributes with t-component dynamic t-att-style is properly added and updated on widget root el 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"ParentWidget\\"
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 'child'
const _4 = scope['state'].style;
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, undefined).then(()=>{if (w2.__owl__.isDestroyed) {return};w2.el.style=_4;});;
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`child\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
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.style = _4;}});});
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[`class and style attributes with t-component t-att-class is properly added/removed on widget root el (v2) 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"ParentWidget\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
context.__owl__.refs = context.__owl__.refs || {};
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
// Component 'Child'
const ref4 = \`child\`;
let _5 = {'a':true};
Object.assign(_5, {b:scope['state'].b})
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, undefined);
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`Child\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
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){}});});
let pvnode = h('dummy', {key: '__3__', hook: {insert(vn) {context.__owl__.refs[ref4] = w2;},remove() {},destroy(vn) {w2.destroy();delete context.__owl__.refs[ref4];}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
}
w2.__owl__.classObj=_5;
w2.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
exports[`class and style attributes with t-component t-att-class is properly added/removed on widget root el (v2) 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"Child\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let _7 = {'c':true};
Object.assign(_7, utils.toObj({d:scope['state'].d}))
let c8 = [], p8 = {key:8,class:_7};
let vn8 = h('span', p8, c8);
return vn8;
}"
`;
exports[`class and style attributes with t-component t-att-class is properly added/removed on widget root el (v3) 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"ParentWidget\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
context.__owl__.refs = context.__owl__.refs || {};
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
// Component 'Child'
const ref4 = \`child\`;
let _5 = {'a':true};
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 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 componentKey2 = \`Child\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
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){}});});
let pvnode = h('dummy', {key: '__3__', hook: {insert(vn) {context.__owl__.refs[ref4] = w2;},remove() {},destroy(vn) {w2.destroy();delete context.__owl__.refs[ref4];}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
}
w2.__owl__.classObj=_5;
w2.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
exports[`class and style attributes with t-component t-att-class is properly added/removed on widget root el (v3) 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"Child\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let _7 = {'c':true};
Object.assign(_7, utils.toObj(scope['state'].d?'d':''))
let c8 = [], p8 = {key:8,class:_7};
let vn8 = h('span', p8, c8);
return vn8;
}"
`;
@@ -257,161 +257,6 @@ exports[`basic widget properties t-key on a component with t-if, and a sibling c
}"
`;
exports[`class and style attributes with t-component dynamic t-att-style is properly added and updated on widget root el 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"ParentWidget\\"
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 'child'
const _4 = scope['state'].style;
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, undefined).then(()=>{if (w2.__owl__.isDestroyed) {return};w2.el.style=_4;});;
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`child\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.style = _4;}};});
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[`class and style attributes with t-component t-att-class is properly added/removed on widget root el (v2) 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"ParentWidget\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
context.__owl__.refs = context.__owl__.refs || {};
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
// Component 'Child'
const ref4 = \`child\`;
let _5 = {'a':true};
Object.assign(_5, {b:scope['state'].b})
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, undefined);
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`Child\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){}};});
let pvnode = h('dummy', {key: '__3__', hook: {insert(vn) {context.__owl__.refs[ref4] = w2;},remove() {},destroy(vn) {w2.destroy();delete context.__owl__.refs[ref4];}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
}
w2.__owl__.classObj=_5;
w2.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
exports[`class and style attributes with t-component t-att-class is properly added/removed on widget root el (v2) 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"Child\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let _7 = {'c':true};
Object.assign(_7, utils.toObj({d:scope['state'].d}))
let c8 = [], p8 = {key:8,class:_7};
let vn8 = h('span', p8, c8);
return vn8;
}"
`;
exports[`class and style attributes with t-component t-att-class is properly added/removed on widget root el (v3) 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"ParentWidget\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
context.__owl__.refs = context.__owl__.refs || {};
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
// Component 'Child'
const ref4 = \`child\`;
let _5 = {'a':true};
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 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 componentKey2 = \`Child\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){}};});
let pvnode = h('dummy', {key: '__3__', hook: {insert(vn) {context.__owl__.refs[ref4] = w2;},remove() {},destroy(vn) {w2.destroy();delete context.__owl__.refs[ref4];}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
}
w2.__owl__.classObj=_5;
w2.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
exports[`class and style attributes with t-component t-att-class is properly added/removed on widget root el (v3) 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"Child\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let _7 = {'c':true};
Object.assign(_7, utils.toObj(scope['state'].d?'d':''))
let c8 = [], p8 = {key:8,class:_7};
let vn8 = h('span', p8, c8);
return vn8;
}"
`;
exports[`composition sub components with some state rendered in a loop 1`] = `
"function anonymous(context, extra
) {
@@ -549,6 +394,66 @@ exports[`composition t-component with dynamic value 2 1`] = `
}"
`;
exports[`composition t-ref on a node, and t-on-click 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__1\\"
context.__owl__.refs = context.__owl__.refs || {};
let h = this.h;
let c4 = [], p4 = {key:4};
let vn4 = h('div', p4, c4);
const ref5 = \`nibor\`;
p4.hook = {
create: (_, n) => {
context.__owl__.refs[ref5] = n.elm;
},
destroy: () => {
delete context.__owl__.refs[ref5];
},
};
c4.push({text: \`nibor\`});
return vn4;
}"
`;
exports[`composition t-ref on a node, and t-on-click 2`] = `
"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 'Child'
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, undefined);
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`Child\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
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__.isMounted){return}utils.getComponent(context)['doSomething'](e);});}});});
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[`dynamic t-props basic use 1`] = `
"function anonymous(context, extra
) {
@@ -837,6 +742,44 @@ exports[`other directives with t-component t-on method call in t-foreach 1`] = `
}"
`;
exports[`other directives with t-component t-on with .capture modifier 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 'Child'
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, undefined);
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`Child\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
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__.isMounted){return}utils.getComponent(context)['capture'](e);}, true);}});});
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[`other directives with t-component t-on with getter as handler 1`] = `
"function anonymous(context, extra
) {
@@ -869,7 +812,7 @@ exports[`other directives with t-component t-on with getter as handler 1`] = `
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3);
parent.__owl__.cmap['__4__'] = w3.__owl__.id;
let fiber = w3.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (!context.__owl__.isMounted){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();}}});
c1.push(pvnode);
w3.__owl__.pvnode = pvnode;
@@ -908,7 +851,7 @@ exports[`other directives with t-component t-on with handler bound to argument 1
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (!context.__owl__.isMounted){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();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
@@ -947,7 +890,7 @@ exports[`other directives with t-component t-on with handler bound to empty obje
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (!context.__owl__.isMounted){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();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
@@ -986,7 +929,7 @@ exports[`other directives with t-component t-on with handler bound to empty obje
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (!context.__owl__.isMounted){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();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
@@ -1025,7 +968,7 @@ exports[`other directives with t-component t-on with handler bound to object 1`]
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (!context.__owl__.isMounted){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();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
@@ -1068,7 +1011,7 @@ exports[`other directives with t-component t-on with inline statement 1`] = `
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3);
parent.__owl__.cmap['__4__'] = w3.__owl__.id;
let fiber = w3.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (!context.__owl__.isMounted){return}state_5.counter++});}};});
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();}}});
c1.push(pvnode);
w3.__owl__.pvnode = pvnode;
@@ -1106,7 +1049,7 @@ exports[`other directives with t-component t-on with no handler (only modifiers)
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (!context.__owl__.isMounted){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();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
@@ -1144,7 +1087,7 @@ exports[`other directives with t-component t-on with prevent and self modifiers
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {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 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();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
@@ -1182,7 +1125,7 @@ exports[`other directives with t-component t-on with self and prevent modifiers
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {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 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();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
@@ -1220,7 +1163,7 @@ exports[`other directives with t-component t-on with self modifier 1`] = `
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {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 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();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
@@ -1258,7 +1201,7 @@ exports[`other directives with t-component t-on with stop and/or prevent modifie
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {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 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();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
@@ -1562,7 +1505,7 @@ exports[`random stuff/miscellaneous t-on with handler bound to dynamic argument
if (!W6) {throw new Error('Cannot find the definition of component \\"' + componentKey6 + '\\"')}
w6 = new W6(parent, props6);
parent.__owl__.cmap[k7] = w6.__owl__.id;
let fiber = w6.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (!context.__owl__.isMounted){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();}}});
c1.push(pvnode);
w6.__owl__.pvnode = pvnode;
File diff suppressed because it is too large Load Diff
+271
View File
@@ -0,0 +1,271 @@
import { Component, Env } from "../../src/component/component";
import { QWeb } from "../../src/qweb/qweb";
import { xml } from "../../src/tags";
import { useState, useRef } from "../../src/hooks";
import { makeTestFixture, makeTestEnv, nextTick } from "../helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
// We create before each test:
// - fixture: a div, appended to the DOM, intended to be the target of dom
// manipulations. Note that it is removed after each test.
// - env: a WEnv, necessary to create new components
let fixture: HTMLElement;
let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
Component.env = env;
});
afterEach(() => {
fixture.remove();
});
describe("class and style attributes with t-component", () => {
test("class is properly added on widget root el", async () => {
class Child extends Component {
static template = xml`<div class="c"/>`;
}
class ParentWidget extends Component {
static template = xml`<div><Child class="a b"/></div>`;
static components = { Child };
}
const widget = new ParentWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(`<div><div class="c a b"></div></div>`);
});
test("empty class attribute is not added on widget root el", async () => {
class Child extends Component {
static template = xml`<span/>`;
}
class Parent extends Component {
static template = xml`<div><Child class=""/></div>`;
static components = { Child };
}
const widget = new Parent();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(`<div><span></span></div>`);
});
test("t-att-class is properly added/removed on widget root el", async () => {
class Child extends Component {
static template = xml`<div class="c"/>`;
}
class ParentWidget extends Component {
static template = xml`<div><Child t-att-class="{a:state.a, b:state.b}"/></div>`;
static components = { Child };
state = useState({ a: true, b: false });
}
const widget = new ParentWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(`<div><div class="c a"></div></div>`);
expect(QWeb.TEMPLATES[ParentWidget.template].fn.toString());
widget.state.a = false;
widget.state.b = true;
await nextTick();
expect(fixture.innerHTML).toBe(`<div><div class="c b"></div></div>`);
});
test("class with extra whitespaces", async () => {
env.qweb.addTemplate(
"ParentWidget",
`<div>
<Child class="a b c d"/>
</div>`
);
class Child extends Component {}
class ParentWidget extends Component {
static components = { Child };
}
env.qweb.addTemplate("Child", `<div/>`);
const widget = new ParentWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(`<div><div class="a b c d"></div></div>`);
});
test("t-att-class is properly added/removed on widget root el (v2)", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="ParentWidget">
<Child class="a" t-att-class="{ b: state.b }" t-ref="child"/>
</div>
<span t-name="Child" class="c" t-att-class="{ d: state.d }"/>
</templates>`);
class Child extends Component {
state = useState({ d: true });
}
class ParentWidget extends Component {
static components = { Child };
state = useState({ b: true });
child = useRef("child");
}
const widget = new ParentWidget();
await widget.mount(fixture);
const span = fixture.querySelector("span")!;
expect(span.className).toBe("c d a b");
widget.state.b = false;
await nextTick();
expect(span.className).toBe("c d a");
(widget.child.comp as Child).state.d = false;
await nextTick();
expect(span.className).toBe("c a");
widget.state.b = true;
await nextTick();
expect(span.className).toBe("c a b");
(widget.child.comp as Child).state.d = true;
await nextTick();
expect(span.className).toBe("c a b d");
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
expect(env.qweb.templates.Child.fn.toString()).toMatchSnapshot();
});
test("t-att-class is properly added/removed on widget root el (v3)", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="ParentWidget">
<Child class="a" t-att-class="state.b ? 'b' : ''" t-ref="child"/>
</div>
<span t-name="Child" class="c" t-att-class="state.d ? 'd' : ''"/>
</templates>`);
class Child extends Component {
state = useState({ d: true });
}
class ParentWidget extends Component {
static components = { Child };
state = useState({ b: true });
child = useRef("child");
}
const widget = new ParentWidget();
await widget.mount(fixture);
const span = fixture.querySelector("span")!;
expect(span.className).toBe("c d a b");
widget.state.b = false;
await nextTick();
expect(span.className).toBe("c d a");
(widget.child.comp as Child).state.d = false;
await nextTick();
expect(span.className).toBe("c a");
widget.state.b = true;
await nextTick();
expect(span.className).toBe("c a b");
(widget.child.comp as Child).state.d = true;
await nextTick();
expect(span.className).toBe("c a b d");
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
expect(env.qweb.templates.Child.fn.toString()).toMatchSnapshot();
});
test("class on components do not interfere with user defined classes", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="App" t-att-class="{ c: state.c }" />
</templates>`);
class App extends Component {
state = useState({ c: true });
mounted() {
this.el!.classList.add("user");
}
}
const widget = new App();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe('<div class="c user"></div>');
widget.state.c = false;
await nextTick();
expect(fixture.innerHTML).toBe('<div class="user"></div>');
});
test("style is properly added on widget root el", async () => {
env.qweb.addTemplate(
"ParentWidget",
`
<div>
<t t-component="child" style="font-weight: bold;"/>
</div>`
);
class SomeComponent extends Component {
static template = xml`<div/>`;
}
class ParentWidget extends Component {
static components = { child: SomeComponent };
}
const widget = new ParentWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(`<div><div style="font-weight: bold;"></div></div>`);
});
test("dynamic t-att-style is properly added and updated on widget root el", async () => {
env.qweb.addTemplate(
"ParentWidget",
`
<div>
<t t-component="child" t-att-style="state.style"/>
</div>`
);
class SomeComponent extends Component {
static template = xml`<div/>`;
}
class ParentWidget extends Component {
static components = { child: SomeComponent };
state = useState({ style: "font-size: 20px" });
}
const widget = new ParentWidget();
await widget.mount(fixture);
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
expect(fixture.innerHTML).toBe(`<div><div style="font-size: 20px;"></div></div>`);
widget.state.style = "font-size: 30px";
await nextTick();
expect(fixture.innerHTML).toBe(`<div><div style="font-size: 30px;"></div></div>`);
});
test("error in subcomponent with class", async () => {
class Child extends Component {
static template = xml`<div t-esc="this.will.crash"/>`;
}
class ParentWidget extends Component {
static template = xml`<div><Child class="a"/></div>`;
static components = { Child };
}
const widget = new ParentWidget();
let error;
try {
await widget.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot read property 'crash' of undefined");
expect(fixture.innerHTML).toBe("");
});
});
File diff suppressed because it is too large Load Diff
+523
View File
@@ -0,0 +1,523 @@
import { Component, Env } from "../../src/component/component";
import { useState } from "../../src/hooks";
import { xml } from "../../src/tags";
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
// We create before each test:
// - fixture: a div, appended to the DOM, intended to be the target of dom
// manipulations. Note that it is removed after each test.
// - env: a WEnv, necessary to create new components
let fixture: HTMLElement;
let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
Component.env = env;
});
afterEach(() => {
fixture.remove();
});
describe("component error handling (catchError)", () => {
/**
* This test suite requires often to wait for 3 ticks. Here is why:
* - First tick is to let the app render and crash.
* - When we crash, we call the catchError handler in a setTimeout (because we
* need to wait for the previous rendering to be completely stopped). So, we
* need to wait for the second tick.
* - Then, when the handler changes the state, we need to wait for the interface
* to be rerendered.
* */
test("can catch an error in a component render function", async () => {
const consoleError = console.error;
console.error = jest.fn();
const handler = jest.fn();
env.qweb.on("error", null, handler);
class ErrorComponent extends Component {
static template = xml`<div>hey<t t-esc="props.flag and state.this.will.crash"/></div>`;
}
class ErrorBoundary extends Component {
static template = xml`
<div>
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>`;
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Component {
static template = xml`
<div>
<ErrorBoundary><ErrorComponent flag="state.flag"/></ErrorBoundary>
</div>`;
state = useState({ flag: false });
static components = { ErrorBoundary, ErrorComponent };
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><div>heyfalse</div></div></div>");
app.state.flag = true;
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
expect(handler).toBeCalledTimes(1);
});
test("no component catching error lead to full app destruction", async () => {
expect.assertions(6);
const handler = jest.fn();
env.qweb.on("error", null, handler);
const consoleError = console.error;
console.error = jest.fn();
class ErrorComponent extends Component {
static template = xml`<div>hey<t t-esc="props.flag and state.this.will.crash"/></div>`;
}
class App extends Component {
static template = xml`<div><ErrorComponent flag="state.flag"/></div>`;
static components = { ErrorComponent };
state = useState({ flag: false });
async render() {
try {
await super.render();
} catch (e) {
expect(e.message).toBe("Cannot read property 'this' of undefined");
}
}
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>heyfalse</div></div>");
app.state.flag = true;
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
expect(app.__owl__.isDestroyed).toBe(true);
expect(handler).toBeCalledTimes(1);
});
test("can catch an error in the initial call of a component render function (parent mounted)", async () => {
const handler = jest.fn();
env.qweb.on("error", null, handler);
const consoleError = console.error;
console.error = jest.fn();
class ErrorComponent extends Component {
static template = xml`<div>hey<t t-esc="state.this.will.crash"/></div>`;
}
class ErrorBoundary extends Component {
static template = xml`
<div>
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>`;
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Component {
static template = xml`
<div>
<ErrorBoundary><ErrorComponent /></ErrorBoundary>
</div>`;
static components = { ErrorBoundary, ErrorComponent };
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
expect(handler).toBeCalledTimes(1);
});
test("can catch an error in the initial call of a component render function (parent updated)", async () => {
const handler = jest.fn();
env.qweb.on("error", null, handler);
const consoleError = console.error;
console.error = jest.fn();
class ErrorComponent extends Component {
static template = xml`<div>hey<t t-esc="state.this.will.crash"/></div>`;
}
class ErrorBoundary extends Component {
static template = xml`
<div>
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>`;
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Component {
static template = xml`
<div>
<ErrorBoundary t-if="state.flag"><ErrorComponent /></ErrorBoundary>
</div>`;
state = useState({ flag: false });
static components = { ErrorBoundary, ErrorComponent };
}
const app = new App();
await app.mount(fixture);
app.state.flag = true;
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
expect(handler).toBeCalledTimes(1);
});
test("can catch an error in the constructor call of a component render function", async () => {
const handler = jest.fn();
env.qweb.on("error", null, handler);
const consoleError = console.error;
console.error = jest.fn();
env.qweb.addTemplates(`
<templates>
<div t-name="ErrorBoundary">
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>
<div t-name="ErrorComponent">Some text</div>
<div t-name="App">
<ErrorBoundary><ErrorComponent /></ErrorBoundary>
</div>
</templates>`);
class ErrorComponent extends Component {
constructor(parent) {
super(parent);
throw new Error("NOOOOO");
}
}
class ErrorBoundary extends Component {
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Component {
static components = { ErrorBoundary, ErrorComponent };
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
expect(handler).toBeCalledTimes(1);
});
test("can catch an error in the willStart call", async () => {
const consoleError = console.error;
console.error = jest.fn();
class ErrorComponent extends Component {
static template = xml`<div t-name="ErrorComponent">Some text</div>`;
async willStart() {
// we wait a little bit to be in a different stack frame
await nextTick();
throw new Error("NOOOOO");
}
}
class ErrorBoundary extends Component {
static template = xml`
<div>
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>`;
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Component {
static template = xml`<div><ErrorBoundary><ErrorComponent /></ErrorBoundary></div>`;
static components = { ErrorBoundary, ErrorComponent };
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
test.skip("can catch an error in the mounted call", async () => {
// we do not catch error in mounted anymore
console.error = jest.fn();
env.qweb.addTemplates(`
<templates>
<div t-name="ErrorBoundary">
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>
<div t-name="ErrorComponent">Some text</div>
<div t-name="App">
<ErrorBoundary><ErrorComponent /></ErrorBoundary>
</div>
</templates>`);
class ErrorComponent extends Component {
mounted() {
throw new Error("NOOOOO");
}
}
class ErrorBoundary extends Component {
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Component {
static components = { ErrorBoundary, ErrorComponent };
}
const app = new App();
await app.mount(fixture);
await nextTick();
await nextTick();
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
});
test.skip("can catch an error in the willPatch call", async () => {
// we do not catch error in willPatch anymore
const consoleError = console.error;
console.error = jest.fn();
class ErrorComponent extends Component {
static template = xml`<div><t t-esc="props.message"/></div>`;
willPatch() {
throw new Error("NOOOOO");
}
}
class ErrorBoundary extends Component {
static template = xml`
<div>
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>`;
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Component {
static template = xml`
<div>
<span><t t-esc="state.message"/></span>
<ErrorBoundary><ErrorComponent message="state.message" /></ErrorBoundary>
</div>`;
state = useState({ message: "abc" });
static components = { ErrorBoundary, ErrorComponent };
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>abc</span><div><div>abc</div></div></div>");
app.state.message = "def";
await nextTick();
await nextTick();
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>def</span><div>Error handled</div></div>");
expect(console.error).toHaveBeenCalledTimes(1);
console.error = consoleError;
});
test("a rendering error will reject the mount promise", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
// we do not catch error in willPatch anymore
class App extends Component {
static template = xml`<div><t t-esc="this.will.crash"/></div>`;
}
const app = new App();
let error;
try {
await app.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot read property 'crash' of undefined");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
test("an error in mounted call will reject the mount promise", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
class App extends Component {
static template = xml`<div>abc</div>`;
mounted() {
throw new Error("boom");
}
}
const app = new App();
let error;
try {
await app.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("boom");
expect(fixture.innerHTML).toBe("");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
test("an error in willPatch call will reject the render promise", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
class App extends Component {
static template = xml`<div><t t-esc="val"/></div>`;
val = 3;
willPatch() {
throw new Error("boom");
}
}
const app = new App();
await app.mount(fixture);
app.val = 4;
let error;
try {
await app.render();
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("boom");
expect(fixture.innerHTML).toBe("");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
test("an error in patched call will reject the render promise", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
class App extends Component {
static template = xml`<div><t t-esc="val"/></div>`;
val = 3;
patched() {
throw new Error("boom");
}
}
const app = new App();
await app.mount(fixture);
app.val = 4;
let error;
try {
await app.render();
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("boom");
expect(fixture.innerHTML).toBe("");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
test("a rendering error in a sub component will reject the mount promise", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
// we do not catch error in willPatch anymore
class Child extends Component {
static template = xml`<div><t t-esc="this.will.crash"/></div>`;
}
class App extends Component {
static template = xml`<div><Child/></div>`;
static components = { Child };
}
const app = new App();
let error;
try {
await app.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot read property 'crash' of undefined");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
test("a rendering error will reject the render promise", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
// we do not catch error in willPatch anymore
class App extends Component {
static template = xml`<div><t t-if="flag" t-esc="this.will.crash"/></div>`;
flag = false;
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div></div>");
app.flag = true;
let error;
try {
await app.render();
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot read property 'crash' of undefined");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
test("a rendering error will reject the render promise (with sub components)", async () => {
class Child extends Component {
static template = xml`<span></span>`;
}
class Parent extends Component {
static template = xml`<div><Child/><t t-esc="x.y"/></div>`;
static components = { Child };
}
let error;
try {
const parent = new Parent();
await parent.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot read property 'y' of undefined");
});
});
+25 -25
View File
@@ -25,7 +25,7 @@ afterEach(() => {
QWeb.dev = dev;
});
class Widget extends Component<any, any> {}
class Widget extends Component {}
//------------------------------------------------------------------------------
// Tests
@@ -96,7 +96,7 @@ describe("props validation", () => {
];
let props;
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`<div><TestWidget p="p"/></div>`;
get p() {
return props.p;
@@ -153,14 +153,14 @@ describe("props validation", () => {
];
let props;
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`<div><TestWidget p="p"/></div>`;
get p() {
return props.p;
}
}
for (let test of Tests) {
let TestWidget = class extends Component<any, any> {
let TestWidget = class extends Component {
static props = { p: { type: test.type } };
static template = xml`<div>hey</div>`;
};
@@ -200,11 +200,11 @@ describe("props validation", () => {
});
test("can validate a prop with multiple types", async () => {
class TestWidget extends Component<any, any> {
class TestWidget extends Component {
static template = xml`<div>hey</div>`;
static props = { p: [String, Boolean] };
}
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`<div><TestWidget p="p"/></div>`;
static components = { TestWidget };
get p() {
@@ -244,11 +244,11 @@ describe("props validation", () => {
});
test("can validate an optional props", async () => {
class TestWidget extends Component<any, any> {
class TestWidget extends Component {
static template = xml`<div>hey</div>`;
static props = { p: { type: String, optional: true } };
}
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`<div><TestWidget p="p"/></div>`;
static components = { TestWidget };
get p() {
@@ -288,11 +288,11 @@ describe("props validation", () => {
});
test("can validate an array with given primitive type", async () => {
class TestWidget extends Component<any, any> {
class TestWidget extends Component {
static template = xml`<div>hey</div>`;
static props = { p: { type: Array, element: String } };
}
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`<div><TestWidget p="p"/></div>`;
static components = { TestWidget };
get p() {
@@ -340,11 +340,11 @@ describe("props validation", () => {
});
test("can validate an array with multiple sub element types", async () => {
class TestWidget extends Component<any, any> {
class TestWidget extends Component {
static template = xml`<div>hey</div>`;
static props = { p: { type: Array, element: [String, Boolean] } };
}
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`<div><TestWidget p="p"/></div>`;
static components = { TestWidget };
get p() {
@@ -393,13 +393,13 @@ describe("props validation", () => {
});
test("can validate an object with simple shape", async () => {
class TestWidget extends Component<any, any> {
class TestWidget extends Component {
static template = xml`<div>hey</div>`;
static props = {
p: { type: Object, shape: { id: Number, url: String } }
};
}
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`<div><TestWidget p="p"/></div>`;
static components = { TestWidget };
get p() {
@@ -451,7 +451,7 @@ describe("props validation", () => {
});
test("can validate recursively complicated prop def", async () => {
class TestWidget extends Component<any, any> {
class TestWidget extends Component {
static template = xml`<div>hey</div>`;
static props = {
p: {
@@ -463,7 +463,7 @@ describe("props validation", () => {
}
};
}
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`<div><TestWidget p="p"/></div>`;
static components = { TestWidget };
get p() {
@@ -503,7 +503,7 @@ describe("props validation", () => {
});
test("can validate optional attributes in nested sub props", () => {
class TestComponent extends Component<any, any> {
class TestComponent extends Component {
static props = {
myprop: {
type: Array,
@@ -536,7 +536,7 @@ describe("props validation", () => {
});
test("can validate with a custom validator", () => {
class TestComponent extends Component<any, any> {
class TestComponent extends Component {
static props = {
size: {
validate: e => ["small", "medium", "large"].includes(e)
@@ -562,7 +562,7 @@ describe("props validation", () => {
test("can validate with a custom validator, and a type", () => {
const validator = jest.fn(n => 0 <= n && n <= 10);
class TestComponent extends Component<any, any> {
class TestComponent extends Component {
static props = {
n: {
type: Number,
@@ -738,7 +738,7 @@ describe("props validation", () => {
test("props are validated whenever component is updated", async () => {
let error;
class TestWidget extends Component<any, any> {
class TestWidget extends Component {
static props = { p: { type: Number } };
static template = xml`<div><t t-esc="props.p"/></div>`;
@@ -750,7 +750,7 @@ describe("props validation", () => {
}
}
}
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`<div><TestWidget p="state.p"/></div>`;
static components = { TestWidget };
state: any = useState({ p: 1 });
@@ -767,12 +767,12 @@ describe("props validation", () => {
});
test("default values are applied before validating props at update", async () => {
class TestWidget extends Component<any, any> {
class TestWidget extends Component {
static props = { p: { type: Number } };
static template = xml`<div><t t-esc="props.p"/></div>`;
static defaultProps = { p: 4 };
}
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`<div><TestWidget p="state.p"/></div>`;
static components = { TestWidget };
state: any = useState({ p: 1 });
@@ -790,11 +790,11 @@ describe("props validation", () => {
describe("default props", () => {
test("can set default values", async () => {
class TestWidget extends Component<any, any> {
class TestWidget extends Component {
static defaultProps = { p: 4 };
static template = xml`<div><t t-esc="props.p"/></div>`;
}
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`<div><TestWidget /></div>`;
static components = { TestWidget };
}
+70 -70
View File
@@ -26,7 +26,7 @@ afterEach(() => {
fixture.remove();
});
function children(w: Component<any, any>): Component<any, any>[] {
function children(w: Component): Component[] {
const childrenMap = w.__owl__.children;
return Object.keys(childrenMap).map(id => childrenMap[id]);
}
@@ -51,8 +51,8 @@ describe("t-slot directive", () => {
</div>
</templates>
`);
class Dialog extends Component<any, any> {}
class Parent extends Component<any, any> {
class Dialog extends Component {}
class Parent extends Component {
static components = { Dialog };
}
const parent = new Parent();
@@ -68,13 +68,13 @@ describe("t-slot directive", () => {
});
test("named slots can define a default content", async () => {
class Dialog extends Component<any, any> {
class Dialog extends Component {
static template = xml`
<span>
<t t-slot="header">default content</t>
</span>`;
}
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`<div><Dialog/></div>`;
static components = { Dialog };
}
@@ -86,13 +86,13 @@ describe("t-slot directive", () => {
});
test("dafault slots can define a default content", async () => {
class Dialog extends Component<any, any> {
class Dialog extends Component {
static template = xml`
<span>
<t t-slot="default">default content</t>
</span>`;
}
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`<div><Dialog/></div>`;
static components = { Dialog };
}
@@ -104,13 +104,13 @@ describe("t-slot directive", () => {
});
test("default content is not rendered if slot is provided", async () => {
class Dialog extends Component<any, any> {
class Dialog extends Component {
static template = xml`
<span>
<t t-slot="default">default content</t>
</span>`;
}
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`<div><Dialog>hey</Dialog></div>`;
static components = { Dialog };
}
@@ -121,13 +121,13 @@ describe("t-slot directive", () => {
});
test("default content is not rendered if named slot is provided", async () => {
class Dialog extends Component<any, any> {
class Dialog extends Component {
static template = xml`
<span>
<t t-slot="header">default content</t>
</span>`;
}
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`<div><Dialog><t t-set="header">hey</t></Dialog></div>`;
static components = { Dialog };
}
@@ -149,8 +149,8 @@ describe("t-slot directive", () => {
<span t-name="Dialog"><t t-slot="footer"/></span>
</templates>
`);
class Dialog extends Component<any, any> {}
class Parent extends Component<any, any> {
class Dialog extends Component {}
class Parent extends Component {
static components = { Dialog };
state = useState({ val: 0 });
doSomething() {
@@ -186,9 +186,9 @@ describe("t-slot directive", () => {
</div>
</templates>
`);
class Link extends Component<any, any> {}
class Link extends Component {}
class App extends Component<any, any> {
class App extends Component {
state = useState({
users: [
{ id: 1, name: "Aaron" },
@@ -230,9 +230,9 @@ describe("t-slot directive", () => {
</div>
</templates>
`);
class Link extends Component<any, any> {}
class Link extends Component {}
class App extends Component<any, any> {
class App extends Component {
state = useState({
users: [
{ id: 1, name: "Aaron" },
@@ -272,9 +272,9 @@ describe("t-slot directive", () => {
</div>
</templates>
`);
class Link extends Component<any, any> {}
class Link extends Component {}
class App extends Component<any, any> {
class App extends Component {
state = useState({ user: { id: 1, name: "Aaron" } });
static components = { Link };
}
@@ -294,10 +294,10 @@ describe("t-slot directive", () => {
});
test("refs are properly bound in slots", async () => {
class Dialog extends Component<any, any> {
class Dialog extends Component {
static template = xml`<span><t t-slot="footer"/></span>`;
}
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`
<div>
<span class="counter"><t t-esc="state.val"/></span>
@@ -340,8 +340,8 @@ describe("t-slot directive", () => {
<div t-name="Dialog"><t t-slot="default"/></div>
</templates>
`);
class Dialog extends Component<any, any> {}
class Parent extends Component<any, any> {
class Dialog extends Component {}
class Parent extends Component {
static components = { Dialog };
}
const parent = new Parent();
@@ -360,8 +360,8 @@ describe("t-slot directive", () => {
<div t-name="Dialog"><t t-slot="default"/></div>
</templates>
`);
class Dialog extends Component<any, any> {}
class Parent extends Component<any, any> {
class Dialog extends Component {}
class Parent extends Component {
static components = { Dialog };
}
const parent = new Parent();
@@ -385,8 +385,8 @@ describe("t-slot directive", () => {
<div t-name="Dialog"><t t-slot="content"/></div>
</templates>
`);
class Dialog extends Component<any, any> {}
class Parent extends Component<any, any> {
class Dialog extends Component {}
class Parent extends Component {
static components = { Dialog };
}
const parent = new Parent();
@@ -408,8 +408,8 @@ describe("t-slot directive", () => {
<div t-name="Dialog"><t t-slot="default"/></div>
</templates>
`);
class Dialog extends Component<any, any> {}
class Parent extends Component<any, any> {
class Dialog extends Component {}
class Parent extends Component {
static components = { Dialog };
}
const parent = new Parent();
@@ -432,8 +432,8 @@ describe("t-slot directive", () => {
</span>
</templates>
`);
class Dialog extends Component<any, any> {}
class Parent extends Component<any, any> {
class Dialog extends Component {}
class Parent extends Component {
static components = { Dialog };
}
const parent = new Parent();
@@ -456,8 +456,8 @@ describe("t-slot directive", () => {
</span>
</templates>
`);
class Dialog extends Component<any, any> {}
class Parent extends Component<any, any> {
class Dialog extends Component {}
class Parent extends Component {
static components = { Dialog };
}
const parent = new Parent();
@@ -478,9 +478,9 @@ describe("t-slot directive", () => {
<div t-name="GrandChild">Grand Child</div>
</templates>
`);
class Child extends Component<any, any> {}
class GrandChild extends Component<any, any> {}
class Parent extends Component<any, any> {
class Child extends Component {}
class GrandChild extends Component {}
class Parent extends Component {
static components = { Child, GrandChild };
}
const parent = new Parent();
@@ -499,24 +499,24 @@ describe("t-slot directive", () => {
test("nested slots: evaluation context and parented relationship", async () => {
let slot;
class Slot extends Component<any, any> {
class Slot extends Component {
static template = xml`<span t-esc="props.val"/>`;
constructor(parent, props) {
super(parent, props);
slot = this;
}
}
class GrandChild extends Component<any, any> {
class GrandChild extends Component {
static template = xml`<div><t t-slot="default"/></div>`;
}
class Child extends Component<any, any> {
class Child extends Component {
static components = { GrandChild };
static template = xml`
<GrandChild>
<t t-slot="default"/>
</GrandChild>`;
}
class Parent extends Component<any, any> {
class Parent extends Component {
static components = { Child, Slot };
static template = xml`<Child><Slot val="state.val"/></Child>`;
state = useState({ val: 3 });
@@ -548,9 +548,9 @@ describe("t-slot directive", () => {
</div>
</templates>
`);
class SomeComponent extends Component<any, any> {}
class GenericComponent extends Component<any, any> {}
class App extends Component<any, any> {
class SomeComponent extends Component {}
class GenericComponent extends Component {}
class App extends Component {
static components = { GenericComponent, SomeComponent };
state = useState({ val: 4 });
@@ -568,14 +568,14 @@ describe("t-slot directive", () => {
});
test("slots and wrapper components", async () => {
class Link extends Component<any, any> {
class Link extends Component {
static template = xml`
<a href="abc">
<t t-slot="default"/>
</a>`;
}
class A extends Component<any, any> {
class A extends Component {
static template = xml`<Link>hey</Link>`;
static components = { Link: Link };
}
@@ -587,14 +587,14 @@ describe("t-slot directive", () => {
});
test("template can just return a slot", async () => {
class Child extends Component<any, any> {
class Child extends Component {
static template = xml`<span><t t-esc="props.value"/></span>`;
}
class SlotComponent extends Component<any, any> {
class SlotComponent extends Component {
static template = xml`<t t-slot="default"/>`;
}
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`
<div>
<SlotComponent><Child value="state.value"/></SlotComponent>
@@ -614,13 +614,13 @@ describe("t-slot directive", () => {
});
test("multiple slots containing components", async () => {
class C extends Component<any, any> {
class C extends Component {
static template = xml`<span><t t-esc="props.val"/></span>`;
}
class B extends Component<any, any> {
class B extends Component {
static template = xml`<div><t t-slot="s1"/><t t-slot="s2"/></div>`;
}
class A extends Component<any, any> {
class A extends Component {
static template = xml`
<B>
<t t-set="s1"><C val="1"/></t>
@@ -636,14 +636,14 @@ describe("t-slot directive", () => {
});
test("slots in t-foreach and re-rendering", async () => {
class Child extends Component<any, any> {
class Child extends Component {
static template = xml`<span><t t-esc="state.val"/><t t-slot="default"/></span>`;
state = useState({ val: "A" });
mounted() {
this.state.val = "B";
}
}
class Parent extends Component<any, any> {
class Parent extends Component {
static components = { Child };
static template = xml`
<div>
@@ -661,7 +661,7 @@ describe("t-slot directive", () => {
});
test("slots in t-foreach with t-set and re-rendering", async () => {
class Child extends Component<any, any> {
class Child extends Component {
static template = xml`
<span>
<t t-esc="state.val"/>
@@ -672,7 +672,7 @@ describe("t-slot directive", () => {
this.state.val = "B";
}
}
class ParentWidget extends Component<any, any> {
class ParentWidget extends Component {
static components = { Child };
static template = xml`
<div>
@@ -693,7 +693,7 @@ describe("t-slot directive", () => {
test("nested slots in same template", async () => {
let child, child2, child3;
class Child extends Component<any, any> {
class Child extends Component {
static template = xml`
<span id="c1">
<div>
@@ -705,7 +705,7 @@ describe("t-slot directive", () => {
child = this;
}
}
class Child2 extends Component<any, any> {
class Child2 extends Component {
static template = xml`
<span id="c2">
<t t-slot="default"/>
@@ -715,7 +715,7 @@ describe("t-slot directive", () => {
child2 = this;
}
}
class Child3 extends Component<any, any> {
class Child3 extends Component {
static template = xml`
<span>Child 3</span>`;
constructor(parent, props) {
@@ -723,7 +723,7 @@ describe("t-slot directive", () => {
child3 = this;
}
}
class Parent extends Component<any, any> {
class Parent extends Component {
static components = { Child, Child2, Child3 };
static template = xml`
<span id="parent">
@@ -748,7 +748,7 @@ describe("t-slot directive", () => {
test("t-slot nested within another slot", async () => {
let portal, modal, child3;
class Child3 extends Component<any, any> {
class Child3 extends Component {
static template = xml`
<span>Child 3</span>`;
constructor(parent, props) {
@@ -756,7 +756,7 @@ describe("t-slot directive", () => {
child3 = this;
}
}
class Modal extends Component<any, any> {
class Modal extends Component {
static template = xml`
<span id="modal">
<t t-slot="default"/>
@@ -766,7 +766,7 @@ describe("t-slot directive", () => {
modal = this;
}
}
class Portal extends Component<any, any> {
class Portal extends Component {
static template = xml`
<span id="portal">
<t t-slot="default"/>
@@ -776,7 +776,7 @@ describe("t-slot directive", () => {
portal = this;
}
}
class Dialog extends Component<any, any> {
class Dialog extends Component {
static components = { Modal, Portal };
static template = xml`
<span id="c2">
@@ -787,7 +787,7 @@ describe("t-slot directive", () => {
</Modal>
</span>`;
}
class Parent extends Component<any, any> {
class Parent extends Component {
static components = { Child3, Dialog };
static template = xml`
<span id="c1">
@@ -809,7 +809,7 @@ describe("t-slot directive", () => {
test("t-slot supports many instances", async () => {
let child3;
class Child3 extends Component<any, any> {
class Child3 extends Component {
static template = xml`
<span>Child 3</span>`;
constructor(parent, props) {
@@ -817,13 +817,13 @@ describe("t-slot directive", () => {
child3 = this;
}
}
class Dialog extends Component<any, any> {
class Dialog extends Component {
static template = xml`
<span id="c2">
<t t-slot="default"/>
</span>`;
}
class Parent extends Component<any, any> {
class Parent extends Component {
static components = { Child3, Dialog };
static template = xml`
<span id="c1">
@@ -845,10 +845,10 @@ describe("t-slot directive", () => {
});
test("slots in slots, with vars", async () => {
class B extends Component<any, any> {
class B extends Component {
static template = xml`<span><t t-slot="default"/></span>`;
}
class A extends Component<any, any> {
class A extends Component {
static template = xml`
<div>
<B>
@@ -857,7 +857,7 @@ describe("t-slot directive", () => {
</div>`;
static components = { B };
}
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`
<div>
<t t-set="test" t-value="state.name"/>
+4 -4
View File
@@ -32,7 +32,7 @@ afterEach(() => {
describe("styles and component", () => {
test("can define an inline stylesheet", async () => {
class App extends Component<any, any> {
class App extends Component {
static template = xml`<div class="app">text</div>`;
static style = css`
.app {
@@ -54,7 +54,7 @@ describe("styles and component", () => {
});
test("inherited components properly apply css", async () => {
class App extends Component<any, any> {
class App extends Component {
static template = xml`<div class="app">text</div>`;
static style = css`
.app {
@@ -86,7 +86,7 @@ describe("styles and component", () => {
});
test("get a meaningful error message if css helper is missing", async () => {
class App extends Component<any, any> {
class App extends Component {
static template = xml`<div class="app">text</div>`;
static style = `.app {color: red;}`;
}
@@ -103,7 +103,7 @@ describe("styles and component", () => {
});
test("inline stylesheets are processed", async () => {
class App extends Component<any, any> {
class App extends Component {
static template = xml`<div class="app">text</div>`;
static style = css`
.app {
+473
View File
@@ -0,0 +1,473 @@
import { Component, Env } from "../../src/component/component";
import { useState } from "../../src/hooks";
import { xml } from "../../src/tags";
import { makeDeferred, makeTestEnv, makeTestFixture, nextTick, nextMicroTick } from "../helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
// We create before each test:
// - fixture: a div, appended to the DOM, intended to be the target of dom
// manipulations. Note that it is removed after each test.
// - env: a WEnv, necessary to create new components
let fixture: HTMLElement;
let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
Component.env = env;
});
afterEach(() => {
fixture.remove();
});
describe("mount targets", () => {
test("can attach a component to an existing node (if same tagname)", async () => {
class App extends Component {
static template = xml`<div>app</div>`;
}
const div = document.createElement("div");
fixture.appendChild(div);
const app = new App();
await app.mount(div, { position: "self" });
expect(fixture.innerHTML).toBe("<div>app</div>");
});
test("cannot attach a component to an existing node (if not same tagname)", async () => {
class App extends Component {
static template = xml`<span>app</span>`;
}
const div = document.createElement("div");
fixture.appendChild(div);
const app = new App();
let error;
try {
await app.mount(div, { position: "self" });
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot attach 'App' to target node (not same tag name)");
});
test("can mount a component (with position='first-child')", async () => {
class App extends Component {
static template = xml`<div>app</div>`;
}
const span = document.createElement("span");
fixture.appendChild(span);
const app = new App();
await app.mount(fixture, { position: "first-child" });
expect(fixture.innerHTML).toBe("<div>app</div><span></span>");
});
test("can mount a component (with position='last-child')", async () => {
class App extends Component {
static template = xml`<div>app</div>`;
}
const span = document.createElement("span");
fixture.appendChild(span);
const app = new App();
await app.mount(fixture, { position: "last-child" });
expect(fixture.innerHTML).toBe("<span></span><div>app</div>");
});
test("default mount option is 'last-child'", async () => {
class App extends Component {
static template = xml`<div>app</div>`;
}
const span = document.createElement("span");
fixture.appendChild(span);
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<span></span><div>app</div>");
});
});
describe("unmounting and remounting", () => {
test("widget can be unmounted and remounted", async () => {
const steps: string[] = [];
class MyWidget extends Component {
static template = xml`<div>Hey</div>`;
async willStart() {
steps.push("willstart");
}
mounted() {
steps.push("mounted");
}
willUnmount() {
steps.push("willunmount");
}
patched() {
throw new Error("patched should not be called");
}
}
const w = new MyWidget();
await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div>Hey</div>");
expect(steps).toEqual(["willstart", "mounted"]);
w.unmount();
expect(fixture.innerHTML).toBe("");
expect(steps).toEqual(["willstart", "mounted", "willunmount"]);
await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div>Hey</div>");
expect(steps).toEqual(["willstart", "mounted", "willunmount", "mounted"]);
});
test("widget can be mounted twice without ill effect", async () => {
const steps: string[] = [];
class MyWidget extends Component {
static template = xml`<div>Hey</div>`;
async willStart() {
steps.push("willstart");
}
mounted() {
steps.push("mounted");
}
willUnmount() {
steps.push("willunmount");
}
}
const w = new MyWidget();
await w.mount(fixture);
await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div>Hey</div>");
expect(steps).toEqual(["willstart", "mounted"]);
});
test("state changes in willUnmount do not trigger rerender", async () => {
const steps: string[] = [];
class Child extends Component {
static template = xml`
<span><t t-esc="props.val"/><t t-esc="state.n"/></span>
`;
state = useState({ n: 2 });
__render(f) {
steps.push("render");
return super.__render(f);
}
willPatch() {
steps.push("willPatch");
}
patched() {
steps.push("patched");
}
willUnmount() {
steps.push("willUnmount");
this.state.n = 3;
}
}
class Parent extends Component {
static template = xml`
<div>
<Child t-if="state.flag" val="state.val"/>
</div>
`;
static components = { Child };
state = useState({ val: 1, flag: true });
}
const widget = new Parent();
await widget.mount(fixture);
expect(steps).toEqual(["render"]);
expect(fixture.innerHTML).toBe("<div><span>12</span></div>");
widget.state.flag = false;
await nextTick();
// we make sure here that no call to __render is done
expect(steps).toEqual(["render", "willUnmount"]);
});
test("state changes in willUnmount will be applied on remount", async () => {
class TestWidget extends Component {
static template = xml`
<div><t t-esc="state.val"/></div>
`;
state = useState({ val: 1 });
willUnmount() {
this.state.val = 3;
}
}
const widget = new TestWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div>1</div>");
widget.unmount();
expect(fixture.innerHTML).toBe("");
await nextTick(); // wait for changes to be detected before remounting
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div>3</div>");
// we want to make sure that there are no remaining tasks left at this point.
expect(Component.scheduler.tasks.length).toBe(0);
});
test("sub component is still active after being unmounted and remounted", async () => {
class Child extends Component {
static template = xml`
<p t-on-click="state.value++">
<t t-esc="state.value"/>
</p>`;
state = useState({ value: 1 });
}
class Parent extends Component {
static components = { Child };
static template = xml`<div><Child/></div>`;
}
const w = new Parent();
await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div><p>1</p></div>");
fixture.querySelector("p")!.click();
await nextTick();
expect(fixture.innerHTML).toBe("<div><p>2</p></div>");
w.unmount();
await nextTick();
await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div><p>2</p></div>");
fixture.querySelector("p")!.click();
await nextTick();
expect(fixture.innerHTML).toBe("<div><p>3</p></div>");
});
test("change state just before mounting component", async () => {
const steps: number[] = [];
class TestWidget extends Component {
static template = xml`
<div><t t-esc="state.val"/></div>
`;
state = useState({ val: 1 });
__render(f) {
steps.push(this.state.val);
return super.__render(f);
}
}
TestWidget.prototype.__render = jest.fn(TestWidget.prototype.__render);
const widget = new TestWidget();
widget.state.val = 2;
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div>2</div>");
expect(TestWidget.prototype.__render).toHaveBeenCalledTimes(1);
// unmount and re-mount, as in this case, willStart won't be called, so it's
// slightly different
widget.unmount();
widget.state.val = 3;
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div>3</div>");
expect(TestWidget.prototype.__render).toHaveBeenCalledTimes(2);
expect(steps).toEqual([2, 3]);
});
test("change state while mounting component", async () => {
const steps: number[] = [];
class TestWidget extends Component {
static template = xml`
<div><t t-esc="state.val"/></div>
`;
state = useState({ val: 1 });
__render(f) {
steps.push(this.state.val);
return super.__render(f);
}
}
TestWidget.prototype.__render = jest.fn(TestWidget.prototype.__render);
TestWidget.prototype.__patch = jest.fn(TestWidget.prototype.__patch);
const widget = new TestWidget();
let prom = widget.mount(fixture);
widget.state.val = 2;
await prom;
expect(fixture.innerHTML).toBe("<div>2</div>");
expect(TestWidget.prototype.__render).toHaveBeenCalledTimes(1);
// unmount and re-mount, as in this case, willStart won't be called, so it's
// slightly different
widget.unmount();
prom = widget.mount(fixture);
widget.state.val = 3;
await prom;
expect(fixture.innerHTML).toBe("<div>3</div>");
expect(TestWidget.prototype.__render).toHaveBeenCalledTimes(3);
expect(TestWidget.prototype.__patch).toHaveBeenCalledTimes(2);
expect(steps).toEqual([2, 2, 3]);
});
test("change state while component is unmounted", async () => {
let child;
class Child extends Component {
static template = xml`<span t-esc="state.val"/>`;
state = useState({
val: "C1"
});
constructor(parent, props) {
super(parent, props);
child = this;
}
}
class Parent extends Component {
static components = { Child };
static template = xml`<div><t t-esc="state.val"/><Child/></div>`;
state = useState({ val: "P1" });
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div>P1<span>C1</span></div>");
parent.unmount();
expect(fixture.innerHTML).toBe("");
parent.state.val = "P2";
child.state.val = "C2";
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div>P2<span>C2</span></div>");
});
test("unmount component during a re-rendering", async () => {
const def = makeDeferred();
class Child extends Component {
static template = xml`<span><t t-esc="props.val"/></span>`;
willUpdateProps() {
return def;
}
}
Child.prototype.__render = jest.fn(Child.prototype.__render);
class Parent extends Component {
static template = xml`<div><Child val="state.val"/></div>`;
static components = { Child };
state = useState({ val: 1 });
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
expect(Child.prototype.__render).toBeCalledTimes(1);
parent.state.val = 2;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
parent.unmount();
expect(fixture.innerHTML).toBe("");
def.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(Child.prototype.__render).toBeCalledTimes(1);
});
test("widget can be mounted on different target", async () => {
class MyWidget extends Component {
static template = xml`<div>Hey</div>`;
patched() {
throw new Error("patched should not be called");
}
}
const div = document.createElement("div");
const span = document.createElement("span");
fixture.appendChild(div);
fixture.appendChild(span);
const w = new MyWidget();
await w.mount(div);
expect(fixture.innerHTML).toBe("<div><div>Hey</div></div><span></span>");
await w.mount(span);
expect(fixture.innerHTML).toBe("<div></div><span><div>Hey</div></span>");
});
test("widget can be mounted on different target, another situation", async () => {
const def = makeDeferred();
const steps: string[] = [];
class MyWidget extends Component {
static template = xml`<div>Hey</div>`;
async willStart() {
return def;
}
patched() {
throw new Error("patched should not be called");
}
}
const div = document.createElement("div");
const span = document.createElement("span");
fixture.appendChild(div);
fixture.appendChild(span);
const w = new MyWidget();
w.mount(div).catch(() => steps.push("1 catch"));
await nextTick();
expect(fixture.innerHTML).toBe("<div></div><span></span>");
w.mount(span).then(() => steps.push("2 resolved"));
// we wait two microticks because this is the number of internal promises
// that need to be resolved/rejected, and because we want to prove here
// that the first mount operation is cancelled immediately, and not after
// one full tick.
await nextMicroTick();
await nextMicroTick();
expect(steps).toEqual(["1 catch"]);
await nextTick();
expect(fixture.innerHTML).toBe("<div></div><span></span>");
def.resolve();
await nextTick();
expect(steps).toEqual(["1 catch", "2 resolved"]);
expect(fixture.innerHTML).toBe("<div></div><span><div>Hey</div></span>");
});
test("widget can be mounted on same target, another situation", async () => {
const def = makeDeferred();
const steps: string[] = [];
class MyWidget extends Component {
static template = xml`<div>Hey</div>`;
async willStart() {
return def;
}
patched() {
throw new Error("patched should not be called");
}
}
const w = new MyWidget();
w.mount(fixture).then(() => steps.push("1 resolved"));
await nextTick();
expect(fixture.innerHTML).toBe("");
w.mount(fixture).then(() => steps.push("2 resolved"));
await nextTick();
expect(steps).toEqual([]);
expect(fixture.innerHTML).toBe("");
def.resolve();
await nextTick();
expect(steps).toEqual(["1 resolved", "2 resolved"]);
expect(fixture.innerHTML).toBe("<div>Hey</div>");
});
});
+21 -21
View File
@@ -32,7 +32,7 @@ describe("Context", () => {
test("very simple use, with initial value", async () => {
const testContext = new Context({ value: 123 });
class Test extends Component<any, any> {
class Test extends Component {
static template = xml`<div><t t-esc="contextObj.value"/></div>`;
contextObj = useContext(testContext);
}
@@ -44,7 +44,7 @@ describe("Context", () => {
test("useContext hook is reactive, for one component", async () => {
const testContext = new Context({ value: 123 });
class Test extends Component<any, any> {
class Test extends Component {
static template = xml`<div><t t-esc="contextObj.value"/></div>`;
contextObj = useContext(testContext);
}
@@ -59,11 +59,11 @@ describe("Context", () => {
test("two components can subscribe to same context", async () => {
const testContext = new Context({ value: 123 });
class Child extends Component<any, any> {
class Child extends Component {
static template = xml`<span><t t-esc="contextObj.value"/></span>`;
contextObj = useContext(testContext);
}
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`<div><Child /><Child /></div>`;
static components = { Child };
}
@@ -80,7 +80,7 @@ describe("Context", () => {
const def = makeDeferred();
const steps: string[] = [];
class Child extends Component<any, any> {
class Child extends Component {
static template = xml`<span><t t-esc="contextObj.value"/></span>`;
contextObj = useContext(testContext);
async render() {
@@ -90,7 +90,7 @@ describe("Context", () => {
}
}
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`<div><Child /><Child /></div>`;
static components = { Child };
}
@@ -111,13 +111,13 @@ describe("Context", () => {
const def = makeDeferred();
const steps: string[] = [];
class SlowComp extends Component<any, any> {
class SlowComp extends Component {
static template = xml`<p><t t-esc="props.value"/></p>`;
willUpdateProps() {
return def;
}
}
class Child extends Component<any, any> {
class Child extends Component {
static template = xml`<span><SlowComp value="contextObj.value"/></span>`;
static components = { SlowComp };
contextObj = useContext(testContext);
@@ -128,12 +128,12 @@ describe("Context", () => {
}
}
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`<div><Child /><Child /></div>`;
static components = { Child };
}
class App extends Component<any, any> {
class App extends Component {
static template = xml`<div><Child /><Parent /></div>`;
static components = { Child, Parent };
}
@@ -166,7 +166,7 @@ describe("Context", () => {
const testContext = new Context({ a: 1, b: 2 });
const steps: string[] = [];
class Child extends Component<any, any> {
class Child extends Component {
static template = xml`<span><t t-esc="contextObj1.a"/><t t-esc="contextObj2.b"/></span>`;
contextObj1 = useContext(testContext);
contextObj2 = useContext(testContext);
@@ -175,7 +175,7 @@ describe("Context", () => {
return super.__render(fiber);
}
}
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`<div><Child /></div>`;
static components = { Child };
}
@@ -193,7 +193,7 @@ describe("Context", () => {
const testContext = new Context({ a: 123, b: 321 });
const steps: string[] = [];
class Child extends Component<any, any> {
class Child extends Component {
static template = xml`<span><t t-esc="contextObj.a"/></span>`;
contextObj = useContext(testContext);
__render(fiber) {
@@ -201,7 +201,7 @@ describe("Context", () => {
return super.__render(fiber);
}
}
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`<div><Child /><t t-esc="contextObj.b"/></div>`;
static components = { Child };
contextObj = useContext(testContext);
@@ -227,7 +227,7 @@ describe("Context", () => {
const testContext = new Context({ a: 123 });
const steps: string[] = [];
class Child extends Component<any, any> {
class Child extends Component {
static template = xml`<span><t t-esc="contextObj.a"/></span>`;
contextObj = useContext(testContext);
__render(fiber) {
@@ -235,7 +235,7 @@ describe("Context", () => {
return super.__render(fiber);
}
}
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`<div><Child t-if="state.flag"/></div>`;
static components = { Child };
state = useState({ flag: true });
@@ -263,14 +263,14 @@ describe("Context", () => {
test("destroyed component before being mounted is inactive", async () => {
const testContext = new Context({ a: 123 });
class Child extends Component<any, any> {
class Child extends Component {
static template = xml`<span><t t-esc="contextObj.a"/></span>`;
contextObj = useContext(testContext);
willStart() {
return makeDeferred();
}
}
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`<div><Child t-if="state.flag"/></div>`;
static components = { Child };
state = useState({ flag: true });
@@ -293,7 +293,7 @@ describe("Context", () => {
const testContext = new Context({ x: { n: 1 }, key: "x" });
const def = makeDeferred();
let stateC;
class ComponentC extends Component<any, any> {
class ComponentC extends Component {
static template = xml`<span><t t-esc="context[props.key].n"/><t t-esc="state.x"/></span>`;
context = useContext(testContext);
state = useState({ x: "a" });
@@ -303,7 +303,7 @@ describe("Context", () => {
stateC = this.state;
}
}
class ComponentB extends Component<any, any> {
class ComponentB extends Component {
static components = { ComponentC };
static template = xml`<p><ComponentC key="props.key"/></p>`;
@@ -311,7 +311,7 @@ describe("Context", () => {
return def;
}
}
class ComponentA extends Component<any, any> {
class ComponentA extends Component {
static components = { ComponentB };
static template = xml`<div><ComponentB key="context.key"/></div>`;
context = useContext(testContext);
+28 -28
View File
@@ -42,7 +42,7 @@ afterEach(() => {
describe("hooks", () => {
test("can use a state hook", async () => {
class Counter extends Component<any, any> {
class Counter extends Component {
static template = xml`<div><t t-esc="counter.value"/></div>`;
counter = useState({ value: 42 });
}
@@ -64,7 +64,7 @@ describe("hooks", () => {
steps.push("willunmount");
});
}
class MyComponent extends Component<any, any> {
class MyComponent extends Component {
static template = xml`<div>hey</div>`;
constructor() {
super();
@@ -92,7 +92,7 @@ describe("hooks", () => {
steps.push("willunmount");
});
}
class MyComponent extends Component<any, any> {
class MyComponent extends Component {
static template = xml`<div>hey</div>`;
constructor(parent, props) {
super(parent, props);
@@ -100,7 +100,7 @@ describe("hooks", () => {
}
}
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`<div><MyComponent t-if="state.flag"/></div>`;
static components = { MyComponent };
state = useState({ flag: true });
@@ -126,7 +126,7 @@ describe("hooks", () => {
steps.push("hook:willunmount");
});
}
class MyComponent extends Component<any, any> {
class MyComponent extends Component {
static template = xml`<div>hey</div>`;
constructor() {
super();
@@ -157,7 +157,7 @@ describe("hooks", () => {
steps.push("hook:willunmount");
});
}
class MyComponent extends Component<any, any> {
class MyComponent extends Component {
static template = xml`<div>hey</div>`;
constructor(parent, props) {
super(parent, props);
@@ -171,7 +171,7 @@ describe("hooks", () => {
}
}
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`<div><MyComponent t-if="state.flag"/></div>`;
static components = { MyComponent };
state = useState({ flag: true });
@@ -197,7 +197,7 @@ describe("hooks", () => {
steps.push("hook:willunmount" + i);
});
}
class MyComponent extends Component<any, any> {
class MyComponent extends Component {
static template = xml`<div>hey</div>`;
constructor() {
super();
@@ -219,7 +219,7 @@ describe("hooks", () => {
});
test("useRef hook", async () => {
class Counter extends Component<any, any> {
class Counter extends Component {
static template = xml`<div><button t-ref="button"><t t-esc="value"/></button></div>`;
button = useRef("button");
value = 0;
@@ -241,7 +241,7 @@ describe("hooks", () => {
test("useRef hook is null if ref is removed ", async () => {
expect.assertions(4);
class TestRef extends Component<any, any> {
class TestRef extends Component {
static template = xml`<div><span t-if="state.flag" t-ref="span">owl</span></div>`;
spanRef = useRef("span");
state = useState({ flag: true });
@@ -261,10 +261,10 @@ describe("hooks", () => {
});
test("t-refs on widget are components", async () => {
class WidgetB extends Component<any, any> {
class WidgetB extends Component {
static template = xml`<div>b</div>`;
}
class WidgetC extends Component<any, any> {
class WidgetC extends Component {
static template = xml`<div class="outer-div">Hello<WidgetB t-ref="mywidgetb" /></div>`;
static components = { WidgetB };
ref = useRef("mywidgetb");
@@ -280,11 +280,11 @@ describe("hooks", () => {
test("t-refs are bound at proper timing", async () => {
expect.assertions(2);
class Widget extends Component<any, any> {
class Widget extends Component {
static template = xml`<div>widget</div>`;
}
class ParentWidget extends Component<any, any> {
class ParentWidget extends Component {
static template = xml`
<div>
<t t-foreach="state.list" t-as="elem" t-ref="child" t-key="elem" t-component="Widget"/>
@@ -309,10 +309,10 @@ describe("hooks", () => {
test("t-refs are bound at proper timing (2)", async () => {
expect.assertions(10);
class Widget extends Component<any, any> {
class Widget extends Component {
static template = xml`<div>widget</div>`;
}
class ParentWidget extends Component<any, any> {
class ParentWidget extends Component {
static template = xml`
<div>
<t t-if="state.child1" t-ref="child1" t-component="Widget"/>
@@ -369,7 +369,7 @@ describe("hooks", () => {
});
}
class MyComponent extends Component<any, any> {
class MyComponent extends Component {
static template = xml`<div><t t-if="state.flag">hey</t></div>`;
state = useState({ flag: true });
@@ -403,7 +403,7 @@ describe("hooks", () => {
steps.push("hook:willPatch");
});
}
class MyComponent extends Component<any, any> {
class MyComponent extends Component {
static template = xml`<div><t t-if="state.flag">hey</t></div>`;
state = useState({ flag: true });
@@ -437,7 +437,7 @@ describe("hooks", () => {
steps.push("hook:willPatch" + i);
});
}
class MyComponent extends Component<any, any> {
class MyComponent extends Component {
static template = xml`<div>hey<t t-esc="state.value"/></div>`;
state = useState({ value: 1 });
constructor() {
@@ -473,7 +473,7 @@ describe("hooks", () => {
}
test("simple input", async () => {
class SomeComponent extends Component<any, any> {
class SomeComponent extends Component {
static template = xml`
<div>
<input t-ref="input1"/>
@@ -494,7 +494,7 @@ describe("hooks", () => {
});
test("input in a t-if", async () => {
class SomeComponent extends Component<any, any> {
class SomeComponent extends Component {
static template = xml`
<div>
<input t-ref="input1"/>
@@ -521,7 +521,7 @@ describe("hooks", () => {
});
test("can use sub env", async () => {
class TestComponent extends Component<any, any> {
class TestComponent extends Component {
static template = xml`<div><t t-esc="env.val"/></div>`;
constructor() {
super();
@@ -536,7 +536,7 @@ describe("hooks", () => {
});
test("parent and child env", async () => {
class Child extends Component<any, any> {
class Child extends Component {
static template = xml`<div><t t-esc="env.val"/></div>`;
constructor(parent, props) {
super(parent, props);
@@ -544,7 +544,7 @@ describe("hooks", () => {
}
}
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`<div><t t-esc="env.val"/><Child/></div>`;
static components = { Child };
constructor() {
@@ -568,14 +568,14 @@ describe("hooks", () => {
steps.push("onWillUpdateProps");
});
}
class MyComponent extends Component<any, any> {
class MyComponent extends Component {
static template = xml`<span><t t-esc="props.value"/></span>`;
constructor(parent, props) {
super(parent, props);
useMyHook();
}
}
class App extends Component<any, any> {
class App extends Component {
static template = xml`<div><MyComponent value="state.value"/></div>`;
static components = { MyComponent };
state = useState({ value: 1 });
@@ -597,7 +597,7 @@ describe("hooks", () => {
test("useExternalListener", async () => {
let n = 0;
class MyComponent extends Component<any, any> {
class MyComponent extends Component {
static template = xml`<span><t t-esc="props.value"/></span>`;
constructor(parent, props) {
super(parent, props);
@@ -607,7 +607,7 @@ describe("hooks", () => {
n++;
}
}
class App extends Component<any, any> {
class App extends Component {
static template = xml`<div><MyComponent t-if="state.flag"/></div>`;
static components = { MyComponent };
state = useState({ flag: false });
+6 -6
View File
@@ -27,7 +27,7 @@ afterEach(() => {
describe("Asyncroot", () => {
test("delayed component with AsyncRoot component", async () => {
let def;
class Child extends Component<any, any> {
class Child extends Component {
static template = xml`<span><t t-esc="props.val"/></span>`;
}
class AsyncChild extends Child {
@@ -35,7 +35,7 @@ describe("Asyncroot", () => {
return def;
}
}
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`
<div>
<button t-on-click="updateApp">Update App State</button>
@@ -74,7 +74,7 @@ describe("Asyncroot", () => {
test("fast component with AsyncRoot", async () => {
let def;
class Child extends Component<any, any> {
class Child extends Component {
static template = xml`<span><t t-esc="props.val"/></span>`;
}
class AsyncChild extends Child {
@@ -83,7 +83,7 @@ describe("Asyncroot", () => {
}
}
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`
<div>
<button t-on-click="updateApp">Update App State</button>
@@ -122,7 +122,7 @@ describe("Asyncroot", () => {
test("asyncroot component: mixed re-renderings", async () => {
let def;
class Child extends Component<any, any> {
class Child extends Component {
static template = xml`
<span t-on-click="increment">
<t t-esc="state.val"/>/<t t-esc="props.val"/>
@@ -138,7 +138,7 @@ describe("Asyncroot", () => {
return def;
}
}
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`
<div>
<button t-on-click="updateApp">Update App State</button>
+45 -45
View File
@@ -36,7 +36,7 @@ describe("Portal: Props validation", () => {
test("target is mandatory", async () => {
const dev = QWeb.dev;
QWeb.dev = true;
class Parent extends Component<any, any> {
class Parent extends Component {
static components = { Portal };
static template = xml`
<div>
@@ -61,7 +61,7 @@ describe("Portal: Props validation", () => {
test("target is not list", async () => {
const dev = QWeb.dev;
QWeb.dev = true;
class Parent extends Component<any, any> {
class Parent extends Component {
static components = { Portal };
static template = xml`
<div>
@@ -88,7 +88,7 @@ describe("Portal: Basic use and DOM placement", () => {
test("basic use of portal", async () => {
const dev = QWeb.dev;
QWeb.dev = true;
class Parent extends Component<any, any> {
class Parent extends Component {
static components = { Portal };
static template = xml`
<div>
@@ -113,7 +113,7 @@ describe("Portal: Basic use and DOM placement", () => {
});
test("conditional use of Portal", async () => {
class Parent extends Component<any, any> {
class Parent extends Component {
static components = { Portal };
static template = xml`
<div>
@@ -148,10 +148,10 @@ describe("Portal: Basic use and DOM placement", () => {
});
test("conditional use of Portal (with sub Component)", async () => {
class Child extends Component<any, any> {
class Child extends Component {
static template = xml`<div><t t-esc="props.val"/></div>`;
}
class Parent extends Component<any, any> {
class Parent extends Component {
static components = { Portal, Child };
static template = xml`
<div>
@@ -190,7 +190,7 @@ describe("Portal: Basic use and DOM placement", () => {
});
test("with target in template (before portal)", async () => {
class Parent extends Component<any, any> {
class Parent extends Component {
static components = { Portal };
static template = xml`
<div>
@@ -210,7 +210,7 @@ describe("Portal: Basic use and DOM placement", () => {
});
test("with target in template (after portal)", async () => {
class Parent extends Component<any, any> {
class Parent extends Component {
static components = { Portal };
static template = xml`
<div>
@@ -233,7 +233,7 @@ describe("Portal: Basic use and DOM placement", () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
class Parent extends Component<any, any> {
class Parent extends Component {
static components = { Portal };
static template = xml`
<div>
@@ -260,7 +260,7 @@ describe("Portal: Basic use and DOM placement", () => {
test("portal with child and props", async () => {
const steps: string[] = [];
class Child extends Component<any, any> {
class Child extends Component {
static template = xml`<span><t t-esc="props.val"/></span>`;
mounted() {
steps.push("mounted");
@@ -271,7 +271,7 @@ describe("Portal: Basic use and DOM placement", () => {
expect(outside.innerHTML).toBe("<span>2</span>");
}
}
class Parent extends Component<any, any> {
class Parent extends Component {
static components = { Portal, Child };
static template = xml`
<div>
@@ -298,7 +298,7 @@ describe("Portal: Basic use and DOM placement", () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
class Parent extends Component<any, any> {
class Parent extends Component {
static components = { Portal };
static template = xml`
<div>
@@ -326,7 +326,7 @@ describe("Portal: Basic use and DOM placement", () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
class Parent extends Component<any, any> {
class Parent extends Component {
static components = { Portal };
static template = xml`
<div>
@@ -354,7 +354,7 @@ describe("Portal: Basic use and DOM placement", () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
class Parent extends Component<any, any> {
class Parent extends Component {
static components = { Portal };
static template = xml`
<div>
@@ -379,7 +379,7 @@ describe("Portal: Basic use and DOM placement", () => {
});
test("portal with dynamic body", async () => {
class Parent extends Component<any, any> {
class Parent extends Component {
static components = { Portal };
static template = xml`
<div>
@@ -405,7 +405,7 @@ describe("Portal: Basic use and DOM placement", () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
class Parent extends Component<any, any> {
class Parent extends Component {
static components = { Portal };
static template = xml`
<div>
@@ -439,7 +439,7 @@ describe("Portal: Basic use and DOM placement", () => {
test("lifecycle hooks of portal sub component are properly called", async () => {
const steps: any[] = [];
class Child extends Component<any, any> {
class Child extends Component {
static template = xml`<span t-esc="props.val"/>`;
mounted() {
steps.push("child:mounted");
@@ -454,7 +454,7 @@ describe("Portal: Basic use and DOM placement", () => {
steps.push("child:willUnmount");
}
}
class Parent extends Component<any, any> {
class Parent extends Component {
static components = { Portal, Child };
static template = xml`
<div>
@@ -520,11 +520,11 @@ describe("Portal: Basic use and DOM placement", () => {
});
test("portal destroys on crash", async () => {
class Child extends Component<any, any> {
class Child extends Component {
static template = xml`<span t-esc="props.error and this.will.crash" />`;
state = {};
}
class Parent extends Component<any, any> {
class Parent extends Component {
static components = { Portal, Child };
static template = xml`
<div>
@@ -550,7 +550,7 @@ describe("Portal: Basic use and DOM placement", () => {
});
test("portal manual unmount", async () => {
class Parent extends Component<any, any> {
class Parent extends Component {
static components = { Portal };
static template = xml`
<div>
@@ -577,7 +577,7 @@ describe("Portal: Basic use and DOM placement", () => {
test("portal manual unmount with subcomponent", async () => {
expect.assertions(9);
class Child extends Component<any, any> {
class Child extends Component {
static template = xml`<span>gloria</span>`;
mounted() {
expect(outside.contains(this.el)).toBeTruthy();
@@ -586,7 +586,7 @@ describe("Portal: Basic use and DOM placement", () => {
expect(outside.contains(this.el)).toBeTruthy();
}
}
class Parent extends Component<any, any> {
class Parent extends Component {
static components = { Portal, Child };
static template = xml`
<div>
@@ -614,7 +614,7 @@ describe("Portal: Basic use and DOM placement", () => {
describe("Portal: Events handling", () => {
test("events triggered on movable pure node are handled", async () => {
class Parent extends Component<any, any> {
class Parent extends Component {
static components = { Portal };
static template = xml`
<div>
@@ -638,8 +638,8 @@ describe("Portal: Events handling", () => {
});
test("events triggered on movable owl components are redirected", async () => {
let childInst: Component<any, any> | null = null;
class Child extends Component<any, any> {
let childInst: Component | null = null;
class Child extends Component {
static template = xml`
<span t-on-custom="_onCustom" t-esc="props.val"/>`;
@@ -652,7 +652,7 @@ describe("Portal: Events handling", () => {
this.trigger("custom-portal");
}
}
class Parent extends Component<any, any> {
class Parent extends Component {
static components = { Portal, Child };
static template = xml`
<div t-on-custom-portal="_onCustomPortal">
@@ -677,8 +677,8 @@ describe("Portal: Events handling", () => {
test("events triggered on contained movable owl components are redirected", async () => {
const steps: string[] = [];
let childInst: Component<any, any> | null = null;
class Child extends Component<any, any> {
let childInst: Component | null = null;
class Child extends Component {
static template = xml`
<span t-on-custom="_onCustom"/>`;
@@ -691,7 +691,7 @@ describe("Portal: Events handling", () => {
this.trigger("custom-portal");
}
}
class Parent extends Component<any, any> {
class Parent extends Component {
static components = { Portal, Child };
static template = xml`
<div t-on-custom="_handled" t-on-custom-portal="_handled">
@@ -717,9 +717,9 @@ describe("Portal: Events handling", () => {
});
test("Dom events are not mapped", async () => {
let childInst: Component<any, any> | null = null;
let childInst: Component | null = null;
const steps: string[] = [];
class Child extends Component<any, any> {
class Child extends Component {
static template = xml`
<button>child</button>`;
@@ -728,7 +728,7 @@ describe("Portal: Events handling", () => {
childInst = this;
}
}
class Parent extends Component<any, any> {
class Parent extends Component {
static components = { Portal, Child };
static template = xml`
<div t-on-click="_handled">
@@ -760,22 +760,22 @@ describe("Portal: Events handling", () => {
fixture.appendChild(outside2);
const steps: Array<string> = [];
let childInst: Component<any, any> | null = null;
class Child2 extends Component<any, any> {
let childInst: Component | null = null;
class Child2 extends Component {
static template = xml`<div>child2</div>`;
constructor(parent, props) {
super(parent, props);
childInst = this;
}
}
class Child extends Component<any, any> {
class Child extends Component {
static components = { Portal, Child2 };
static template = xml`
<Portal target="'#outside2'">
<Child2 />
</Portal>`;
}
class Parent extends Component<any, any> {
class Parent extends Component {
static components = { Portal, Child };
static template = xml`
<div t-on-custom='_handled'>
@@ -797,11 +797,11 @@ describe("Portal: Events handling", () => {
});
test("portal's parent's env is not polluted", async () => {
class Child extends Component<any, any> {
class Child extends Component {
static template = xml`
<button>child</button>`;
}
class Parent extends Component<any, any> {
class Parent extends Component {
static components = { Portal, Child };
static template = xml`
<div>
@@ -818,22 +818,22 @@ describe("Portal: Events handling", () => {
test("Portal composed with t-slot", async () => {
const steps: Array<string> = [];
let childInst: Component<any, any> | null = null;
class Child2 extends Component<any, any> {
let childInst: Component | null = null;
class Child2 extends Component {
static template = xml`<div>child2</div>`;
constructor(parent, props) {
super(parent, props);
childInst = this;
}
}
class Child extends Component<any, any> {
class Child extends Component {
static components = { Portal, Child2 };
static template = xml`
<Portal target="'#outside'">
<t t-slot="default"/>
</Portal>`;
}
class Parent extends Component<any, any> {
class Parent extends Component {
static components = { Child, Child2 };
static template = xml`
<div t-on-custom='_handled'>
@@ -857,11 +857,11 @@ describe("Portal: Events handling", () => {
describe("Portal: UI/UX", () => {
test("focus is kept across re-renders", async () => {
class Child extends Component<any, any> {
class Child extends Component {
static template = xml`
<input id="target-me" t-att-placeholder="props.val"/>`;
}
class Parent extends Component<any, any> {
class Parent extends Component {
static components = { Portal, Child };
static template = xml`
<div>
@@ -923,6 +923,19 @@ exports[`static templates div with a class attribute 1`] = `
}"
`;
exports[`static templates div with a class attribute with a quote 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let h = this.h;
let _2 = {'a\\\\'bc':true};
let c3 = [], p3 = {key:3,class:_2};
let vn3 = h('div', p3, c3);
c3.push({text: \`word\`});
return vn3;
}"
`;
exports[`static templates div with a empty class attribute 1`] = `
"function anonymous(context, extra
) {
@@ -962,6 +975,19 @@ exports[`static templates div with a text node 1`] = `
}"
`;
exports[`static templates div with an arbitrary attribute with a quote 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let h = this.h;
let _1 = 'a\\\\'bc';
let c2 = [], p2 = {key:2,attrs:{abc: _1}};
let vn2 = h('div', p2, c2);
c2.push({text: \`word\`});
return vn2;
}"
`;
exports[`static templates empty div 1`] = `
"function anonymous(context, extra
) {
@@ -987,6 +1013,30 @@ exports[`static templates properly handle comments 1`] = `
}"
`;
exports[`static templates properly handle comments between t-if/t-else 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
if (true) {
let c2 = [], p2 = {key:2};
let vn2 = h('span', p2, c2);
c1.push(vn2);
c2.push({text: \`true\`});
}
else {
let c3 = [], p3 = {key:3};
let vn3 = h('span', p3, c3);
c1.push(vn3);
c3.push({text: \`owl\`});
}
return vn1;
}"
`;
exports[`static templates simple dynamic value 1`] = `
"function anonymous(context, extra
) {
@@ -2375,6 +2425,26 @@ exports[`t-on t-on combined with t-raw 1`] = `
}"
`;
exports[`t-on t-on with .capture modifier 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
let h = this.h;
let c1 = [], p1 = {key:1,on:{}};
let vn1 = h('div', p1, c1);
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__'];
let c3 = [], p3 = {key:3,on:{}};
let vn3 = h('button', p3, c3);
c1.push(vn3);
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__'];
c3.push({text: \`Button\`});
return vn1;
}"
`;
exports[`t-on t-on with empty handler (only modifiers) 1`] = `
"function anonymous(context, extra
) {
+48
View File
@@ -51,6 +51,16 @@ describe("static templates", () => {
expect(renderToString(qweb, "test")).toBe(`<div class="abc">word</div>`);
});
test("div with a class attribute with a quote", () => {
qweb.addTemplate("test", `<div class="a'bc">word</div>`);
expect(renderToString(qweb, "test")).toBe(`<div class="a'bc">word</div>`);
});
test("div with an arbitrary attribute with a quote", () => {
qweb.addTemplate("test", `<div abc="a'bc">word</div>`);
expect(renderToString(qweb, "test")).toBe(`<div abc="a'bc">word</div>`);
});
test("div with a empty class attribute", () => {
qweb.addTemplate("test", `<div class="">word</div>`);
expect(renderToString(qweb, "test")).toBe(`<div>word</div>`);
@@ -65,6 +75,19 @@ describe("static templates", () => {
qweb.addTemplate("test", "<div>hello <!-- comment-->owl</div>");
expect(renderToString(qweb, "test")).toBe("<div>hello <!-- comment-->owl</div>");
});
test("properly handle comments between t-if/t-else", () => {
qweb.addTemplate(
"test",
`
<div>
<span t-if="true">true</span>
<!-- comment-->
<span t-else="">owl</span>
</div>`
);
expect(renderToString(qweb, "test")).toBe("<div><span>true</span></div>");
});
});
describe("error handling", () => {
@@ -1604,6 +1627,31 @@ describe("t-on", () => {
expect(steps).toEqual(["onClick"]);
});
test("t-on with .capture modifier", () => {
expect.assertions(2);
qweb.addTemplate(
"test",
`<div t-on-click.capture="onCapture">
<button t-on-click="doSomething">Button</button>
</div>`
);
const steps: string[] = [];
const owner = {
onCapture() {
steps.push("captured");
},
doSomething() {
steps.push("normal");
}
};
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
const button = (<HTMLElement>node).getElementsByTagName("button")[0];
button.click();
expect(steps).toEqual(["captured", "normal"]);
});
});
describe("t-ref", () => {
+2 -2
View File
@@ -31,7 +31,7 @@ describe("Link component", () => {
</div>
</templates>
`);
class App extends Component<any, any> {
class App extends Component {
static components = { Link: Link };
}
@@ -65,7 +65,7 @@ describe("Link component", () => {
</div>
</templates>
`);
class App extends Component<any, any> {
class App extends Component {
static components = { Link: Link };
}
+7 -7
View File
@@ -33,9 +33,9 @@ describe("RouteComponent", () => {
<span t-name="Users">Users</span>
</templates>
`);
class About extends Component<any, any> {}
class Users extends Component<any, any> {}
class App extends Component<any, any> {
class About extends Component {}
class Users extends Component {}
class App extends Component {
static components = { RouteComponent };
}
@@ -65,8 +65,8 @@ describe("RouteComponent", () => {
<span t-name="Book">Book <t t-esc="props.title"/></span>
</templates>
`);
class Book extends Component<any, any> {}
class App extends Component<any, any> {
class Book extends Component {}
class App extends Component {
static components = { RouteComponent };
}
@@ -87,12 +87,12 @@ describe("RouteComponent", () => {
<span t-name="Book">Book <t t-esc="props.title"/>|<t t-esc="incVal"/></span>
</templates>
`);
class Book extends Component<any, any> {
class Book extends Component {
get incVal() {
return this.props.val + 1;
}
}
class App extends Component<any, any> {
class App extends Component {
static components = { RouteComponent };
}
+44 -44
View File
@@ -29,7 +29,7 @@ describe("connecting a component to store", () => {
};
const store = new Store({ state, actions });
class App extends Component<any, any> {
class App extends Component {
static template = xml`
<div>
<span t-foreach="todos" t-key="todo.id" t-as="todo"><t t-esc="todo.msg"/></span>
@@ -60,7 +60,7 @@ describe("connecting a component to store", () => {
};
const store = new Store({ state, actions });
class App extends Component<any, any> {
class App extends Component {
static template = xml`
<div>
<span t-if="isBoolean">ok</span>
@@ -102,7 +102,7 @@ describe("connecting a component to store", () => {
const state = { smallerArray: [1], biggerArray: [2, 3], useSmallArray: true };
const store = new Store({ state });
class App extends Component<any, any> {
class App extends Component {
static template = xml`<div t-esc="mapAdd"/>`;
storeProps = {
array: useStore(state => {
@@ -131,7 +131,7 @@ describe("connecting a component to store", () => {
});
test("throw error if no store is found", async () => {
class App extends Component<any, any> {
class App extends Component {
static template = xml`<div></div>`;
todos = useStore(state => state.todos);
}
@@ -151,7 +151,7 @@ describe("connecting a component to store", () => {
const actions = {};
const store = new Store({ state, actions });
class App extends Component<any, any> {
class App extends Component {
static template = xml`<div/>`;
storeState = useStore(state => state.a);
}
@@ -174,7 +174,7 @@ describe("connecting a component to store", () => {
};
const store = new Store({ state, actions });
class App extends Component<any, any> {
class App extends Component {
static template = xml`
<div>
<span t-esc="a.value"/>
@@ -208,7 +208,7 @@ describe("connecting a component to store", () => {
};
const store = new Store({ state, actions });
class App extends Component<any, any> {
class App extends Component {
static template = xml`
<div>
<span t-foreach="todos" t-key="todo.id" t-as="todo"><t t-esc="todo.msg"/></span>
@@ -245,7 +245,7 @@ describe("connecting a component to store", () => {
};
const store = new Store({ state, actions });
class App extends Component<any, any> {
class App extends Component {
static template = xml`<div><t t-esc="nbrTodos.value"/></div>`;
nbrTodos = useStore(state => ({ value: state.todos.length }));
}
@@ -273,7 +273,7 @@ describe("connecting a component to store", () => {
}
};
const store = new Store({ state, actions });
class App extends Component<any, any> {
class App extends Component {
static template = xml`<div><t t-esc="nbrTodos.value"/></div>`;
nbrTodos = useStore(state => ({ value: state.todos.length }), { isEqual: shallowEqual });
}
@@ -307,7 +307,7 @@ describe("connecting a component to store", () => {
};
const store = new Store({ state, actions });
class App extends Component<any, any> {
class App extends Component {
static template = xml`
<div>
<span t-foreach="todos" t-key="todo.id" t-as="todo"><t t-esc="todo.msg"/></span>
@@ -336,7 +336,7 @@ describe("connecting a component to store", () => {
});
(<any>env).store = store;
class App extends Component<any, any> {
class App extends Component {
static template = xml`
<div>
<button t-on-click="dispatch('inc')">Inc</button>
@@ -365,14 +365,14 @@ describe("connecting a component to store", () => {
};
const store = new Store({ state, actions: {} });
class TodoItem extends Component<any, any> {
class TodoItem extends Component {
static template = xml`<span><t t-esc="todo.text"/></span>`;
todo = useStore((state, props) => {
return state.todos.find(t => t.id === props.todoId);
});
}
class App extends Component<any, any> {
class App extends Component {
static template = xml`<div><TodoItem todoId="state.currentId"/></div>`;
static components = { TodoItem };
state = useState({ currentId: 1 });
@@ -399,14 +399,14 @@ describe("connecting a component to store", () => {
};
const store = new Store({ state, actions });
class TodoItem extends Component<any, any> {
class TodoItem extends Component {
static template = xml`<span><t t-esc="todo.text"/></span>`;
todo = useStore((state, props) => {
return state.todos.find(t => t.id === props.id);
});
}
class TodoList extends Component<any, any> {
class TodoList extends Component {
static template = xml`
<div>
<TodoItem t-foreach="todos" t-as="todo" id="todo.id" t-key="todo.id"/>
@@ -444,7 +444,7 @@ describe("connecting a component to store", () => {
};
const store = new Store({ state, getters });
class TodoItem extends Component<any, any> {
class TodoItem extends Component {
static template = xml`
<div>
<span><t t-esc="storeProps.activeTodoText"/></span>
@@ -460,7 +460,7 @@ describe("connecting a component to store", () => {
});
}
class TodoList extends Component<any, any> {
class TodoList extends Component {
static components = { TodoItem };
static template = xml`
<div>
@@ -481,12 +481,12 @@ describe("connecting a component to store", () => {
});
test("connected component is updated when props are updated", async () => {
class Beer extends Component<any, any> {
class Beer extends Component {
static template = xml`<span><t t-esc="beer.name"/></span>`;
beer = useStore((state, props) => state.beers[props.id]);
}
class App extends Component<any, any> {
class App extends Component {
static template = xml`<div><Beer id="state.beerId"/></div>`;
static components = { Beer };
state = useState({ beerId: 1 });
@@ -506,7 +506,7 @@ describe("connecting a component to store", () => {
});
test("connected component is updated when store is changed", async () => {
class App extends Component<any, any> {
class App extends Component {
static template = xml`
<div>
<span t-foreach="data.beers" t-as="beer" t-key="beer.name"><t t-esc="beer.name"/></span>
@@ -539,7 +539,7 @@ describe("connecting a component to store", () => {
test("connected component is updated when mixing store and props changes", async () => {
let counter = 0;
class Beer extends Component<any, any> {
class Beer extends Component {
static template = xml`<span><t t-esc="beer.name"/></span>`;
beer = useStore((state, props) => state.beers[props.id], {
onUpdate: result => {
@@ -548,7 +548,7 @@ describe("connecting a component to store", () => {
});
}
class App extends Component<any, any> {
class App extends Component {
static template = xml`<div><Beer id="state.beerId"/></div>`;
static components = { Beer };
state = useState({ beerId: 1 });
@@ -580,7 +580,7 @@ describe("connecting a component to store", () => {
});
test("connected component is properly cleaned up on destroy", async () => {
class App extends Component<any, any> {
class App extends Component {
static template = xml`<div></div>`;
state = useStore((state, props) => state);
}
@@ -599,7 +599,7 @@ describe("connecting a component to store", () => {
});
test("connected component with undefined, null and string props", async () => {
class Beer extends Component<any, any> {
class Beer extends Component {
static template = xml`
<div t-name="Beer">
<span>taster:<t t-esc="data.taster"/></span>
@@ -613,7 +613,7 @@ describe("connecting a component to store", () => {
}));
}
class App extends Component<any, any> {
class App extends Component {
static template = xml`<div><Beer id="state.beerId"/></div>`;
static components = { Beer };
state = useState({ beerId: 0 });
@@ -658,7 +658,7 @@ describe("connecting a component to store", () => {
});
test("connected component deeply reactive with undefined, null and string props", async () => {
class Beer extends Component<any, any> {
class Beer extends Component {
static template = xml`
<div>
<span>taster:<t t-esc="info.taster"/></span>
@@ -674,7 +674,7 @@ describe("connecting a component to store", () => {
});
}
class App extends Component<any, any> {
class App extends Component {
static template = xml`<div><Beer id="state.beerId"/></div>`;
static components = { Beer };
state = useState({ beerId: 0 });
@@ -749,7 +749,7 @@ describe("connecting a component to store", () => {
state.x.val = val;
}
};
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`<div><t t-esc="x.val"/></div>`;
x = useStore(state => {
return Object.assign({}, state.x);
@@ -770,7 +770,7 @@ describe("connecting a component to store", () => {
test("correct update order when parent/children are connected", async () => {
const steps: string[] = [];
class Child extends Component<any, any> {
class Child extends Component {
static template = xml`<span><t t-esc="state.msg"/></span>`;
state = useStore((state, props) => {
@@ -778,7 +778,7 @@ describe("connecting a component to store", () => {
return { msg: state.msg[props.key] };
});
}
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`<div><Child key="state.current"/></div>`;
static components = { Child };
@@ -817,7 +817,7 @@ describe("connecting a component to store", () => {
let def = makeDeferred();
def.resolve();
class Child extends Component<any, any> {
class Child extends Component {
static template = xml`<span><t t-esc="state.msg"/></span>`;
state = useStore((s, props) => {
steps.push("child");
@@ -825,7 +825,7 @@ describe("connecting a component to store", () => {
});
}
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`
<div>
<Child t-if="state.flag" someId="state.someId"/>
@@ -885,7 +885,7 @@ describe("connecting a component to store", () => {
actions
});
class TodoItem extends Component<any, any> {
class TodoItem extends Component {
static template = xml`
<div class="todo">
<t t-esc="state.todo.title"/>
@@ -906,7 +906,7 @@ describe("connecting a component to store", () => {
return super.__render(f);
}
}
class TodoApp extends Component<any, any> {
class TodoApp extends Component {
static template = xml`
<div class="todoapp">
<t t-foreach="Object.values(state.todos)" t-as="todo">
@@ -958,7 +958,7 @@ describe("connecting a component to store", () => {
actions
});
class TodoItem extends Component<any, any> {
class TodoItem extends Component {
static template = xml`
<div class="todo">
<t t-esc="state.todo.title"/>
@@ -980,7 +980,7 @@ describe("connecting a component to store", () => {
}
}
class TodoApp extends Component<any, any> {
class TodoApp extends Component {
static template = xml`
<div class="todoapp">
<t t-foreach="Object.values(state.todos)" t-as="todo">
@@ -1024,7 +1024,7 @@ describe("connecting a component to store", () => {
test("connected component willpatch/patch hooks are called on store updates", async () => {
const steps: string[] = [];
class App extends Component<any, any> {
class App extends Component {
static template = xml`<div><t t-esc="store.msg"/></div>`;
store = useStore(s => ({ msg: s.msg }));
@@ -1059,12 +1059,12 @@ describe("connecting a component to store", () => {
test("connected child components stop listening to store when destroyed", async () => {
let steps: any = [];
class Child extends Component<any, any> {
class Child extends Component {
static template = xml`<div><t t-esc="store.val"/></div>`;
store = useStore(s => s);
}
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`<div><Child t-if="state.child" /></div>`;
static components = { Child };
state = useState({ child: true });
@@ -1097,14 +1097,14 @@ describe("connecting a component to store", () => {
test("connected child component destroyed by dispatched action", async () => {
let steps: any = [];
class Child extends Component<any, any> {
class Child extends Component {
static template = xml`<div><t t-esc="store.val"/></div>`;
store = useStore(s => {
steps.push("child selector");
return s;
});
}
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`<div><Child t-if="store.child" /></div>`;
static components = { Child };
store = useStore(s => {
@@ -1135,7 +1135,7 @@ describe("connecting a component to store", () => {
});
test("dispatch an action", async () => {
class App extends Component<any, any> {
class App extends Component {
static template = xml`<div><t t-esc="store.counter"/></div>`;
store = useStore(state => state);
dispatch = useDispatch();
@@ -1203,7 +1203,7 @@ describe("various scenarios", () => {
};
const store = new Store({ actions, state });
class Attachment extends Component<any, any> {
class Attachment extends Component {
static template = xml`
<div>
<span>Attachment <t t-esc="props.id"/></span>
@@ -1213,7 +1213,7 @@ describe("various scenarios", () => {
attachment = useStore((state, props) => ({ name: state.attachments[props.id].name }));
}
class Message extends Component<any, any> {
class Message extends Component {
static template = xml`
<div>
<button t-on-click="doStuff">Do stuff</button>
+2 -2
View File
@@ -22,11 +22,11 @@ test("can log full lifecycle", async () => {
const log = console.log;
console.log = arg => steps.push(arg);
class Child extends Component<any, any> {
class Child extends Component {
static template = xml`<div>child</div>`;
}
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`<div><Child t-if="state.flag"/></div>`;
static components = { Child };
state = useState({ flag: false });
+2 -2
View File
@@ -21,11 +21,11 @@ test("can log scheduler start and stop", async () => {
const log = console.log;
console.log = arg => steps.push(arg);
class Child extends Component<any, any> {
class Child extends Component {
static template = xml`<div>child</div>`;
}
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`<div><Child /></div>`;
static components = { Child };
}
+1 -1
View File
@@ -21,7 +21,7 @@ test("log a specific message for render method calls if component is not mounted
const log = console.log;
console.log = arg => steps.push(arg);
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`<div><t t-esc="state.value"/></div>`;
state = owl.hooks.useState({ value: 1 });
}
+2 -2
View File
@@ -21,14 +21,14 @@ test("log a sub component with non stringifiable props", async () => {
const log = console.log;
console.log = arg => steps.push(arg);
class Child extends Component<any, any> {
class Child extends Component {
static template = xml`<span><t t-esc="props.obj.val"/></span>`;
}
const circularObject: any = { val: 1 };
circularObject.circularObject = circularObject;
class Parent extends Component<any, any> {
class Parent extends Component {
static template = xml`<div><Child obj="obj"/></div>`;
static components = { Child };
obj = circularObject;
+47
View File
@@ -1811,6 +1811,49 @@ const WMS_CSS = `body {
font-size: 20px;
}`;
const SFC = `// This example illustrates how Owl enables single file components,
// which include code, template and style.
//
// This is very useful in some situations, such as testing or quick prototyping.
// Note that this example has no external xml or css file, everything is
// contained in a single js file.
const { Component, useState, tags } = owl;
const { xml, css } = tags;
// Counter component
const COUNTER_TEMPLATE = xml\`
<button t-on-click="state.value++">
Click! [<t t-esc="state.value"/>]
</button>\`;
const COUNTER_STYLE = css\`
button {
color: blue;
}\`;
class Counter extends Component {
state = useState({ value: 0})
}
Counter.template = COUNTER_TEMPLATE;
Counter.style = COUNTER_STYLE;
// App
const APP_TEMPLATE = xml\`
<div>
<Counter/>
<Counter/>
</div>\`;
class App extends Component {}
App.template = APP_TEMPLATE;
App.components = { Counter };
// Application setup
const app = new App();
app.mount(document.body);
`;
export const SAMPLES = [
{
description: "Components",
@@ -1823,6 +1866,10 @@ export const SAMPLES = [
code: FORM,
xml: FORM_XML
},
{
description: "Single File Components",
code: SFC
},
{
description: "Animations",
code: ANIMATION,
+215
View File
@@ -0,0 +1,215 @@
const package = require("../package.json");
const readline = require("readline");
const fs = require("fs");
const exec = require("child_process").exec;
const chalk = require("chalk");
const GitHub = require("github-api");
const REL_NOTES_FILE = `release-notes.md`;
const STEPS = 10;
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
startRelease().then(() => {
rl.close();
});
// -----------------------------------------------------------------------------
// Relase Script
// -----------------------------------------------------------------------------
async function startRelease() {
log(`*** Owl release script ***`);
log(`Current Version: ${package.version}`);
// ---------------------------------------------------------------------------
log(`Step 1/${STEPS}: collecting info...`);
const current = package.version;
const next = await ask("Next version: ");
let file = await ask(`Release notes (${REL_NOTES_FILE}): `);
file = file || REL_NOTES_FILE;
let content;
try {
content = await readFile("./" + file);
} catch (e) {
logSubContent(e.message);
log("Cannot find release notes... Aborting");
return;
}
// Todo: add playground update feature
// let shouldUpdateStr = await ask("Update Playground? (y/n)");
// const shouldUpdatePlayground = shouldUpdateStr === "y";
const token = await ask("Github token: ");
// ---------------------------------------------------------------------------
log(`Step 2/${STEPS}: running tests...`);
const testsResult = await execCommand("npm run test");
if (testsResult !== 0) {
log("Test suite does not pass. Aborting.");
return;
}
await ask("Ready for next step...");
// ---------------------------------------------------------------------------
log(`Step 3/${STEPS}: updating package.json, readme.md and roadmap.md...`);
await replaceInFile("./package.json", current, next);
await replaceInFile("./README.md", current, next);
await replaceInFile("./roadmap.md", current, next);
await ask("Ready for next step...");
// ---------------------------------------------------------------------------
log(`Step 4/${STEPS}: creating git commit...`);
const gitResult = await execCommand(`git commit -am "[REL] v${next}\n\n${content}"`);
if (gitResult !== 0) {
log("Git commit failed. Aborting.");
return;
}
await ask("Ready for next step...");
// -------------------j--------------------------------------------------------
log(`Step 5/${STEPS}: building owl (iife version)...`);
const buildResult = await execCommand("npm run build");
if (buildResult !== 0) {
log("Build failed. Aborting.");
return;
}
await ask("Ready for next step...");
// ---------------------------------------------------------------------------
log(`Step 6/${STEPS}: minifying owl...`);
const minifyResult = await execCommand("npm run minify");
if (minifyResult !== 0) {
log("Minify failed. Aborting.");
return;
}
await ask("Ready for next step...");
// ---------------------------------------------------------------------------
log(`Step 7/${STEPS}: pushing on github...`);
const pushResult = await execCommand("git push");
if (pushResult !== 0) {
log("git push failed. Aborting.");
return;
}
await ask("Ready for next step...");
// ---------------------------------------------------------------------------
log(`Step 8/${STEPS}: publishing release notes on github...`);
const options = {
tag_name: `v${next}`,
name: `v${next}`,
body: content,
draft: true // todo: remove this someday
};
const result = await createRelease(token, options);
console.log(result);
await ask("Ready for next step...");
// ---------------------------------------------------------------------------
log(`Step 9/${STEPS}: adding assets to release...`);
await ask("Please add owl.js and owl.min.js to draft release, then confirm");
// todo: do this with curl
// curl \
// -H "Authorization: token $GITHUB_TOKEN" \
// -H "Content-Type: $(file -b --mime-type $FILE)" \
// --data-binary @$FILE \
// "https://uploads.github.com/repos/hubot/singularity/releases/123/assets?name=$(basename $FILE)"
// ---------------------------------------------------------------------------
log(`Step 10/${STEPS}: publishing module on npm...`);
await execCommand("npm run publish");
log("Owl Release process completed! Thank you for your patience");
}
// -----------------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------------
function log(text) {
console.log(chalk.yellow(formatLog(text)));
}
function formatLog(text) {
return `[REL] ${text}`;
}
function logSubContent(text) {
for (let line of text.split("\n")) {
if (line.trim()) {
console.log(" " + line);
}
}
}
function ask(question) {
return new Promise(resolve => {
rl.question(question, result => {
resolve(result);
});
});
}
function logStream(stream) {
stream.on("data", data => {
logSubContent(data);
});
}
function execCommand(command) {
return new Promise(resolve => {
const childProcess = exec(command, (err, stdout, stderr) => {
if (err) {
resolve(err.code);
}
});
childProcess.on("exit", code => {
resolve(code);
});
logStream(childProcess.stdout);
logStream(childProcess.stderr);
});
}
function readFile(file) {
return new Promise((resolve, reject) => {
fs.readFile(file, "utf8", function(err, content) {
if (err) {
reject(err);
} else {
resolve(content);
}
});
});
}
async function replaceInFile(file, from, to) {
const content = await readFile(file);
return new Promise((resolve, reject) => {
const updatedContent = content.replace(new RegExp(from, "g"), to);
fs.writeFile(file, updatedContent, "utf8", err => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
function createRelease(token, options) {
return new Promise((resolve, reject) => {
var gh = new GitHub({ token });
gh.getRepo("odoo", "owl").createRelease(options, (err, result, req) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
});
}