Compare commits

..

2 Commits

Author SHA1 Message Date
Simon Genin (ges) 6aaa174a8a wip 2020-10-14 20:33:00 +02:00
Simon Genin (ges) 35121dff59 wip 2020-10-14 15:16:22 +02:00
90 changed files with 2206 additions and 5007 deletions
+2 -3
View File
@@ -14,7 +14,7 @@ jobs:
strategy:
matrix:
node-version: [12.x, 14.x, 16.x]
node-version: [10.x, 12.x, 14.x]
steps:
- uses: actions/checkout@v2
@@ -23,5 +23,4 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm run test
- run: npm run check-formatting
- run: npm test
+1 -6
View File
@@ -26,9 +26,4 @@ node_modules
# Extras temp file
/tools/owl.js
release-notes.md
.rpt2_cache
# useful in some cases
/temp
release-notes.md
+5 -7
View File
@@ -1,9 +1,5 @@
<h1 align="center">🦉 <a href="https://odoo.github.io/owl/">OWL Framework</a> 🦉</h1>
[![License: LGPL v3](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0)
[![npm version](https://badge.fury.io/js/@odoo%2Fowl.svg)](https://badge.fury.io/js/@odoo%2Fowl)
[![Downloads](https://img.shields.io/npm/dm/@odoo%2Fowl.svg)](https://www.npmjs.com/package/@odoo/owl)
_Class based components with hooks, reactive state and concurrent mode_
## Project Overview
@@ -45,7 +41,7 @@ find some more additional information [here](doc/miscellaneous/comparison.md).
Here is a short example to illustrate interactive components:
```javascript
const { Component, useState, mount } = owl;
const { Component, useState } = owl;
const { xml } = owl.tags;
class Counter extends Component {
@@ -67,7 +63,8 @@ class App extends Component {
static components = { Counter };
}
mount(App, { target: document.body });
const app = new App();
app.mount(document.body);
```
Note that the counter component is made reactive with the [`useState` hook](doc/reference/hooks.md#usestate).
@@ -124,7 +121,8 @@ npm install @odoo/owl
If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-1.4.9](https://github.com/odoo/owl/releases/tag/v1.4.9)
- [owl-1.0.11.js](https://github.com/odoo/owl/releases/download/v1.0.11/owl.js)
- [owl-1.0.11.min.js](https://github.com/odoo/owl/releases/download/v1.0.11/owl.min.js)
## License
+2 -1
View File
@@ -85,7 +85,8 @@ afterEach(() => {
describe("SomeComponent", () => {
test("component behaves as expected", async () => {
const props = {...}; // depends on the component
const comp = await mount(SomeComponent, { target: fixture, props });
const comp = new SomeComponent(null, props);
await comp.mount(fixture);
// do some assertions
expect(...).toBe(...);
+1 -1
View File
@@ -96,7 +96,7 @@ class OrderLine extends Component {
</div>`;
add() {
this.trigger("add-to-order", { line: this.props.line });
this.trigger("add-to-order", { line: props.line });
}
}
+11 -8
View File
@@ -54,7 +54,7 @@ Now, `index.html` should contain the following:
And `app.js` should look like this:
```js
const { Component, mount } = owl;
const { Component } = owl;
const { xml } = owl.tags;
const { whenReady } = owl.utils;
@@ -65,7 +65,8 @@ class App extends Component {
// Setup code
function setup() {
mount(App, target: { document.body })
const app = new App();
app.mount(document.body);
}
whenReady(setup);
@@ -123,7 +124,7 @@ Here is the content of `app.js` and `main.js`:
```js
// app.js ----------------------------------------------------------------------
const { Component, mount } = owl;
const { Component } = owl;
const { xml } = owl.tags;
export class App extends Component {
@@ -134,7 +135,8 @@ export class App extends Component {
import { App } from "./app.js";
function setup() {
mount(App, { target: document.body });
const app = new App();
app.mount(document.body);
}
owl.utils.whenReady(setup);
@@ -238,11 +240,12 @@ export class App extends Component {
}
// src/main.js -----------------------------------------------------------------
import { utils, mount } from "@odoo/owl";
import { utils } from "@odoo/owl";
import { App } from "./components/App";
function setup() {
mount(App, { target: document.body });
const app = new App();
app.mount(document.body);
}
utils.whenReady(setup);
@@ -250,7 +253,6 @@ utils.whenReady(setup);
// tests/components/App.test.js ------------------------------------------------
import { App } from "../../src/components/App";
import { makeTestFixture, nextTick, click } from "../helpers";
import { mount } from "@odoo/owl";
let fixture;
@@ -264,7 +266,8 @@ afterEach(() => {
describe("App", () => {
test("Works as expected...", async () => {
await mount(App, { target: fixture });
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>Hello Owl</div>");
click(fixture, "div");
+16 -13
View File
@@ -70,9 +70,7 @@ just put the following code:
Note that we put everything inside an immediately executed function to avoid leaking
anything to the global scope.
Finally, `owl.js` should be the last version downloaded from the Owl repository (you can use `owl.min.js` if you prefer). Be aware that you should download the `owl.iife.js` or `owl.iife.min.js`, because these files
are built to run directly on the browser (other files such as `owl.cjs.js` are
built to be bundled by other tools).
Finally, `owl.js` should be the last version downloaded from the Owl repository (you can use `owl.min.js` if you prefer).
Now, the project should be ready. Loading the `index.html` file into a browser
should show an empty page, with the title `Owl Todo App`, and it should log a
@@ -85,7 +83,7 @@ a single root component. Let us start by defining an `App` component. Replace th
content of the function in `app.js` by the following code:
```js
const { Component, mount } = owl;
const { Component } = owl;
const { xml } = owl.tags;
const { whenReady } = owl.utils;
@@ -96,7 +94,8 @@ class App extends Component {
// Setup code
function setup() {
mount(App, { target: document.body });
const app = new App();
app.mount(document.body);
}
whenReady(setup);
@@ -280,7 +279,8 @@ class App extends Component {
// -------------------------------------------------------------------------
function setup() {
owl.config.mode = "dev";
mount(App, { target: document.body });
const app = new App();
app.mount(document.body);
}
whenReady(setup);
@@ -547,7 +547,7 @@ application), since it involves extracting all task related code out of the
components. Here is the new content of the `app.js` file:
```js
const { Component, Store, mount } = owl;
const { Component, Store } = owl;
const { xml } = owl.tags;
const { whenReady } = owl.utils;
const { useRef, useDispatch, useStore } = owl.hooks;
@@ -639,7 +639,8 @@ function setup() {
owl.config.mode = "dev";
const store = new Store({ actions, state: initialState });
App.env.store = store;
mount(App, { target: document.body });
const app = new App();
app.mount(document.body);
}
whenReady(setup);
@@ -665,8 +666,9 @@ function makeStore() {
function setup() {
owl.config.mode = "dev";
const env = { store: makeStore() };
mount(App, { target: document.body, env });
App.env.store = makeStore();
const app = new App();
app.mount(document.body);
}
```
@@ -810,7 +812,7 @@ For reference, here is the final code:
```js
(function () {
const { Component, Store, mount } = owl;
const { Component, Store } = owl;
const { xml } = owl.tags;
const { whenReady } = owl.utils;
const { useRef, useDispatch, useState, useStore } = owl.hooks;
@@ -941,8 +943,9 @@ For reference, here is the final code:
function setup() {
owl.config.mode = "dev";
const env = { store: makeStore() };
mount(App, { target: document.body, env });
App.env.store = makeStore();
const app = new App();
app.mount(document.body);
}
whenReady(setup);
+2 -2
View File
@@ -4,7 +4,7 @@ OWL, React and Vue have the same main feature: they allow developers to build
declarative user interfaces. To do that, all these frameworks uses a virtual dom. However, there are still obviously many differences.
In this page, we try to highlight some of these differences. Obviously, a lot of
effort was put to be fair. However, if you disagree with some of the points
effort was done to be fair. However, if you disagree with some of the points
discussed, feel free to open an issue/submit a PR to correct this text.
## Content
@@ -47,7 +47,7 @@ components are fast enough for all our usecases, and making it as simple as
possible for developers is more valuable (for us).
Also, functions or class based components are more than just syntax. Functions
come with a mindset of composition and class are about inheritance. Clearly,
comes with a mindset of composition and class are about inheritance. Clearly,
both of these are important mechanisms for reusing code. Also, one does not
exclude the other.
+12 -12
View File
@@ -61,14 +61,14 @@ because a lot of the state is hidden in their internals.
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 installs
or removes an addon), we need to have all that kind of tooling on the production
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 resolved at runtime, by the browser. The
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!)
@@ -78,12 +78,12 @@ 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 components, and is easy to integrate
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 templates as XML documents in a database. This is very powerful, since
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.
@@ -104,12 +104,12 @@ 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 developers as
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 components, hooks, and many other
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
@@ -135,7 +135,7 @@ 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 an xml description. But the form view code
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.
@@ -147,16 +147,16 @@ 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 read-only,
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 performs some action). Then, observing its state
(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 wants a different user experience: most 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).
@@ -175,6 +175,6 @@ 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 makes choices compatible with Odoo.
that is different. For a framework that make choices compatible with Odoo.
And that is why we built Owl 🦉.
-2
View File
@@ -17,7 +17,6 @@ You will find here a complete reference of every feature, class or object
provided by Owl.
- [Animations](reference/animations.md)
- [Browser](reference/browser.md)
- [Component](reference/component.md)
- [Content](reference/content.md)
- [Concurrency Model](reference/concurrency_model.md)
@@ -28,7 +27,6 @@ provided by Owl.
- [Event Handling](reference/event_handling.md)
- [Error Handling](reference/error_handling.md)
- [Hooks](reference/hooks.md)
- [Mounting a component](reference/mounting.md)
- [Miscellaneous Components](reference/misc.md)
- [Observer](reference/observer.md)
- [Props](reference/props.md)
+9 -41
View File
@@ -52,20 +52,21 @@ sequence of events will happen:
At node insertion:
- the css classes `name-enter` and `name-enter-active` will be added directly
when the node is inserted into the DOM.
when the node is inserted into the DOM,
- on the next animation frame: the css class `name-enter` will be removed and the
class `name-enter-to` will be added (so they can be used to trigger css
transition effects).
- at the end of the transition, `name-enter-to` and `name-enter-active` will be removed.
transition effects),
- the css class `name-enter-active` will be removed whenever a css transition
ends.
At node destruction:
- the css classes `name-leave` and `name-leave-active` will be added before the
node is removed to the DOM.
- on the next animation frame: the css class `name-leave` will be removed and the
class `name-leave-to` will be added (so they can be used to trigger css
transition effects).
- at the end of the transition, `name-leave-to` and `name-leave-active` will be removed.
node is removed to the DOM,
- the css class `name-leave` will be removed on the next animation frame (so it
can be used to trigger css transition effects),
- the css class `name-leave-active` will be removed whenever a css transition
ends. Only then will the element be removed from the DOM.
For example, a simple fade in/out effect can be done with this:
@@ -92,36 +93,3 @@ Notes:
Owl does not support more than one transition on a single node, so the
`t-transition` expression must be a single value (i.e. no space allowed).
## SCSS Mixins
If you use SCSS, you can use mixins to make generic animations. Here is an exemple with a fade in / fade out animation:
```scss
@mixin animation-fade($time, $name) {
.#{$name}_fade-enter-active,
.#{$name}_fade-active {
transition: all $time;
}
.#{$name}_fade-enter {
opacity: 0;
}
.#{$name}_fade-leave-to {
opacity: 0;
}
}
```
Usage:
```scss
@include animation-fade(0.5s, "o_notification");
```
You can now have in your template:
```xml
<SomeTag t-transition="o_notification_fade"/>
```
-33
View File
@@ -1,33 +0,0 @@
# 🦉 Browser 🦉
## Content
- [Overview](#overview)
- [Browser Content](#browser-content)
## Overview
The browser object contains some browser native APIs, such as `setTimeout`, that
are used by Owl and its utility functions. They are exposed with the intent of
making them mockable if necessary.
```js
owl.browser.setTimeout === window.setTimeout; // return true
```
For now, this object contains some functions that are not used by Owl. They
will eventually be removed in Owl 2.0.
## Browser Content
More specifically, the `browser` object contains the following methods and objects:
- `setTimeout`
- `clearTimeout`
- `setInterval`
- `clearInterval`
- `requestAnimationFrame`
- `random`
- `Date`
- `fetch`
- `localStorage`
+5 -40
View File
@@ -10,22 +10,13 @@
- [Static Properties](#static-properties)
- [Methods](#methods)
- [Lifecycle](#lifecycle)
- [`constructor(parent, props)`](#constructorparent-props)
- [`setup()`](#setup)
- [`willStart()`](#willstart)
- [`mounted()`](#mounted)
- [`willUpdateProps(nextProps)`](#willupdatepropsnextprops)
- [`willPatch()`](#willpatch)
- [`patched(snapshot)`](#patchedsnapshot)
- [`willUnmount()`](#willunmount)
- [`catchError(error)`](#catcherrorerror)
- [Root Component](#root-component)
- [Composition](#composition)
- [Form Input Bindings](#form-input-bindings)
- [References](#references)
- [Dynamic sub components](#dynamic-sub-components)
- [Functional Components](#functional-components)
- [SVG Components](#svg-components)
- [SVG components](#svg-components)
## Overview
@@ -279,10 +270,6 @@ We explain here all the public methods of the `Component` class.
// app is now visible
```
Note that the normal way of mounting an application is by using the `mount`
method on a component class, not by creating the instance by hand. See the
documentation on [mounting applications](mounting.md).
* **`unmount()`**: in case a component needs to be detached/removed from the DOM, this
method can be used. Most applications should not call `unmount`, this is more
useful to the underlying component system.
@@ -298,13 +285,8 @@ We explain here all the public methods of the `Component` class.
are updated. It returns a boolean, which indicates if the component should
ignore a props update. If it returns false, then `willUpdateProps` will not
be called, and no rendering will occur. Its default implementation is to
always return true. Note that this is an optimization, similar to React's `shouldComponentUpdate`. Most of the time, this should not be used, but it
can be useful if we are handling large number of components. Since this is an
optimization, Owl has the freedom to ignore the result of `shouldUpdate` in
some cases (for example, if a component is remounted, or if we want to force
a full rerender of the UI). However, if `shouldUpdate` returns true, then Owl
provides the guarantee that the component will be rendered at some point in
the future (except if the component is destroyed or if some part of the UI crashes).
always return true. This is an optimization, similar to React's `shouldComponentUpdate`. Most of the time, this should not be used, but it
can be useful if we are handling large number of components.
* **`destroy()`**. As its name suggests, this method will remove the component,
and perform all necessary cleanup, such as unmounting the component, its children,
@@ -325,7 +307,7 @@ a owl component:
| Method | Description |
| ------------------------------------------------ | ----------------------------------------------------------- |
| **[setup](#setup)** | setup |
| **[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 |
@@ -370,23 +352,6 @@ class ClickCounter extends owl.Component {
}
```
Hook functions can be called in the constructor.
#### `setup()`
_setup_ is run just after the component is constructed. It is a lifecycle method,
very similar to the _constructor_, except that it does not receive any argument.
It is a valid method to call hook functions. Note that one of the main reason to
have the `setup` hook in the component lifecycle is to make it possible to
monkey patch it. It is a common need in the Odoo ecosystem.
```javascript
setup() {
useSetupAutofocus();
}
```
#### `willStart()`
willStart is an asynchronous hook that can be implemented to
@@ -789,7 +754,7 @@ template rendered with `props`. In Owl, this can be done by
simply defining a template, that will access the `props` object:
```js
const Welcome = xml`<h1>Hello, <t t-esc="props.name"/></h1>`;
const Welcome = xml`<h1>Hello, {props.name}</h1>`;
class MyComponent extends Component {
static template = xml`
+2 -19
View File
@@ -2,10 +2,9 @@
The Owl framework is designed to work in many situations. However, it is
sometimes necessary to customize some behaviour. This is done by using the
global `config` object. It provides two settings:
global `config` object. It currently has one key:
- [`mode`](#mode) (default value: `prod`),
- [`enableTransitions`](#enabletransitions) (default value: `true`).
- [`mode`](#mode).
## Mode
@@ -26,19 +25,3 @@ So, changing this setting is best done at startup.
An important job done by the `dev` mode is to validate props for each component
creation and update. Also, extra props will cause an error.
## `enableTransitions`
Transitions are usually nice, but they can cause issues in some specific cases,
such as automated tests. It is uncomfortable having to wait for a transition
to end before moving to the next step.
To solve this issue, Owl can be configured to ignore the `t-transition` directive.
To do that, one only needs to set the `enableTransitions` flag to false:
```js
owl.config.enableTransitions = false;
```
Note that it suffers from the same drawback as the "dev" mode: all compiled
templates, if any, will keep their current behaviours.
+4 -8
View File
@@ -7,15 +7,13 @@ For example, `Component` is available at `owl.Component` and `EventBus` is
exported as `owl.core.EventBus`.
```
browser
Component misc
Context AsyncRoot
QWeb Portal
mount router
Store Link
useState RouteComponent
config Router
mode
Store router
useState Link
config RouteComponent
mode Router
core tags
EventBus css
Observer xml
@@ -29,8 +27,6 @@ hooks utils
useContext
useState
useRef
useComponent
useEnv
useSubEnv
useStore
useDispatch
+20 -5
View File
@@ -49,14 +49,15 @@ The correct way to customize an environment is to simply set it up on the root
component class, before the first component is created:
```js
const env = {
App.env = {
_t: myTranslateFunction,
user: {...},
services: {
...
},
};
mount(App, { target: document.body, env });
const app = new App();
app.mount(document.body);
```
It is also possible to simply share an environment between all root components,
@@ -120,8 +121,9 @@ async function myEnv() {
}
async function start() {
const env = await myEnv();
mount(App, { target: document.body, env });
App.env = await myEnv();
const app = new App();
await app.mount(document.body);
}
```
@@ -133,4 +135,17 @@ the `QWeb` instance and a `browser` object:
- `qweb` will be set to an empty `QWeb` instance. This is absolutely necessary
for Owl to be able to render anything
- `browser`: this is an object that contains some common access points to the
browser methods with a side effect. See [browser](browser.md) for more information. Note that the browser object will be removed from the environment in Owl 2.0.
browser methods with a side effect. This is particularly useful when one want
to test more advanced components, and be able to mock those methods.
More specifically, the `browser` object contains the following methods and objects:
- `setTimeout`
- `clearTimeout`
- `setInterval`
- `clearInterval`
- `requestAnimationFrame`
- `random`
- `Date`
- `fetch`
- `localStorage`
-14
View File
@@ -78,20 +78,6 @@ The `t-on` directive allows to prebind its arguments. For example,
Here, `expr` is a valid Owl expression, so it could be `true` or some variable
from the rendering context.
### Type Hinting
Note that if you work with Typescript, the `trigger` method is generic on the type of the payload.
You can then describe the type of the event, so you will see typing errors...
```typescript
this.trigger<MyCustomPayload>("my-custom-event", payload);
```
```typescript
myCustomEventHandler(ev: OwlEvent<MyCustomPayload>) { ... }
```
## Inline Event Handlers
One can also directly specify inline statements. For example,
+5 -22
View File
@@ -21,8 +21,6 @@
- [`useStore`](#usestore)
- [`useDispatch`](#usedispatch)
- [`useGetters`](#usegetters)
- [`useComponent`](#usecomponent)
- [`useEnv`](#useenv)
- [Making customized hooks](#making-customized-hooks)
## Overview
@@ -131,7 +129,7 @@ class SomeComponent extends Component {
### One rule
There is only one rule: every hook for a component has to be called in the
constructor, in the _setup_ method, or in class fields:
constructor (or in class fields):
```js
// ok
@@ -147,13 +145,6 @@ class SomeComponent extends Component {
}
}
// also ok
class SomeComponent extends Component {
setup() {
this.state = useState({ value: 0 });
}
}
// not ok: this is executed after the constructor is called
class SomeComponent extends Component {
async willStart() {
@@ -390,16 +381,6 @@ The `useDispatch` hook is the way for components to get a reference to the store
The `useGetters` hook is the way for components to get a reference to the store
getters. See the [store documentation](store.md) for more information.
### `useComponent`
The `useComponent` hook is useful as a building block for some customized hooks,
that may need a reference to the component calling them.
### `useEnv`
The `useEnv` hook is useful as a building block for some customized hooks,
that may need a reference to the env of the component calling them.
### Making customized hooks
Hooks are a wonderful way to organize the code of a complex component by feature
@@ -454,11 +435,13 @@ not the solution to every problem.
```js
function useRouter() {
const env = useEnv();
return env.router;
return Component.current.env.router;
}
```
This means that we give control to the application developer to create the
router, which is good, so they can set it up, subclass it, ... And then, to
test our components, we can just add a mock router in the environment.
Note: the code above makes use of the `Component.current` property. This is the
way hooks are able to get a reference to the component currently being created.
+3 -2
View File
@@ -43,7 +43,7 @@ workflow to help the user put in some data, which it could use later on.
JavaScript:
```js
const { Component, mount } = owl;
const { Component } = owl;
const { Portal } = owl.misc;
class TeleportedComponent extends Component {}
@@ -51,7 +51,8 @@ class App extends Component {
static components = { Portal, TeleportedComponent };
}
mount(App, { target: document.body });
const app = new App();
app.mount(document.body);
```
XML:
-60
View File
@@ -1,60 +0,0 @@
# 🦉 Mounting an application 🦉
## Content
- [Overview](#overview)
- [API](#api)
## Overview
Mounting an Owl application is done by using the `mount` method (available in
`owl.mount` if you are using the iife build, or it can be directly imported
from `owl` if you are using a module system):
```js
const mount = { owl }; // if owl is available as an object
const env = { ... };
const app = await mount(MyComponent, { target: document.body, env });
```
Another example:
```js
const config = {
env: ...,
props: ...,
target: document.body,
position: "self",
};
const app = await mount(App, config);
```
A common way to initialize an application is to first setup an environment,
then to call the `mount` method.
## API
Mount takes two parameters:
- `C`, which should be a component class (NOT instance),
- `params`, which is an object with the following keys:
- `target (HTMLElement | DocumentFragment)`: the target of the mount operation
- `env (optional, Env)` an environment
- `position (optional, "first-child" | "last-child" | "self")` the position
where it should be mounted (see below for more informations)
- `props (optional, any)`: some initial values that are given as props. Useful
when the root component is configurable, or when testing sub components
Here are the various positions supported by Owl:
- `first-child`: with this option, the component will be prepended inside the target,
- `last-child` (default value): with this option, the component will be
appended in the target element,
- `self`: the target will be used as the root element for the component. This
means that the target has to be an HTMLElement (and not a document fragment).
In this situation, it is possible that the component cannot be unmounted. For
example, if its target is `document.body`.
The `mount` method returns a promise that resolves to the instance of the created
component.
+5 -5
View File
@@ -15,7 +15,7 @@ use cases, there is no need to directly instantiate an observer.
For example, this code will display `update` in the console:
```javascript
const observer = new owl.core.Observer();
const observer = new owl.Observer();
observer.notifyCB = () => console.log("update");
const obj = observer.observe({ a: { b: 1 } });
@@ -39,14 +39,14 @@ is incremented every time the value is observed. Sometimes, it can be useful
to obtain that number:
```js
const observer = new owl.core.Observer();
const observer = new owl.Observer();
const obj = observer.observe({ a: { b: 1 } });
observer.revNumber(obj.a); // 1
observer.deepRevNumber(obj.a); // 1
obj.a.b = 2;
observer.revNumber(obj.a); // 2
observer.deepRevNumber(obj.a); // 2
```
The `revNumber` can also return 0, which indicates that the value is not
The `deepRevNumber` can also return 0, which indicates that the value is not
observed.
+1 -1
View File
@@ -18,7 +18,7 @@ class Child extends Component {
}
class Parent extends Component {
static template = xml`<div><Child a="state.a" b="'string'"/></div>`;
static template = xml`<div><ComponentA a="state.a" b="'string'"/></div>`;
static components = { Child };
state = useState({ a: "fromparent" });
}
-65
View File
@@ -13,11 +13,8 @@
- [Setting Variables](#setting-variables)
- [Conditionals](#conditionals)
- [Dynamic Attributes](#dynamic-attributes)
- [Dynamic Class Attribute](#dynamic-class-attribute)
- [Dynamic Tag Names](#dynamic-tag-names)
- [Loops](#loops)
- [Rendering Sub Templates](#rendering-sub-templates)
- [Dynamic Sub Templates](#dynamic-sub-templates)
- [Translations](#translations)
- [Debugging](#debugging)
@@ -78,7 +75,6 @@ needs. Here is a list of all Owl specific directives:
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
| `t-slot` | [Rendering a slot](slots.md) |
| `t-model` | [Form input bindings](component.md#form-input-bindings) |
| `t-tag` | [Rendering nodes with dynamic tag name](#dynamic-tag-names) |
## Reference
@@ -327,40 +323,6 @@ values) or a pair `[key, value]`. For example:
<div t-att="['a', 'b']"/> <!-- <div a="b"></div> -->
```
### Dynamic class attribute
For convenience, Owl supports a special case for the `t-att-class` case: one can
use an object with keys describing the classes, and values boolean value denoting
if the class is or is not present:
```xml
<div t-att-class="{'a': true, 'b': true}"/> <!-- result: <div class="a b"></div> -->
<div t-att-class="{'a b': true, 'c': true}"/> <!-- result: <div class="a b c"></div> -->
```
Note that it can be combined with normal class attribute:
```xml
<div class="a" t-att-class="{'b': true}"/> <!-- result: <div class="a b"></div> -->
```
### Dynamic tag names
When writing generic components or templates, the specific concrete tag for an
HTML element is not known yet. In those situations, the `t-tag` directive is
useful. It simply evaluates dynamically an expression to use as a tag name. The
template:
```xml
<t t-tag="tag">
<span>content</span>
</t>
```
will be rendered as `<div><span>content</span></div>` if the `tag` context key
is set to `div`.
### Loops
QWeb has an iteration directive `t-foreach` which take an expression returning the
@@ -490,17 +452,6 @@ are all equivalent:
If there is no `t-key` directive, Owl will use the index as a default key.
Note: the `t-foreach` directive only accepts arrays (lists) or objects. It does
not work with other iterables, such as `Set`. However, it is only a matter of
using the `...` javascript operator. For example:
```xml
<t t-foreach="...items" t-as="item">...</t>
```
The `...` operator will convert the `Set` (or any other iterables) into a list,
which will work with Owl QWeb.
### Rendering Sub Templates
QWeb templates can be used for top level rendering, but they can also be used
@@ -558,22 +509,6 @@ This can be used to define variables scoped to a sub template:
<!-- "var" does not exist here -->
```
### Dynamic sub templates
The `t-call` directive can also be used to dynamically call a sub template,
using string interpolation. For example:
```xml
<div t-name="main-template">
<t t-call="{{template}}">
<em>content</em>
</t>
</div>
```
Here, the name of the template is obtained from the `template` value in the
template rendering context.
### Translations
By default, QWeb specify that templates should be translated. If this behaviour
+3 -18
View File
@@ -25,8 +25,6 @@ some sub template, but still be the owner. For example, a generic dialog compone
will need to render some content, some footer, but with the parent as the
rendering context.
Slots are inserted with the `t-slot` directive:
```xml
<div t-name="Dialog" class="modal">
<div class="modal-title"><t t-esc="props.title"/></div>
@@ -44,7 +42,7 @@ Slots are defined by the caller, with the `t-set-slot` directive:
```xml
<div t-name="SomeComponent">
<div>some component</div>
<Dialog title="'Some Dialog'">
<Dialog title="Some Dialog">
<t t-set-slot="content">
<div>hey</div>
</t>
@@ -64,9 +62,7 @@ This is deprecated and should no longer be used in new code.
## Reference
### Default Slot
The first element inside the component which is not a named slot will
Default slot: the first element inside the component which is not a named slot will
be considered the `default` slot. For example:
```xml
@@ -81,9 +77,7 @@ be considered the `default` slot. For example:
</div>
```
### Default content
Slots can define a default content, in case the parent did not define them:
Default content: slots can define a default content, in case the parent did not define them:
```xml
<div t-name="Parent">
@@ -100,12 +94,3 @@ 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).
### Dynamic Slots
The `t-slot` directive is actually able to use any expressions, using string
interplolation:
```xml
<t t-slot="{{current}}" />
```
+10 -10
View File
@@ -62,14 +62,14 @@ The CSS tag is useful to define a css stylesheet in the javascript file:
```js
class MyComponent extends Component {
static template = xml`
static template = xml`
<div class="my-component">some template</div>
`;
static style = css`
.my-component {
color: red;
}
`;
static css`
.my-component {
color: red;
}
`;
}
```
@@ -176,9 +176,9 @@ const uuid = generateUUID();
class MyComponent extends Component {
static template = xml`<div data-o-${uuid}="">...</div>`;
static style = css`
[data-o-${uuid}] {
color: red;
}
`;
[data-o-${uuid}] {
color: red;
}
`;
}
```
+4 -4
View File
@@ -21,8 +21,8 @@ argument, it executes it as soon as the DOM ready (or directly).
```js
Promise.all([loadFile("templates.xml"), owl.utils.whenReady()]).then(function ([templates]) {
const qweb = new owl.QWeb({ templates });
const env = { qweb };
await mount(App, { env, target: document.body });
const app = new App({ qweb });
app.mount(document.body);
});
```
@@ -31,8 +31,8 @@ or alternatively:
```js
owl.utils.whenReady(function () {
const qweb = new owl.QWeb();
const env = { qweb };
await mount(App, { env, target: document.body });
const app = new App({ qweb });
app.mount(document.body);
});
```
+19 -19
View File
@@ -1,20 +1,23 @@
{
"name": "@odoo/owl",
"version": "1.4.9",
"version": "1.0.11",
"description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js",
"browser": "dist/owl.iife.js",
"module": "dist/owl.es.js",
"main": "dist/owl.js",
"types": "dist/types/index.d.ts",
"files": [
"dist"
"dist/types/",
"dist/owl.js",
"dist/owl-iife.js"
],
"engines": {
"node": ">=12.18.3"
"node": ">=10.15.3"
},
"scripts": {
"build:js": "tsc --target esnext --module es6 --outDir dist/owl",
"build:bundle": "rollup -c",
"build": "npm run build:bundle",
"build": "npm run build:js && npm run build:bundle",
"buildcommonjs": "npm run build:js && npm run build:bundle -- -f cjs",
"minify": "uglifyjs dist/owl.js -o dist/owl.min.js --compress --mangle",
"test": "jest",
"test:watch": "jest --watch",
"tools:serve": "python3 tools/server.py || python tools/server.py",
@@ -22,8 +25,7 @@
"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",
"check-formatting": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --check",
"publish": "npm run build && npm publish",
"publish": "npm run build && mv dist/owl.js dist/owl-iife.js && npm run buildcommonjs && npm publish",
"release": "node tools/release.js"
},
"repository": {
@@ -36,30 +38,28 @@
"url": "https://github.com/odoo/owl/issues"
},
"homepage": "https://github.com/odoo/owl#readme",
"dependencies": {},
"devDependencies": {
"@types/jest": "^27.0.1",
"@types/node": "^14.11.8",
"@types/jest": "^23.3.12",
"chalk": "^3.0.0",
"cpx": "^1.5.0",
"current-git-branch": "^1.1.0",
"git-rev-sync": "^1.12.0",
"github-api": "^3.3.0",
"jest": "^27.1.0",
"jest-environment-jsdom": "^27.1.0",
"jest": "^23.6.0",
"jest-environment-jsdom": "^24.7.1",
"live-server": "^1.2.1",
"monaco-editor": "^0.21.2",
"npm-run-all": "^4.1.5",
"prettier": "^2.0.4",
"rollup": "^2.56.3",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.30.0",
"rollup": "^1.6.0",
"rollup-plugin-typescript2": "^0.20.1",
"sass": "^1.16.1",
"source-map-support": "^0.5.10",
"ts-jest": "^27.0.5",
"ts-jest": "^23.10.5",
"typescript": "^3.7.2",
"uglify-es": "^3.3.9"
},
"jest": {
"testEnvironment": "jsdom",
"roots": [
"<rootDir>/src",
"<rootDir>/tests"
+1 -1
View File
@@ -1,6 +1,6 @@
# 🦉 OWL Roadmap 🦉
- Current version: 1.4.9
- Current version: 1.0.11
- Status: stable
This roadmap is only an attempt at predicting Owl's future. Everything may
+10 -66
View File
@@ -1,70 +1,14 @@
import pkg from "./package.json";
import { version } from "./package.json";
import git from "git-rev-sync";
import typescript from 'rollup-plugin-typescript2';
import { terser } from "rollup-plugin-terser";
const name = "owl";
const extend = true;
/**
* Meta data to be added on the __info__ object.
* Used to let external tools know the current owl version.
*/
const outro = `
__info__.version = '${pkg.version}';
__info__.date = '${new Date().toISOString()}';
__info__.hash = '${git.short()}';
__info__.url = 'https://github.com/odoo/owl';
`;
/**
* Generate from a string depicting a path a new path for the minified version.
* @param {string} pkgFileName file name
*/
function generateMinifiedNameFromPkgName(pkgFileName) {
const parts = pkgFileName.split('.');
parts.splice(parts.length - 1, 0, "min");
return parts.join('.');
}
/**
* Get the rollup config based on the arguments
* @param {string} format format of the bundle
* @param {string} generatedFileName generated file name
* @param {boolean} minified should it be minified
*/
function getConfigForFormat(format, generatedFileName, minified = false) {
return {
file: minified ? generateMinifiedNameFromPkgName(generatedFileName) : generatedFileName,
format: format,
name: name,
extend: extend,
outro: outro,
plugins: minified ? [terser()] : [],
indent: ' ', // indent with 4 spaces
};
}
// rollup.config.js
export default {
input: "src/index.ts",
output: [
/**
* Read about module formats:
* https://auth0.com/blog/javascript-module-systems-showdown/
* https://medium.com/@kelin2025/so-you-wanna-use-es6-modules-714f48b3a953
*/
getConfigForFormat('esm', pkg.module),
getConfigForFormat('esm', pkg.module, true),
getConfigForFormat('cjs', pkg.main),
getConfigForFormat('cjs', pkg.main, true),
getConfigForFormat('iife', pkg.browser),
getConfigForFormat('iife', pkg.browser, true),
],
plugins: [
typescript({
useTsconfigDeclarationDir: true
}),
]
input: "dist/owl/index.js",
output: {
file: "dist/owl.js",
format: "iife",
name: "owl",
extend: true,
outro: `exports.__info__.version = '${version}';\nexports.__info__.date = '${new Date().toISOString()}';\nexports.__info__.hash = '${git.short()}';\nexports.__info__.url = 'https://github.com/odoo/owl';`
}
};
+1 -8
View File
@@ -10,8 +10,6 @@ export interface Browser {
localStorage: Window["localStorage"];
}
let localStorage: Window["localStorage"] | null = null;
export const browser: Browser = {
setTimeout: window.setTimeout.bind(window),
clearTimeout: window.clearTimeout.bind(window),
@@ -21,10 +19,5 @@ export const browser: Browser = {
random: Math.random,
Date: window.Date,
fetch: (window.fetch || (() => {})).bind(window),
get localStorage() {
return localStorage || window.localStorage;
},
set localStorage(newLocalStorage: Window["localStorage"]) {
localStorage = newLocalStorage;
},
localStorage: window.localStorage,
};
+83 -133
View File
@@ -44,15 +44,6 @@ interface MountOptions {
position?: MountPosition;
}
export const enum STATUS {
CREATED,
WILLSTARTED, // willstart has been called
RENDERED, // first render is completed (so, vnode is now defined)
MOUNTED, // is ready, and in DOM. It has a valid el
UNMOUNTED, // has a valid el, but is not in DOM
DESTROYED,
}
/**
* This is mostly an internal detail of implementation. The Meta interface is
* useful to typecheck and describe the internal keys used by Owl to manage the
@@ -65,7 +56,8 @@ interface Internal<T extends Env> {
depth: number;
vnode: VNode | null;
pvnode: VNode | null;
status: STATUS;
isMounted: boolean;
isDestroyed: boolean;
// parent and children keys are obviously useful to setup the parent-children
// relationship.
@@ -167,23 +159,20 @@ export class Component<Props extends {} = any, T extends Env = Env> {
if (!this.env.qweb) {
this.env.qweb = new QWeb();
}
// TODO: remove this in owl 2.0
if (!this.env.browser) {
this.env.browser = browser;
}
this.env.qweb.on("update", this, () => {
switch (this.__owl__.status) {
case STATUS.MOUNTED:
this.render(true);
break;
case STATUS.DESTROYED:
// this is unlikely to happen, but if a root widget is destroyed,
// we want to remove our subscription. The usual way to do that
// would be to perform some check in the destroy method, but since
// it is very performance sensitive, and since this is a rare event,
// we simply do it lazily
this.env.qweb.off("update", this);
break;
if (this.__owl__.isMounted) {
this.render(true);
}
if (this.__owl__.isDestroyed) {
// this is unlikely to happen, but if a root widget is destroyed,
// we want to remove our subscription. The usual way to do that
// would be to perform some check in the destroy method, but since
// it is very performance sensitive, and since this is a rare event,
// we simply do it lazily
this.env.qweb.off("update", this);
}
});
depth = 0;
@@ -196,7 +185,8 @@ export class Component<Props extends {} = any, T extends Env = Env> {
depth: depth,
vnode: null,
pvnode: null,
status: STATUS.CREATED,
isMounted: false,
isDestroyed: false,
parent: parent || null,
children: {},
cmap: {},
@@ -218,20 +208,8 @@ export class Component<Props extends {} = any, T extends Env = Env> {
if (constr.style) {
this.__applyStyles(constr);
}
this.setup();
}
/**
* setup is run just after the component is constructed. This is the standard
* location where the component can setup its hooks. It has some advantages
* over the constructor:
* - it can be patched (useful in odoo ecosystem)
* - it does not need to propagate the arguments to the super call
*
* Note: this method should not be called manually.
*/
setup() {}
/**
* willStart is an asynchronous hook that can be implemented to perform some
* action before the initial rendering of a component.
@@ -326,49 +304,42 @@ export class Component<Props extends {} = any, T extends Env = Env> {
* Note that a component can be mounted an unmounted several times
*/
async mount(target: HTMLElement | DocumentFragment, options: MountOptions = {}): Promise<void> {
const position = options.position || "last-child";
const __owl__ = this.__owl__;
if (__owl__.isMounted) {
if (position !== "self" && this.el!.parentNode !== target) {
// in this situation, we are trying to mount a component on a different
// target. In this case, we need to unmount first, otherwise it will
// not work.
this.unmount();
} else {
return Promise.resolve();
}
}
if (__owl__.isDestroyed) {
throw new Error("Cannot mount a destroyed component");
}
if (__owl__.currentFiber) {
const currentFiber = __owl__.currentFiber;
if (currentFiber.target === target && currentFiber.position === position) {
return scheduler.addFiber(currentFiber);
} else {
scheduler.rejectFiber(currentFiber, "Mounting operation cancelled");
}
}
if (!(target instanceof HTMLElement || target instanceof DocumentFragment)) {
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);
}
const position = options.position || "last-child";
const __owl__ = this.__owl__;
const currentFiber = __owl__.currentFiber;
switch (__owl__.status) {
case STATUS.CREATED: {
const fiber = new Fiber(null, this, true, target, position);
fiber.shouldPatch = false;
this.__prepareAndRender(fiber, () => {});
return scheduler.addFiber(fiber);
}
case STATUS.WILLSTARTED:
case STATUS.RENDERED:
currentFiber.target = target;
currentFiber.position = position;
return scheduler.addFiber(currentFiber);
case STATUS.UNMOUNTED: {
const fiber = new Fiber(null, this, true, target, position);
fiber.shouldPatch = false;
this.__render(fiber);
return scheduler.addFiber(fiber);
}
case STATUS.MOUNTED: {
if (position !== "self" && this.el!.parentNode !== target) {
const fiber = new Fiber(null, this, true, target, position);
fiber.shouldPatch = false;
this.__render(fiber);
return scheduler.addFiber(fiber);
} else {
return Promise.resolve();
}
}
case STATUS.DESTROYED:
throw new Error("Cannot mount a destroyed component");
const fiber = new Fiber(null, this, false, target, position);
fiber.shouldPatch = false;
if (!__owl__.vnode) {
this.__prepareAndRender(fiber, () => {});
} else {
this.__render(fiber);
}
return scheduler.addFiber(fiber);
}
/**
@@ -376,7 +347,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
* to call willUnmount calls and remove the component from the DOM.
*/
unmount() {
if (this.__owl__.status === STATUS.MOUNTED) {
if (this.__owl__.isMounted) {
this.__callWillUnmount();
this.el!.remove();
}
@@ -394,23 +365,27 @@ export class Component<Props extends {} = any, T extends Env = Env> {
async render(force: boolean = false): Promise<void> {
const __owl__ = this.__owl__;
const currentFiber = __owl__.currentFiber;
if (!__owl__.vnode && !currentFiber) {
if (!__owl__.isMounted && !currentFiber) {
// if we get here, this means that the component was either never mounted,
// or was unmounted and some state change triggered a render. Either way,
// we do not want to actually render anything in this case.
return;
}
if (currentFiber && !currentFiber.isRendered && !currentFiber.isCompleted) {
return scheduler.addFiber(currentFiber.root);
}
// if we aren't mounted at this point, it implies that there is a
// currentFiber that is already rendered (isRendered is true), so we are
// about to be mounted
const status = __owl__.status;
const isMounted = __owl__.isMounted;
const fiber = new Fiber(null, this, force, null, null);
Promise.resolve().then(() => {
if (__owl__.status === STATUS.MOUNTED || status !== STATUS.MOUNTED) {
if (fiber.isCompleted || fiber.isRendered) {
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);
} else {
// we were mounted when render was called, but we aren't anymore, so we
@@ -434,7 +409,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
*/
destroy() {
const __owl__ = this.__owl__;
if (__owl__.status !== STATUS.DESTROYED) {
if (!__owl__.isDestroyed) {
const el = this.el;
this.__destroy(__owl__.parent);
if (el) {
@@ -458,8 +433,8 @@ export class Component<Props extends {} = any, T extends Env = Env> {
* up to the parent DOM nodes. Thus, it must be called between mounted() and
* willUnmount().
*/
trigger<T = any>(eventType: string, payload?: T) {
this.__trigger<T>(this, eventType, payload);
trigger(eventType: string, payload?: any) {
this.__trigger(this, eventType, payload);
}
//--------------------------------------------------------------------------
@@ -479,12 +454,13 @@ export class Component<Props extends {} = any, T extends Env = Env> {
*/
__destroy(parent: Component | null) {
const __owl__ = this.__owl__;
if (__owl__.status === STATUS.MOUNTED) {
const isMounted = __owl__.isMounted;
if (isMounted) {
if (__owl__.willUnmountCB) {
__owl__.willUnmountCB();
}
this.willUnmount();
__owl__.status = STATUS.UNMOUNTED;
__owl__.isMounted = false;
}
const children = __owl__.children;
for (let key in children) {
@@ -495,7 +471,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
delete parent.__owl__.children[id];
__owl__.parent = null;
}
__owl__.status = STATUS.DESTROYED;
__owl__.isDestroyed = true;
delete __owl__.vnode;
if (__owl__.currentFiber) {
__owl__.currentFiber.isCompleted = true;
@@ -505,7 +481,8 @@ export class Component<Props extends {} = any, T extends Env = Env> {
__callMounted() {
const __owl__ = this.__owl__;
__owl__.status = STATUS.MOUNTED;
__owl__.isMounted = true;
__owl__.currentFiber = null;
this.mounted();
if (__owl__.mountedCB) {
__owl__.mountedCB();
@@ -518,7 +495,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
__owl__.willUnmountCB();
}
this.willUnmount();
__owl__.status = STATUS.UNMOUNTED;
__owl__.isMounted = false;
if (__owl__.currentFiber) {
__owl__.currentFiber.isCompleted = true;
__owl__.currentFiber.root.counter = 0;
@@ -526,7 +503,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
const children = __owl__.children;
for (let id in children) {
const comp = children[id];
if (comp.__owl__.status === STATUS.MOUNTED) {
if (comp.__owl__.isMounted) {
comp.__callWillUnmount();
}
}
@@ -535,9 +512,9 @@ export class Component<Props extends {} = any, T extends Env = Env> {
* Private trigger method, allows to choose the component which triggered
* the event in the first place
*/
__trigger<T>(component: Component, eventType: string, payload?: T) {
__trigger(component: Component, eventType: string, payload?: any) {
if (this.el) {
const ev = new OwlEvent<T>(component, eventType, {
const ev = new OwlEvent(component, eventType, {
bubbles: true,
cancelable: true,
detail: payload,
@@ -647,25 +624,18 @@ export class Component<Props extends {} = any, T extends Env = Env> {
}
return p._template;
}
async __prepareAndRender(fiber: Fiber, cb: CallableFunction) {
try {
const proms = Promise.all([
this.willStart(),
this.__owl__.willStartCB && this.__owl__.willStartCB(),
]);
this.__owl__.status = STATUS.WILLSTARTED;
await proms;
if (this.__owl__.status === <any>STATUS.DESTROYED) {
return Promise.resolve();
}
await Promise.all([this.willStart(), this.__owl__.willStartCB && this.__owl__.willStartCB()]);
} catch (e) {
fiber.handleError(e);
return Promise.resolve();
}
if (this.__owl__.isDestroyed) {
return Promise.resolve();
}
if (!fiber.isCompleted) {
this.__render(fiber);
this.__owl__.status = STATUS.RENDERED;
cb();
}
}
@@ -688,7 +658,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
for (let childKey in __owl__.children) {
const child = __owl__.children[childKey];
const childOwl = child.__owl__;
if (childOwl.status !== STATUS.MOUNTED && childOwl.parentLastFiberId < fiber.id) {
if (!childOwl.isMounted && childOwl.parentLastFiberId < fiber.id) {
// we only do here a "soft" destroy, meaning that we leave the child
// dom node alone, without removing it. Most of the time, it does not
// matter, because the child component is already unmounted. However,
@@ -735,6 +705,17 @@ export class Component<Props extends {} = any, T extends Env = Env> {
}
}
/**
* Only called by qweb t-component directive (when t-keepalive is set)
*/
__remount() {
const __owl__ = this.__owl__;
if (!__owl__.isMounted) {
__owl__.isMounted = true;
this.mounted();
}
}
/**
* Apply default props (only top level).
*
@@ -748,34 +729,3 @@ export class Component<Props extends {} = any, T extends Env = Env> {
}
}
}
interface MountParameters {
env?: Env;
target: HTMLElement | DocumentFragment;
props?: any;
position?: MountOptions["position"];
}
interface Type<T> extends Function {
new (...args: any[]): T;
}
export async function mount<T extends Type<Component>>(
C: T,
params: MountParameters
): Promise<InstanceType<T>> {
const { env, props, target } = params;
let origEnv = C.hasOwnProperty("env") ? (C as any).env : null;
if (env) {
(C as any as typeof Component).env = env;
}
const component: Component = new C(null, props);
if (origEnv) {
(C as any).env = origEnv;
} else {
delete (C as any).env;
}
const position = params.position || "last-child";
await component.mount(target, { position });
return component as any;
}
+16 -43
View File
@@ -1,7 +1,6 @@
import { QWeb } from "../qweb/index";
import { INTERP_REGEXP } from "../qweb/compilation_context";
import { makeHandlerCode, MODS_CODE } from "../qweb/extensions";
import { STATUS } from "./component";
//------------------------------------------------------------------------------
// t-component
@@ -228,17 +227,11 @@ QWeb.addDirective({
if (name.startsWith("t-on-")) {
events.push([name, value]);
} else if (name === "t-transition") {
if (QWeb.enableTransitions) {
transition = value;
}
transition = value;
} else if (!name.startsWith("t-")) {
if (name !== "class" && name !== "style") {
// this is a prop!
if (value.includes("=>")) {
props[name] = ctx.captureExpression(value);
} else {
props[name] = ctx.formatExpression(value) || "undefined";
}
props[name] = ctx.formatExpression(value) || "undefined";
}
}
}
@@ -249,18 +242,7 @@ QWeb.addDirective({
.join(",");
let componentID = ctx.generateID();
let hasDefinedKey = false;
let templateKey;
if (node.tagName === "t" && !node.hasAttribute("t-key") && value.match(INTERP_REGEXP)) {
defineComponentKey();
const id = ctx.generateID();
// the ___ is to make sure we have no possible conflict with normal
// template keys
ctx.addLine(`let k${id} = '___' + componentKey${componentID}`);
templateKey = `k${id}`;
} else {
templateKey = ctx.generateTemplateKey();
}
const templateKey = ctx.generateTemplateKey();
let ref = node.getAttribute("t-ref");
let refExpr = "";
let refKey: string = "";
@@ -306,7 +288,7 @@ QWeb.addDirective({
if (tattClass) {
let tattExpr = ctx.formatExpression(tattClass);
if (tattExpr[0] !== "{" || tattExpr[tattExpr.length - 1] !== "}") {
tattExpr = `utils.toClassObj(${tattExpr})`;
tattExpr = `utils.toObj(${tattExpr})`;
}
if (classAttr) {
ctx.addLine(`Object.assign(${classObj}, ${tattExpr})`);
@@ -351,7 +333,7 @@ QWeb.addDirective({
}
if (hasDynamicProps) {
const dynamicProp = ctx.formatExpression(node.getAttribute("t-props")!);
ctx.addLine(`let props${componentID} = Object.assign({}, ${dynamicProp}, {${propStr}});`);
ctx.addLine(`let props${componentID} = Object.assign({${propStr}}, ${dynamicProp});`);
} else {
ctx.addLine(`let props${componentID} = {${propStr}};`);
}
@@ -370,14 +352,14 @@ QWeb.addDirective({
// SLOTS
const hasSlots = node.childNodes.length;
let scope = hasSlots ? `utils.combine(context, scope)` : "undefined";
let scope = hasSlots ? `Object.assign(Object.create(context), scope)` : "undefined";
ctx.addIf(`w${componentID}`);
// need to update component
let styleCode = "";
if (tattStyle) {
styleCode = `.then(()=>{if (w${componentID}.__owl__.status === ${STATUS.DESTROYED}) {return};w${componentID}.el.style=${tattStyle};});`;
styleCode = `.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};w${componentID}.el.style=${tattStyle};});`;
}
ctx.addLine(
`w${componentID}.__updateProps(props${componentID}, extra.fiber, ${scope})${styleCode};`
@@ -393,17 +375,14 @@ QWeb.addDirective({
ctx.addElse();
// new component
function defineComponentKey() {
if (!hasDefinedKey) {
const interpValue = ctx.interpolate(value);
ctx.addLine(`let componentKey${componentID} = ${interpValue};`);
hasDefinedKey = true;
}
let dynamicFallback = "";
if (!value.match(INTERP_REGEXP)) {
dynamicFallback = `|| ${ctx.formatExpression(value)}`;
}
defineComponentKey();
const contextualValue = value.match(INTERP_REGEXP) ? "false" : ctx.formatExpression(value);
const interpValue = ctx.interpolate(value);
ctx.addLine(`let componentKey${componentID} = ${interpValue};`);
ctx.addLine(
`let W${componentID} = ${contextualValue} || context.constructor.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}];`
`let W${componentID} = context.constructor.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}]${dynamicFallback};`
);
// maybe only do this in dev mode...
@@ -455,7 +434,6 @@ QWeb.addDirective({
isInSubComponent = true;
break;
}
el = el.parentElement;
}
if (isInSubComponent) {
continue;
@@ -468,22 +446,17 @@ QWeb.addDirective({
slotNode.removeAttribute("t-set-slot");
slotNode.parentElement!.removeChild(slotNode);
const slotFn = qweb._compile(`slot_${key}_template`, { elem: slotNode, hasParent: true });
const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx);
QWeb.slots[`${slotId}_${key}`] = slotFn;
}
}
if (clone.childNodes.length) {
let hasContent = false;
const t = clone.ownerDocument!.createElement("t");
for (let child of Object.values(clone.childNodes)) {
hasContent =
hasContent || (child instanceof Text ? Boolean(child.textContent.trim().length) : true);
t.appendChild(child);
}
if (hasContent) {
const slotFn = qweb._compile(`slot_default_template`, { elem: t, hasParent: true });
QWeb.slots[`${slotId}_default`] = slotFn;
}
const slotFn = qweb._compile(`slot_default_template`, t, ctx);
QWeb.slots[`${slotId}_default`] = slotFn;
}
}
+32 -66
View File
@@ -1,5 +1,5 @@
import { h, VNode } from "../vdom/index";
import { Component, MountPosition, STATUS } from "./component";
import { Component, MountPosition } from "./component";
import { scheduler } from "./scheduler";
/**
@@ -82,7 +82,6 @@ export class Fiber {
let oldFiber = __owl__.currentFiber;
if (oldFiber && !oldFiber.isCompleted) {
this.force = true;
if (oldFiber.root === oldFiber && !parent) {
// both oldFiber and this fiber are root fibers
this._reuseFiber(oldFiber);
@@ -107,8 +106,6 @@ export class Fiber {
*/
_reuseFiber(oldFiber: Fiber) {
oldFiber.cancel(); // cancel children fibers
oldFiber.target = this.target || oldFiber.target;
oldFiber.position = this.position || oldFiber.position;
oldFiber.isCompleted = false; // keep the root fiber alive
oldFiber.isRendered = false; // the fiber has to be re-rendered
if (oldFiber.child) {
@@ -190,15 +187,13 @@ export class Fiber {
complete() {
let component = this.component;
this.isCompleted = true;
const status = component.__owl__.status;
if (status === STATUS.DESTROYED) {
if (!this.target && !component.__owl__.isMounted) {
return;
}
// build patchQueue
const patchQueue: Fiber[] = [];
const doWork: (Fiber) => Fiber | null = function (f) {
f.component.__owl__.currentFiber = null;
patchQueue.push(f);
return f.child;
};
@@ -206,16 +201,14 @@ export class Fiber {
const patchLen = patchQueue.length;
// call willPatch hook on each fiber of patchQueue
if (status === STATUS.MOUNTED) {
for (let i = 0; i < patchLen; i++) {
const fiber = patchQueue[i];
if (fiber.shouldPatch) {
component = fiber.component;
if (component.__owl__.willPatchCB) {
component.__owl__.willPatchCB();
}
component.willPatch();
for (let i = 0; i < patchLen; i++) {
const fiber = patchQueue[i];
if (fiber.shouldPatch) {
component = fiber.component;
if (component.__owl__.willPatchCB) {
component.__owl__.willPatchCB();
}
component.willPatch();
}
}
@@ -243,9 +236,8 @@ export class Fiber {
}
component.__patch(target!, fiber.vnode!);
} else {
const vnode = component.__owl__.vnode;
if (fiber.shouldPatch && vnode) {
component.__patch(vnode, fiber.vnode!);
if (fiber.shouldPatch) {
component.__patch(component.__owl__.vnode!, fiber.vnode!);
// When updating a Component's props (in directive),
// the component has a pvnode AND should be patched.
// However, its pvnode.elm may have changed if it is a High Order Component
@@ -257,6 +249,9 @@ export class Fiber {
component.__owl__.pvnode!.elm = component.__owl__.vnode!.elm;
}
}
if (fiber === component.__owl__.currentFiber) {
component.__owl__.currentFiber = null;
}
}
// insert into the DOM (mount case)
@@ -275,24 +270,16 @@ export class Fiber {
}
// call patched/mounted hook on each fiber of (reversed) patchQueue
if (status === STATUS.MOUNTED || inDOM) {
for (let i = patchLen - 1; i >= 0; i--) {
const fiber = patchQueue[i];
component = fiber.component;
if (fiber.shouldPatch && !this.target) {
component.patched();
if (component.__owl__.patchedCB) {
component.__owl__.patchedCB();
}
} else {
component.__callMounted();
for (let i = patchLen - 1; i >= 0; i--) {
const fiber = patchQueue[i];
component = fiber.component;
if (fiber.shouldPatch && !this.target) {
component.patched();
if (component.__owl__.patchedCB) {
component.__owl__.patchedCB();
}
}
} else {
for (let i = patchLen - 1; i >= 0; i--) {
const fiber = patchQueue[i];
component = fiber.component;
component.__owl__.status = STATUS.UNMOUNTED;
} else if (this.target ? inDOM : true) {
component.__callMounted();
}
}
}
@@ -324,43 +311,22 @@ export class Fiber {
const qweb = component.env.qweb;
let root = component;
function handle(error) {
let canCatch = false;
qweb.trigger("error", error);
while (component && !(canCatch = !!component.catchError)) {
root = component;
component = component.__owl__.parent!;
}
if (canCatch) {
try {
component.catchError!(error);
} catch (e) {
root = component;
component = component.__owl__.parent!;
return handle(e);
}
return true;
}
return false;
let canCatch = false;
while (component && !(canCatch = !!component.catchError)) {
root = component;
component = component.__owl__.parent!;
}
qweb.trigger("error", error);
let isHandled = handle(error);
if (!isHandled) {
if (canCatch) {
component.catchError!(error);
} else {
// the 3 next lines aim to mark the root fiber as being in error, and
// to force it to end, without waiting for its children
this.root.counter = 0;
this.root.error = error;
scheduler.flush();
// at this point, the state of the application is corrupted and we could
// have a lot of issues or crashes. So we destroy the application in a try
// catch and swallow these errors because the fiber is already in error,
// and this is the actual issue that needs to be solved, not those followup
// errors.
try {
root.destroy();
} catch (e) {}
root.destroy();
}
}
}
+7 -19
View File
@@ -1,5 +1,4 @@
import { QWeb } from "./qweb/index";
import { TRANSLATABLE_ATTRS } from "./qweb/qweb";
/**
* This file creates and exports the OWL 'config' object, with keys:
@@ -9,13 +8,9 @@ import { TRANSLATABLE_ATTRS } from "./qweb/qweb";
interface Config {
mode: string;
enableTransitions: boolean;
translatableAttributes: string[];
}
export const config = {
translatableAttributes: TRANSLATABLE_ATTRS,
} as Config;
export const config = {} as Config;
Object.defineProperty(config, "mode", {
get() {
@@ -24,19 +19,12 @@ Object.defineProperty(config, "mode", {
set(mode: string) {
QWeb.dev = mode === "dev";
if (QWeb.dev) {
console.info(`Owl is running in 'dev' mode.
This is not suitable for production use.
See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for more information.`);
const url = `https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode`;
console.warn(
`Owl is running in 'dev' mode. This is not suitable for production use. See ${url} for more information.`
);
} else {
console.log(`Owl is now running in 'prod' mode.`);
}
},
});
Object.defineProperty(config, "enableTransitions", {
get() {
return QWeb.enableTransitions;
},
set(value: boolean) {
QWeb.enableTransitions = value;
},
});
+10
View File
@@ -115,6 +115,16 @@ export function useContextWithCB(ctx: Context, component: Component, method): an
__owl__.observer = new Observer();
__owl__.observer.notifyCB = component.render.bind(component);
}
const currentCB = __owl__.observer.notifyCB;
__owl__.observer.notifyCB = function () {
if (ctx.rev > mapping[id]) {
// in this case, the context has been updated since we were rendering
// last, and we do not need to render here with the observer. A
// rendering is coming anyway, with the correct props.
return;
}
currentCB();
};
mapping[id] = 0;
const renderFn = __owl__.renderFn;
+1 -21
View File
@@ -1,4 +1,4 @@
import { Component, Env } from "./component/component";
import { Component } from "./component/component";
import { Observer } from "./core/observer";
/**
@@ -118,26 +118,6 @@ export function useRef<C extends Component = Component>(name: string): Ref<C> {
};
}
// -----------------------------------------------------------------------------
// "Builder" hooks
// -----------------------------------------------------------------------------
/**
* This hook is useful as a building block for some customized hooks, that may
* need a reference to the component calling them.
*/
export function useComponent<P, E extends Env>(): Component<P, E> {
return Component.current as any;
}
/**
* This hook is useful as a building block for some customized hooks, that may
* need a reference to the env of the component calling them.
*/
export function useEnv<E extends Env>(): E {
return Component.current.env as any;
}
// -----------------------------------------------------------------------------
// useSubEnv
// -----------------------------------------------------------------------------
+1 -3
View File
@@ -19,10 +19,9 @@ import { Link } from "./router/link";
import { RouteComponent } from "./router/route_component";
import { Router } from "./router/router";
export { Component, mount } from "./component/component";
export { Component } from "./component/component";
export { QWeb };
export { config };
export { browser } from "./browser";
export const Context = _context.Context;
export const useState = _hooks.useState;
@@ -38,5 +37,4 @@ export const hooks = Object.assign({}, _hooks, {
useGetters: _store.useGetters,
useStore: _store.useStore,
});
export const __info__ = {};
+16 -33
View File
@@ -1,4 +1,4 @@
import { CompilationContext, INTERP_REGEXP } from "./compilation_context";
import { CompilationContext } from "./compilation_context";
import { QWeb } from "./qweb";
import { htmlToVDOM } from "../vdom/html_to_vdom";
import { QWebVar } from "./expression_parser";
@@ -224,36 +224,19 @@ QWeb.addDirective({
ctx.rootContext.shouldDefineScope = true;
ctx.rootContext.shouldDefineUtils = true;
const subTemplate = node.getAttribute("t-call")!;
const isDynamic = INTERP_REGEXP.test(subTemplate);
const nodeTemplate = qweb.templates[subTemplate];
if (!isDynamic && !nodeTemplate) {
if (!nodeTemplate) {
throw new Error(`Cannot find template "${subTemplate}" (t-call)`);
}
// Step 2: compile target template in sub templates
// ------------------------------------------------
let subIdstr: string;
if (isDynamic) {
const _id = ctx.generateID();
ctx.addLine(`let tname${_id} = ${ctx.interpolate(subTemplate)};`);
ctx.addLine(`let tid${_id} = this.subTemplates[tname${_id}];`);
ctx.addIf(`!tid${_id}`);
ctx.addLine(`tid${_id} = this.constructor.nextId++;`);
ctx.addLine(`this.subTemplates[tname${_id}] = tid${_id};`);
ctx.addLine(
`this.constructor.subTemplates[tid${_id}] = this._compile(tname${_id}, {hasParent: true, defineKey: true});`
);
ctx.closeIf();
subIdstr = `tid${_id}`;
} else {
let subId = qweb.subTemplates[subTemplate];
if (!subId) {
subId = QWeb.nextId++;
qweb.subTemplates[subTemplate] = subId;
const subTemplateFn = qweb._compile(subTemplate, { hasParent: true, defineKey: true });
QWeb.subTemplates[subId] = subTemplateFn;
}
subIdstr = `'${subId}'`;
let subId = qweb.subTemplates[subTemplate];
if (!subId) {
subId = QWeb.nextId++;
qweb.subTemplates[subTemplate] = subId;
const subTemplateFn = qweb._compile(subTemplate, nodeTemplate.elem, ctx, true);
QWeb.subTemplates[subId] = subTemplateFn;
}
// Step 3: compile t-call body if necessary
@@ -268,32 +251,33 @@ QWeb.addDirective({
for (let attr of ["t-if", "t-else", "t-elif", "t-call"]) {
nodeCopy.removeAttribute(attr);
}
const parentNode = ctx.parentNode;
ctx.parentNode = "__0";
// this local scope is intended to trap c__0
ctx.addLine(`{`);
ctx.indent();
ctx.addLine("let c__0 = [];");
qweb._compileNode(nodeCopy, ctx.subContext("parentNode", "__0"));
qweb._compileNode(nodeCopy, ctx);
ctx.rootContext.shouldDefineUtils = true;
ctx.addLine("scope[utils.zero] = c__0;");
ctx.parentNode = parentNode;
ctx.dedent();
ctx.addLine(`}`);
}
// Step 4: add the appropriate function call to current component
// ------------------------------------------------
const parentComponent = ctx.rootContext.shouldDefineParent
? `parent`
: `utils.getComponent(context)`;
const parentComponent = `utils.getComponent(context)`;
const key = ctx.generateTemplateKey();
const parentNode = ctx.parentNode ? `c${ctx.parentNode}` : "result";
const extra = `Object.assign({}, extra, {parentNode: ${parentNode}, parent: ${parentComponent}, key: ${key}})`;
if (ctx.parentNode) {
ctx.addLine(`this.constructor.subTemplates[${subIdstr}].call(this, scope, ${extra});`);
ctx.addLine(`this.constructor.subTemplates['${subId}'].call(this, scope, ${extra});`);
} else {
// this is a t-call with no parentnode, we need to extract the result
ctx.rootContext.shouldDefineResult = true;
ctx.addLine(`result = []`);
ctx.addLine(`this.constructor.subTemplates[${subIdstr}].call(this, scope, ${extra});`);
ctx.addLine(`this.constructor.subTemplates['${subId}'].call(this, scope, ${extra});`);
ctx.addLine(`result = result[0]`);
}
@@ -326,8 +310,7 @@ QWeb.addDirective({
ctx.addLine(`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`);
let keysID = ctx.generateID();
let valuesID = ctx.generateID();
ctx.addLine(`let _${keysID} = _${arrayID};`);
ctx.addLine(`let _${valuesID} = _${arrayID};`);
ctx.addLine(`let _${keysID} = _${valuesID} = _${arrayID};`);
ctx.addIf(`!(_${arrayID} instanceof Array)`);
ctx.addLine(`_${keysID} = Object.keys(_${arrayID});`);
ctx.addLine(`_${valuesID} = Object.values(_${arrayID});`);
+1 -12
View File
@@ -163,18 +163,7 @@ export class CompilationContext {
const done = new Set();
return tokens
.map((tok) => {
// "this" in captured expressions should be the current component
if (tok.value === "this") {
if (!done.has("this")) {
done.add("this");
this.addLine(`const this_${argId} = utils.getComponent(context);`);
}
tok.value = `this_${argId}`;
}
// Variables that should be looked up in the scope. isLocal is for arrow
// function arguments that should stay untouched (eg "ev => ev" should
// not become "const ev_1 = scope['ev']; ev_1 => ev_1")
if (tok.varName && !tok.isLocal) {
if (tok.varName) {
if (!done.has(tok.varName)) {
done.add(tok.varName);
this.addLine(`const ${tok.varName}_${argId} = ${tok.value};`);
+9 -68
View File
@@ -25,19 +25,18 @@
// Misc types, constants and helpers
//------------------------------------------------------------------------------
const RESERVED_WORDS =
"true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,eval,void,Math,RegExp,Array,Object,Date".split(
","
);
const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,eval,void,Math,RegExp,Array,Object,Date".split(
","
);
const WORD_REPLACEMENT = Object.assign(Object.create(null), {
const WORD_REPLACEMENT = {
and: "&&",
or: "||",
gt: ">",
gte: ">=",
lt: "<",
lte: "<=",
});
};
export interface QWebVar {
id: string; // foo
@@ -58,7 +57,6 @@ type TKind =
| "RIGHT_PAREN"
| "COMMA"
| "VALUE"
| "TEMPLATE_STRING"
| "SYMBOL"
| "OPERATOR"
| "COLON";
@@ -69,11 +67,9 @@ interface Token {
originalValue?: string;
size?: number;
varName?: string;
replace?: Function;
isLocal?: boolean;
}
const STATIC_TOKEN_MAP: { [key: string]: TKind } = Object.assign(Object.create(null), {
const STATIC_TOKEN_MAP: { [key: string]: TKind } = {
"{": "LEFT_BRACE",
"}": "RIGHT_BRACE",
"[": "LEFT_BRACKET",
@@ -82,7 +78,7 @@ const STATIC_TOKEN_MAP: { [key: string]: TKind } = Object.assign(Object.create(n
",": "COMMA",
"(": "LEFT_PAREN",
")": "RIGHT_PAREN",
});
};
// note that the space after typeof is relevant. It makes sure that the formatted
// expression has a space after typeof
@@ -93,7 +89,7 @@ type Tokenizer = (expr: string) => Token | false;
let tokenizeString: Tokenizer = function (expr) {
let s = expr[0];
let start = s;
if (s !== "'" && s !== '"' && s !== "`") {
if (s !== "'" && s !== '"') {
return false;
}
let i = 1;
@@ -115,17 +111,6 @@ let tokenizeString: Tokenizer = function (expr) {
throw new Error("Invalid expression");
}
s += start;
if (start === "`") {
return {
type: "TEMPLATE_STRING",
value: s,
replace(replacer) {
return s.replace(/\$\{(.*?)\}/g, (match, group) => {
return "${" + replacer(group) + "}";
});
},
};
}
return { type: "VALUE", value: s };
};
@@ -225,10 +210,6 @@ export function tokenize(expr: string): Token[] {
// Expression "evaluator"
//------------------------------------------------------------------------------
const isLeftSeparator = (token) => token && (token.type === "LEFT_BRACE" || token.type === "COMMA");
const isRightSeparator = (token) =>
token && (token.type === "RIGHT_BRACE" || token.type === "COMMA");
/**
* This is the main function exported by this file. This is the code that will
* process an expression (given as a string) and returns another expression with
@@ -255,42 +236,15 @@ const isRightSeparator = (token) =>
* the list of variables so it does not get replaced by a lookup in the context
*/
export function compileExprToArray(expr: string, scope: { [key: string]: QWebVar }): Token[] {
const localVars = new Set<string>();
scope = Object.create(scope);
const tokens = tokenize(expr);
let i = 0;
let stack = []; // to track last opening [ or {
while (i < tokens.length) {
for (let i = 0; i < tokens.length; i++) {
let token = tokens[i];
let prevToken = tokens[i - 1];
let nextToken = tokens[i + 1];
let groupType = stack[stack.length - 1];
switch (token.type) {
case "LEFT_BRACE":
case "LEFT_BRACKET":
stack.push(token.type);
break;
case "RIGHT_BRACE":
case "RIGHT_BRACKET":
stack.pop();
}
let isVar = token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value);
if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) {
if (prevToken) {
// normalize missing tokens: {a} should be equivalent to {a:a}
if (
groupType === "LEFT_BRACE" &&
isLeftSeparator(prevToken) &&
isRightSeparator(nextToken)
) {
tokens.splice(i + 1, 0, { type: "COLON", value: ":" }, { ...token });
nextToken = tokens[i + 1];
}
if (prevToken.type === "OPERATOR" && prevToken.value === ".") {
isVar = false;
} else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") {
@@ -300,9 +254,6 @@ export function compileExprToArray(expr: string, scope: { [key: string]: QWebVar
}
}
}
if (token.type === "TEMPLATE_STRING") {
token.value = token.replace((expr) => compileExpr(expr, scope));
}
if (nextToken && nextToken.type === "OPERATOR" && nextToken.value === "=>") {
if (token.type === "RIGHT_PAREN") {
let j = i - 1;
@@ -310,13 +261,11 @@ export function compileExprToArray(expr: string, scope: { [key: string]: QWebVar
if (tokens[j].type === "SYMBOL" && tokens[j].originalValue) {
tokens[j].value = tokens[j].originalValue!;
scope[tokens[j].value] = { id: tokens[j].value, expr: tokens[j].value };
localVars.add(tokens[j].value);
}
j--;
}
} else {
scope[token.value] = { id: token.value, expr: token.value };
localVars.add(token.value);
}
}
@@ -329,14 +278,6 @@ export function compileExprToArray(expr: string, scope: { [key: string]: QWebVar
token.value = `scope['${token.value}']`;
}
}
i++;
}
// Mark all variables that have been used locally.
// This assumes the expression has only one scope (incorrect but "good enough for now")
for (const token of tokens) {
if (token.type === "SYMBOL" && localVars.has(token.value)) {
token.isLocal = true;
}
}
return tokens;
}
+3 -18
View File
@@ -1,8 +1,5 @@
import { STATUS } from "../component/component";
import { VNode } from "../vdom/index";
import { INTERP_REGEXP } from "./compilation_context";
import { QWeb } from "./qweb";
import { browser } from "../browser";
/**
* Owl QWeb Extensions
@@ -76,10 +73,9 @@ export function makeHandlerCode(
// we need to capture every variable in it
putInCache = false;
code = ctx.captureExpression(value);
code = `const res = (() => { return ${code} })(); if (typeof res === 'function') { res(e) }`;
}
const modCode = mods.map((mod) => modcodes[mod]).join("");
let handler = `function (e) {if (context.__owl__.status === ${STATUS.DESTROYED}){return}${modCode}${code}}`;
let handler = `function (e) {if (!context.__owl__.isMounted){return}${modCode}${code}}`;
if (putInCache) {
const key = ctx.generateTemplateKey(event);
ctx.addLine(`extra.handlers[${key}] = extra.handlers[${key}] || ${handler};`);
@@ -199,14 +195,7 @@ function whenTransitionEnd(elm: HTMLElement, cb) {
const durations: Array<string> = (styles.transitionDuration || "").split(", ");
const timeout: number = getTimeout(delays, durations);
if (timeout > 0) {
const transitionEndCB = () => {
if (!elm.parentNode) return;
cb();
browser.clearTimeout(fallbackTimeout);
elm.removeEventListener("transitionend", transitionEndCB);
};
elm.addEventListener("transitionend", transitionEndCB, { once: true });
const fallbackTimeout = browser.setTimeout(transitionEndCB, timeout + 1);
elm.addEventListener("transitionend", cb, { once: true });
} else {
cb();
}
@@ -216,9 +205,6 @@ QWeb.addDirective({
name: "transition",
priority: 96,
atNodeCreation({ ctx, value, addNodeHook }) {
if (!QWeb.enableTransitions) {
return;
}
ctx.rootContext.shouldDefineUtils = true;
let name = value;
const hooks = {
@@ -239,9 +225,8 @@ QWeb.addDirective({
priority: 80,
atNodeEncounter({ ctx, value, node, qweb }): boolean {
const slotKey = ctx.generateID();
const valueExpr = value.match(INTERP_REGEXP) ? ctx.interpolate(value) : `'${value}'`;
ctx.addLine(
`const slot${slotKey} = this.constructor.slots[context.__owl__.slotId + '_' + ${valueExpr}];`
`const slot${slotKey} = this.constructor.slots[context.__owl__.slotId + '_' + '${value}'];`
);
ctx.addIf(`slot${slotKey}`);
let parentNode = `c${ctx.parentNode}`;
+29 -103
View File
@@ -66,11 +66,10 @@ interface QWebConfig {
// Const/global stuff/helpers
//------------------------------------------------------------------------------
export const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
const lineBreakRE = /[\r\n]/;
const whitespaceRE = /\s+/g;
const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
const NODE_HOOKS_PARAMS = {
create: "(_, n)",
@@ -80,7 +79,7 @@ const NODE_HOOKS_PARAMS = {
};
interface Utils {
toClassObj(expr: any): Object;
toObj(expr: any): Object;
shallowEqual(p1: Object, p2: Object): boolean;
[key: string]: any;
}
@@ -111,53 +110,20 @@ function vDomToString(vdom: VNode[]): string {
const UTILS: Utils = {
zero: Symbol("zero"),
toClassObj(expr) {
const result = {};
toObj(expr) {
if (typeof expr === "string") {
// we transform here a list of classes into an object:
// 'hey you' becomes {hey: true, you: true}
expr = expr.trim();
if (!expr) {
return {};
}
let words = expr.split(/\s+/);
let result = {};
for (let i = 0; i < words.length; i++) {
result[words[i]] = true;
}
return result;
}
// this is already an object, but we may need to split keys:
// {'a b': true, 'a c': false} should become {a: true, b: true, c: false}
for (let key in expr) {
const value = expr[key];
const words = key.split(/\s+/);
for (let word of words) {
result[word] = result[word] || value;
}
}
return result;
},
/**
* This method combines the current context with the variables defined in a
* scope for use in a slot.
*
* The implementation is kind of tricky because we want to preserve the
* prototype chain structure of the cloned result. So we need to traverse the
* prototype chain, cloning each level respectively.
*/
combine(context, scope) {
let clone = context;
const scopeStack = [];
while (!isComponent(scope)) {
scopeStack.push(scope);
scope = scope.__proto__;
}
while (scopeStack.length) {
let scope = scopeStack.pop();
clone = Object.create(clone);
Object.assign(clone, scope);
}
return clone;
return expr;
},
shallowEqual,
addNameSpace(vnode) {
@@ -236,7 +202,6 @@ export class QWeb extends EventBus {
att: 1,
attf: 1,
translation: 1,
tag: 1,
};
static DIRECTIVES: Directive[] = [];
@@ -247,7 +212,6 @@ export class QWeb extends EventBus {
h = h;
// dev mode enables better error messages or more costly validations
static dev: boolean = false;
static enableTransitions: boolean = true;
// slots contains sub templates defined with t-set inside t-component nodes, and
// are meant to be used by the t-slot directive.
@@ -329,9 +293,6 @@ export class QWeb extends EventBus {
* template, with the name given by the t-name attribute.
*/
addTemplates(xmlstr: string | Document) {
if (!xmlstr) {
return;
}
const doc = typeof xmlstr === "string" ? parseXML(xmlstr) : xmlstr;
const templates = doc.getElementsByTagName("templates")[0];
if (!templates) {
@@ -351,7 +312,7 @@ export class QWeb extends EventBus {
const template = {
elem,
fn: function (this: QWeb, context, extra) {
const compiledFunction = this._compile(name);
const compiledFunction = this._compile(name, elem);
template.fn = compiledFunction;
return compiledFunction.call(this, context, extra);
},
@@ -456,34 +417,30 @@ export class QWeb extends EventBus {
_compile(
name: string,
options: {
elem?: Element;
hasParent?: boolean;
defineKey?: boolean;
} = {}
elem: Element,
parentContext?: CompilationContext,
defineKey?: boolean
): CompiledTemplate {
const elem = options.elem || this.templates[name].elem;
const isDebug = elem.attributes.hasOwnProperty("t-debug");
const ctx = new CompilationContext(name);
if (elem.tagName !== "t") {
ctx.shouldDefineResult = false;
}
if (options.hasParent) {
ctx.variables = Object.create(null);
ctx.parentNode = ctx.generateID();
if (parentContext) {
ctx.variables = Object.create(parentContext.variables);
ctx.parentNode = parentContext.parentNode || ctx.generateID();
ctx.allowMultipleRoots = true;
ctx.shouldDefineParent = true;
ctx.hasParentWidget = true;
ctx.shouldDefineResult = false;
ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`);
if (options.defineKey) {
if (defineKey) {
ctx.addLine(`let key0 = extra.key || "";`);
ctx.hasKey0 = true;
}
}
this._compileNode(elem, ctx);
if (!options.hasParent) {
if (!parentContext) {
if (ctx.shouldDefineResult) {
ctx.addLine(`return result;`);
} else {
@@ -535,8 +492,7 @@ export class QWeb extends EventBus {
}
if (this.translateFn) {
if ((node.parentNode as any).getAttribute("t-translation") !== "off") {
const match = translationRE.exec(text);
text = match[1] + this.translateFn(match[2]) + match[3];
text = this.translateFn(text);
}
}
if (ctx.parentNode) {
@@ -560,11 +516,7 @@ export class QWeb extends EventBus {
}
if (node.tagName !== "t" && node.hasAttribute("t-call")) {
const tCallNode = document.implementation.createDocument(
"http://www.w3.org/1999/xhtml",
"t",
null
).documentElement;
const tCallNode = document.createElement("t");
tCallNode.setAttribute("t-call", node.getAttribute("t-call")!);
node.removeAttribute("t-call");
node.prepend(tCallNode);
@@ -600,11 +552,7 @@ export class QWeb extends EventBus {
throw new Error(`Unknown QWeb directive: '${attrName}'`);
}
if (node.tagName !== "t" && (attrName === "t-esc" || attrName === "t-raw")) {
const tNode = document.implementation.createDocument(
"http://www.w3.org/1999/xhtml",
"t",
null
).documentElement;
const tNode = document.createElement("t");
tNode.setAttribute(attrName, node.getAttribute(attrName)!);
for (let child of Array.from(node.childNodes)) {
tNode.appendChild(child);
@@ -660,22 +608,14 @@ export class QWeb extends EventBus {
}
}
if (node.nodeName !== "t" || node.hasAttribute("t-tag")) {
if (node.nodeName !== "t") {
let nodeID = this._compileGenericNode(node, ctx, withHandlers);
ctx = ctx.withParent(nodeID);
let nodeHooks = {};
let addNodeHook = function (hook, handler) {
nodeHooks[hook] = nodeHooks[hook] || [];
nodeHooks[hook].push(handler);
};
if (node.tagName === "select" && node.hasAttribute("t-att-value")) {
const value = node.getAttribute("t-att-value");
let exprId = ctx.generateID();
ctx.addLine(`let expr${exprId} = ${ctx.formatExpression(value)};`);
let expr = `expr${exprId}`;
node.setAttribute("t-att-value", expr);
addNodeHook("create", `n.elm.value=${expr};`);
}
let nodeID = this._compileGenericNode(node, ctx, withHandlers);
ctx = ctx.withParent(nodeID);
for (let { directive, value, fullName } of validDirectives) {
if (directive.atNodeCreation) {
@@ -757,18 +697,16 @@ export class QWeb extends EventBus {
isProp = key === "selected" || key === "disabled";
break;
case "textarea":
isProp = key === "readonly" || key === "disabled" || key === "value";
break;
case "select":
isProp = key === "disabled" || key === "value";
isProp = key === "readonly" || key === "disabled";
break;
case "button":
case "select":
case "optgroup":
isProp = key === "disabled";
break;
}
if (isProp) {
props.push(`${key}: ${val}`);
props.push(`${key}: _${val}`);
}
}
let classObj = "";
@@ -804,7 +742,7 @@ export class QWeb extends EventBus {
name = '"' + name + '"';
}
attrs.push(`${name}: _${attID}`);
handleProperties(name, `_${attID}`);
handleProperties(name, attID);
}
}
@@ -816,7 +754,7 @@ export class QWeb extends EventBus {
if (attName === "class") {
ctx.rootContext.shouldDefineUtils = true;
formattedValue = `utils.toClassObj(${formattedValue})`;
formattedValue = `utils.toObj(${formattedValue})`;
if (classObj) {
ctx.addLine(`Object.assign(${classObj}, ${formattedValue})`);
} else {
@@ -839,14 +777,9 @@ export class QWeb extends EventBus {
const attrIndex = attrs.findIndex((att) => att.startsWith(attName + ":"));
attrs.splice(attrIndex, 1);
}
if (node.nodeName === "select" && attName === "value") {
attrs.push(`${attName}: ${v}`);
handleProperties(attName, v);
} else {
ctx.addLine(`let _${attID} = ${formattedValue};`);
attrs.push(`${attName}: _${attID}`);
handleProperties(attName, "_" + attID);
}
ctx.addLine(`let _${attID} = ${formattedValue};`);
attrs.push(`${attName}: _${attID}`);
handleProperties(attName, attID);
}
}
@@ -902,14 +835,7 @@ export class QWeb extends EventBus {
ctx.addLine(`}`);
ctx.closeIf();
}
let nodeName = `'${node.nodeName}'`;
if ((<Element>node).hasAttribute("t-tag")) {
const tagExpr = (<Element>node).getAttribute("t-tag");
(<Element>node).removeAttribute("t-tag");
nodeName = `tag${ctx.generateID()}`;
ctx.addLine(`let ${nodeName} = ${ctx.formatExpression(tagExpr)};`);
}
ctx.addLine(`let vn${nodeID} = h(${nodeName}, p${nodeID}, c${nodeID});`);
ctx.addLine(`let vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`);
if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`);
} else if (ctx.loopNumber || ctx.hasKey0) {
+32 -43
View File
@@ -11,7 +11,6 @@ type NavigationGuard = (info: {
export interface Route {
name: string;
path: string;
extractionRegExp: RegExp;
component?: any;
redirect?: Destination;
params: string[];
@@ -55,7 +54,6 @@ export interface EnvWithRouter extends Env {
}
const paramRegexp = /\{\{(.*?)\}\}/;
const globalParamRegexp = new RegExp(paramRegexp.source, "g");
export class Router {
currentRoute: Route | null = null;
@@ -89,7 +87,6 @@ export class Router {
this.validateDestination(partialRoute.redirect);
}
partialRoute.params = partialRoute.path ? findParams(partialRoute.path) : [];
partialRoute.extractionRegExp = makeExtractionRegExp(partialRoute.path);
this.routes[partialRoute.name] = partialRoute as Route;
this.routeIds.push(partialRoute.name);
}
@@ -125,10 +122,7 @@ export class Router {
const initialParams = this.currentParams;
const result = await this.matchAndApplyRules(path);
if (result.type === "match") {
let finalPath = this.routeToPath(result.route, result.params);
if (path.indexOf("?") > -1) {
finalPath += "?" + path.split("?")[1];
}
const finalPath = this.routeToPath(result.route, result.params);
const isPopStateEvent = ev && ev instanceof PopStateEvent;
if (!isPopStateEvent) {
this.setUrlFromPath(finalPath);
@@ -176,14 +170,19 @@ export class Router {
}
private routeToPath(route: Route, params: RouteParams): string {
const path = route.path;
const parts = path.split("/");
const l = parts.length;
for (let i = 0; i < l; i++) {
const part = parts[i];
const match = part.match(paramRegexp);
if (match) {
const key = match[1].split(".")[0];
parts[i] = <string>params[key];
}
}
const prefix = this.mode === "hash" ? "#" : "";
return (
prefix +
route.path.replace(globalParamRegexp, (match, param) => {
const [key] = param.split(".");
return <string>params[key];
})
);
return prefix + parts.join("/");
}
private currentPath(): string {
@@ -242,53 +241,43 @@ export class Router {
if (route.path === "*") {
return {};
}
if (path.indexOf("?") > -1) {
path = path.split("?")[0];
}
if (path.startsWith("#")) {
path = path.slice(1);
}
const paramsMatch = path.match(route.extractionRegExp);
if (!paramsMatch) {
const descrParts = route.path.split("/");
const targetParts = path.split("/");
const l = descrParts.length;
if (l !== targetParts.length) {
return false;
}
const result = {};
route.params.forEach((param, index) => {
const [key, suffix] = param.split(".");
const paramValue = paramsMatch[index + 1];
if (suffix === "number") {
return (result[key] = parseInt(paramValue, 10));
for (let i = 0; i < l; i++) {
const descr = descrParts[i];
let target: string | number = targetParts[i];
const match = descr.match(paramRegexp);
if (match) {
const [key, suffix] = match[1].split(".");
if (suffix === "number") {
target = parseInt(target, 10);
}
result[key] = target;
} else if (descr !== target) {
return false;
}
return (result[key] = paramValue);
});
}
return result;
}
}
function findParams(str: string): string[] {
const globalParamRegexp = /\{\{(.*?)\}\}/g;
const result: string[] = [];
let m;
do {
m = globalParamRegexp.exec(str);
if (m) {
result.push(m[1]);
result.push(m[1].split(".")[0]);
}
} while (m);
return result;
}
function escapeRegExp(str: string) {
return str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
}
function makeExtractionRegExp(path: string) {
// replace param strings with capture groups so that we can build a regex to match over the path
const extractionString = path
.split(paramRegexp)
.map((part, index) => {
return index % 2 ? "(.*)" : escapeRegExp(part);
})
.join("");
// Example: /home/{{param1}}/{{param2}} => ^\/home\/(.*)\/(.*)$
return new RegExp(`^${extractionString}$`);
}
+5 -15
View File
@@ -1,4 +1,5 @@
import { Component, Env } from "./component/component";
import { Component } from "./component/component";
import { Env } from "./component/component";
import { Context, useContextWithCB } from "./context";
import { onWillUpdateProps } from "./hooks";
@@ -75,11 +76,6 @@ export class Store extends Context {
);
return result;
}
__notifyComponents(): Promise<void> {
this.trigger("before-update");
return super.__notifyComponents();
}
}
interface SelectorOptions {
@@ -110,16 +106,13 @@ export function useStore(selector, options: SelectorOptions = {}): any {
const newRevNumber = hashFn(result);
if ((newRevNumber > 0 && revNumber !== newRevNumber) || !isEqual(oldResult, result)) {
revNumber = newRevNumber;
if (options.onUpdate) {
options.onUpdate(result);
}
return true;
}
return false;
}
if (options.onUpdate) {
store.on("before-update", component, () => {
const newValue = selector(store!.state, component.props!);
options.onUpdate(newValue);
});
}
store.updateFunctions[componentId].push(function (): boolean {
return selectCompareUpdate(store!.state, component.props);
});
@@ -140,9 +133,6 @@ export function useStore(selector, options: SelectorOptions = {}): any {
const __destroy = component.__destroy;
component.__destroy = (parent) => {
delete store.updateFunctions[componentId];
if (options.onUpdate) {
store.off("before-update", component);
}
__destroy.call(component, parent);
};
-3
View File
@@ -13,9 +13,6 @@ export function htmlToVDOM(html: string): VNode[] {
function htmlToVNode(node: ChildNode): VNode {
if (!(node instanceof Element)) {
if (node instanceof Comment) {
return h("!", node.textContent);
}
return { text: node.textContent! } as VNode;
}
const attrs = {};
+1 -2
View File
@@ -231,8 +231,7 @@ function updateClass(oldVnode: VNode, vnode: VNode): void {
elm = vnode.elm as Element;
for (name in oldClass) {
if (name && !klass[name] && !Object.prototype.hasOwnProperty.call(klass, name)) {
// was `true` and now not provided
if (name && !klass[name]) {
elm.classList.remove(name);
}
}
+3 -3
View File
@@ -24,7 +24,7 @@ exports[`animations t-transition combined with component 1`] = `
c1.push(pvnode);
} else {
let componentKey2 = \`Child\`;
let W2 = scope['Child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2];
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
const __patch2 = w2.__patch;
@@ -69,7 +69,7 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
c1.push(pvnode);
} else {
let componentKey2 = \`Child\`;
let W2 = scope['Child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2];
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
const __patch2 = w2.__patch;
@@ -115,7 +115,7 @@ exports[`animations t-transition combined with t-component, remove and re-add be
c1.push(pvnode);
} else {
let componentKey2 = \`Child\`;
let W2 = scope['Child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2];
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
const __patch2 = w2.__patch;
+8 -8
View File
@@ -1,15 +1,15 @@
import { Component, Env } from "../src/component/component";
import { useRef, useState } from "../src/hooks";
import { QWeb } from "../src/qweb/index";
import { useState, useRef } from "../src/hooks";
import { xml } from "../src/tags";
import {
makeDeferred,
makeTestEnv,
makeTestFixture,
nextFrame,
makeTestEnv,
patchNextFrame,
renderToDOM,
unpatchNextFrame,
nextTick,
} from "./helpers";
//------------------------------------------------------------------------------
@@ -420,29 +420,29 @@ describe("animations", () => {
widget.state.flag = true;
await nextFrame();
await nextTick();
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend"));
expect(fixture.innerHTML).toBe('<div><span class="">blue</span></div>');
expect(QWeb.utils.transitionInsert).toBeCalledTimes(1);
widget.state.flag = false;
await nextFrame();
await nextTick();
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="__3__">blue</span></div>'
);
expect(QWeb.utils.transitionInsert).toBeCalledTimes(1);
widget.state.flag = true;
await nextFrame();
await nextTick();
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-enter-active chimay-enter-to" data-owl-key="__3__">blue</span></div>'
);
expect(QWeb.utils.transitionInsert).toBeCalledTimes(2);
widget.state.flag = false;
await nextFrame();
await nextTick();
widget.state.flag = true;
await nextFrame();
await nextTick();
expect(QWeb.utils.transitionInsert).toBeCalledTimes(3);
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend"));
@@ -20,12 +20,12 @@ exports[`class and style attributes with t-component dynamic t-att-style is prop
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, undefined).then(()=>{if (w2.__owl__.status === 5) {return};w2.el.style=_4;});;
w2.__updateProps(props2, extra.fiber, undefined).then(()=>{if (w2.__owl__.isDestroyed) {return};w2.el.style=_4;});;
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`child\`;
let W2 = scope['child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2];
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
@@ -67,7 +67,7 @@ exports[`class and style attributes with t-component t-att-class is properly add
c1.push(pvnode);
} else {
let componentKey2 = \`Child\`;
let W2 = scope['Child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2];
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
@@ -90,7 +90,7 @@ exports[`class and style attributes with t-component t-att-class is properly add
let scope = Object.create(context);
let h = this.h;
let _7 = {'c':true};
Object.assign(_7, utils.toClassObj({d:scope['state'].d}))
Object.assign(_7, utils.toObj({d:scope['state'].d}))
let c8 = [], p8 = {key:8,class:_7};
let vn8 = h('span', p8, c8);
return vn8;
@@ -112,7 +112,7 @@ exports[`class and style attributes with t-component t-att-class is properly add
// Component 'Child'
const ref4 = \`child\`;
let _5 = {'a':true};
Object.assign(_5, utils.toClassObj(scope['state'].b?'b':''))
Object.assign(_5, utils.toObj(scope['state'].b?'b':''))
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = {};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
@@ -125,7 +125,7 @@ exports[`class and style attributes with t-component t-att-class is properly add
c1.push(pvnode);
} else {
let componentKey2 = \`Child\`;
let W2 = scope['Child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2];
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
@@ -148,7 +148,7 @@ exports[`class and style attributes with t-component t-att-class is properly add
let scope = Object.create(context);
let h = this.h;
let _7 = {'c':true};
Object.assign(_7, utils.toClassObj(scope['state'].d?'d':''))
Object.assign(_7, utils.toObj(scope['state'].d?'d':''))
let c8 = [], p8 = {key:8,class:_7};
let vn8 = h('span', p8, c8);
return vn8;
File diff suppressed because it is too large Load Diff
@@ -24,7 +24,7 @@ exports[`props validation props are validated in dev mode (code snapshot) 1`] =
c1.push(pvnode);
} else {
let componentKey2 = \`Child\`;
let W2 = scope['Child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2];
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
+136 -301
View File
@@ -19,17 +19,17 @@ exports[`t-slot directive can define and call slots 1`] = `
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, utils.combine(context, scope));
w2.__updateProps(props2, extra.fiber, Object.assign(Object.create(context), scope));
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`Dialog\`;
let W2 = scope['Dialog'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2];
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Dialog'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
w2.__owl__.slotId = 1;
let fiber = w2.__prepare(extra.fiber, utils.combine(context, scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let fiber = w2.__prepare(extra.fiber, Object.assign(Object.create(context), scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
@@ -44,23 +44,23 @@ exports[`t-slot directive can define and call slots 2`] = `
) {
// Template name: \\"Dialog\\"
let h = this.h;
let c8 = [], p8 = {key:8};
let vn8 = h('div', p8, c8);
let c6 = [], p6 = {key:6};
let vn6 = h('div', p6, c6);
let c7 = [], p7 = {key:7};
let vn7 = h('div', p7, c7);
c6.push(vn7);
const slot8 = this.constructor.slots[context.__owl__.slotId + '_' + 'header'];
if (slot8) {
slot8.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c7, parent: extra.parent || context}));
}
let c9 = [], p9 = {key:9};
let vn9 = h('div', p9, c9);
c8.push(vn9);
const slot10 = this.constructor.slots[context.__owl__.slotId + '_' + 'header'];
c6.push(vn9);
const slot10 = this.constructor.slots[context.__owl__.slotId + '_' + 'footer'];
if (slot10) {
slot10.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c9, parent: extra.parent || context}));
}
let c11 = [], p11 = {key:11};
let vn11 = h('div', p11, c11);
c8.push(vn11);
const slot12 = this.constructor.slots[context.__owl__.slotId + '_' + 'footer'];
if (slot12) {
slot12.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c11, parent: extra.parent || context}));
}
return vn8;
return vn6;
}"
`;
@@ -68,13 +68,12 @@ exports[`t-slot directive can define and call slots 3`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_header_template\\"
let parent = extra.parent;
let h = this.h;
let c4 = extra.parentNode;
let c5 = [], p5 = {key:5};
let vn5 = h('span', p5, c5);
c4.push(vn5);
c5.push({text: \`header\`});
let c1 = extra.parentNode;
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
c1.push(vn4);
c4.push({text: \`header\`});
}"
`;
@@ -82,13 +81,12 @@ exports[`t-slot directive can define and call slots 4`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_footer_template\\"
let parent = extra.parent;
let h = this.h;
let c6 = extra.parentNode;
let c7 = [], p7 = {key:7};
let vn7 = h('span', p7, c7);
c6.push(vn7);
c7.push({text: \`footer\`});
let c1 = extra.parentNode;
let c5 = [], p5 = {key:5};
let vn5 = h('span', p5, c5);
c1.push(vn5);
c5.push({text: \`footer\`});
}"
`;
@@ -111,17 +109,17 @@ exports[`t-slot directive can define and call slots using old t-set keyword 1`]
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, utils.combine(context, scope));
w2.__updateProps(props2, extra.fiber, Object.assign(Object.create(context), scope));
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`Dialog\`;
let W2 = scope['Dialog'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2];
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Dialog'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
w2.__owl__.slotId = 1;
let fiber = w2.__prepare(extra.fiber, utils.combine(context, scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let fiber = w2.__prepare(extra.fiber, Object.assign(Object.create(context), scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
@@ -136,23 +134,23 @@ exports[`t-slot directive can define and call slots using old t-set keyword 2`]
) {
// Template name: \\"__template__1\\"
let h = this.h;
let c8 = [], p8 = {key:8};
let vn8 = h('div', p8, c8);
let c6 = [], p6 = {key:6};
let vn6 = h('div', p6, c6);
let c7 = [], p7 = {key:7};
let vn7 = h('div', p7, c7);
c6.push(vn7);
const slot8 = this.constructor.slots[context.__owl__.slotId + '_' + 'header'];
if (slot8) {
slot8.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c7, parent: extra.parent || context}));
}
let c9 = [], p9 = {key:9};
let vn9 = h('div', p9, c9);
c8.push(vn9);
const slot10 = this.constructor.slots[context.__owl__.slotId + '_' + 'header'];
c6.push(vn9);
const slot10 = this.constructor.slots[context.__owl__.slotId + '_' + 'footer'];
if (slot10) {
slot10.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c9, parent: extra.parent || context}));
}
let c11 = [], p11 = {key:11};
let vn11 = h('div', p11, c11);
c8.push(vn11);
const slot12 = this.constructor.slots[context.__owl__.slotId + '_' + 'footer'];
if (slot12) {
slot12.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c11, parent: extra.parent || context}));
}
return vn8;
return vn6;
}"
`;
@@ -160,13 +158,12 @@ exports[`t-slot directive can define and call slots using old t-set keyword 3`]
"function anonymous(context, extra
) {
// Template name: \\"slot_header_template\\"
let parent = extra.parent;
let h = this.h;
let c4 = extra.parentNode;
let c5 = [], p5 = {key:5};
let vn5 = h('span', p5, c5);
c4.push(vn5);
c5.push({text: \`header\`});
let c1 = extra.parentNode;
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
c1.push(vn4);
c4.push({text: \`header\`});
}"
`;
@@ -174,13 +171,12 @@ exports[`t-slot directive can define and call slots using old t-set keyword 4`]
"function anonymous(context, extra
) {
// Template name: \\"slot_footer_template\\"
let parent = extra.parent;
let h = this.h;
let c6 = extra.parentNode;
let c7 = [], p7 = {key:7};
let vn7 = h('span', p7, c7);
c6.push(vn7);
c7.push({text: \`footer\`});
let c1 = extra.parentNode;
let c5 = [], p5 = {key:5};
let vn5 = h('span', p5, c5);
c1.push(vn5);
c5.push({text: \`footer\`});
}"
`;
@@ -188,13 +184,12 @@ exports[`t-slot directive content is the default slot 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_default_template\\"
let parent = extra.parent;
let h = this.h;
let c4 = extra.parentNode;
let c5 = [], p5 = {key:5};
let vn5 = h('span', p5, c5);
c4.push(vn5);
c5.push({text: \`sts rocks\`});
let c1 = extra.parentNode;
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
c1.push(vn4);
c4.push({text: \`sts rocks\`});
}"
`;
@@ -215,72 +210,13 @@ exports[`t-slot directive dafault slots can define a default content 1`] = `
}"
`;
exports[`t-slot directive default slot next to named slot, with default content 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__2\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
// Component 'Dialog'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = {};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, utils.combine(context, scope));
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`Dialog\`;
let W2 = scope['Dialog'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
w2.__owl__.slotId = 1;
let fiber = w2.__prepare(extra.fiber, utils.combine(context, scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
}
w2.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
exports[`t-slot directive default slot work with text nodes 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_default_template\\"
let parent = extra.parent;
let h = this.h;
let c4 = extra.parentNode;
c4.push({text: \`sts rocks\`});
}"
`;
exports[`t-slot directive dynamic t-slot call 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__1\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let c9 = [], p9 = {key:9,on:{}};
let vn9 = h('button', p9, c9);
extra.handlers['click__10__'] = extra.handlers['click__10__'] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['toggle'](e);};
p9.on['click'] = extra.handlers['click__10__'];
const slot11 = this.constructor.slots[context.__owl__.slotId + '_' + (scope['current'].slot)];
if (slot11) {
slot11.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c9, parent: extra.parent || context}));
}
return vn9;
let c1 = extra.parentNode;
c1.push({text: \`sts rocks\`});
}"
`;
@@ -288,17 +224,16 @@ exports[`t-slot directive multiple roots are allowed in a default slot 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_default_template\\"
let parent = extra.parent;
let h = this.h;
let c4 = extra.parentNode;
let c1 = extra.parentNode;
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
c1.push(vn4);
c4.push({text: \`sts\`});
let c5 = [], p5 = {key:5};
let vn5 = h('span', p5, c5);
c4.push(vn5);
c5.push({text: \`sts\`});
let c6 = [], p6 = {key:6};
let vn6 = h('span', p6, c6);
c4.push(vn6);
c6.push({text: \`rocks\`});
c1.push(vn5);
c5.push({text: \`rocks\`});
}"
`;
@@ -306,17 +241,16 @@ exports[`t-slot directive multiple roots are allowed in a named slot 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_content_template\\"
let parent = extra.parent;
let h = this.h;
let c4 = extra.parentNode;
let c1 = extra.parentNode;
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
c1.push(vn4);
c4.push({text: \`sts\`});
let c5 = [], p5 = {key:5};
let vn5 = h('span', p5, c5);
c4.push(vn5);
c5.push({text: \`sts\`});
let c6 = [], p6 = {key:6};
let vn6 = h('span', p6, c6);
c4.push(vn6);
c6.push({text: \`rocks\`});
c1.push(vn5);
c5.push({text: \`rocks\`});
}"
`;
@@ -342,25 +276,24 @@ exports[`t-slot directive refs are properly bound in slots 1`] = `
) {
// Template name: \\"slot_footer_template\\"
let utils = this.constructor.utils;
let parent = extra.parent;
context.__owl__.refs = context.__owl__.refs || {};
let h = this.h;
let c8 = extra.parentNode;
let c9 = [], p9 = {key:9,on:{}};
let vn9 = h('button', p9, c9);
c8.push(vn9);
extra.handlers['click__10__'] = extra.handlers['click__10__'] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['doSomething'](e);};
p9.on['click'] = extra.handlers['click__10__'];
const ref11 = \`myButton\`;
p9.hook = {
let c1 = extra.parentNode;
let c8 = [], p8 = {key:8,on:{}};
let vn8 = h('button', p8, c8);
c1.push(vn8);
extra.handlers['click__9__'] = extra.handlers['click__9__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['doSomething'](e);};
p8.on['click'] = extra.handlers['click__9__'];
const ref10 = \`myButton\`;
p8.hook = {
create: (_, n) => {
context.__owl__.refs[ref11] = n.elm;
context.__owl__.refs[ref10] = n.elm;
},
destroy: () => {
delete context.__owl__.refs[ref11];
delete context.__owl__.refs[ref10];
},
};
c9.push({text: \`do something\`});
c8.push({text: \`do something\`});
}"
`;
@@ -369,15 +302,14 @@ exports[`t-slot directive slots are rendered with proper context 1`] = `
) {
// Template name: \\"slot_footer_template\\"
let utils = this.constructor.utils;
let parent = extra.parent;
let h = this.h;
let c8 = extra.parentNode;
let c9 = [], p9 = {key:9,on:{}};
let vn9 = h('button', p9, c9);
c8.push(vn9);
extra.handlers['click__10__'] = extra.handlers['click__10__'] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['doSomething'](e);};
p9.on['click'] = extra.handlers['click__10__'];
c9.push({text: \`do something\`});
let c1 = extra.parentNode;
let c8 = [], p8 = {key:8,on:{}};
let vn8 = h('button', p8, c8);
c1.push(vn8);
extra.handlers['click__9__'] = extra.handlers['click__9__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['doSomething'](e);};
p8.on['click'] = extra.handlers['click__9__'];
c8.push({text: \`do something\`});
}"
`;
@@ -387,14 +319,14 @@ exports[`t-slot directive slots are rendered with proper context, part 2 1`] = `
// Template name: \\"Link\\"
let scope = Object.create(context);
let h = this.h;
let _12 = scope['props'].to;
let c13 = [], p13 = {key:13,attrs:{href: _12}};
let vn13 = h('a', p13, c13);
const slot14 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot14) {
slot14.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c13, parent: extra.parent || context}));
let _11 = scope['props'].to;
let c12 = [], p12 = {key:12,attrs:{href: _11}};
let vn12 = h('a', p12, c12);
const slot13 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot13) {
slot13.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c12, parent: extra.parent || context}));
}
return vn13;
return vn12;
}"
`;
@@ -414,8 +346,7 @@ exports[`t-slot directive slots are rendered with proper context, part 2 2`] = `
c1.push(vn2);
let _3 = scope['state'].users;
if (!_3) { throw new Error('QWeb error: Invalid loop expression')}
let _4 = _3;
let _5 = _3;
let _4 = _5 = _3;
if (!(_3 instanceof Array)) {
_4 = Object.keys(_3);
_5 = Object.values(_3);
@@ -442,17 +373,17 @@ exports[`t-slot directive slots are rendered with proper context, part 2 2`] = `
w8 = false;
}
if (w8) {
w8.__updateProps(props8, extra.fiber, utils.combine(context, scope));
w8.__updateProps(props8, extra.fiber, Object.assign(Object.create(context), scope));
let pvnode = w8.__owl__.pvnode;
c7.push(pvnode);
} else {
let componentKey8 = \`Link\`;
let W8 = scope['Link'] || context.constructor.components[componentKey8] || QWeb.components[componentKey8];
let W8 = context.constructor.components[componentKey8] || QWeb.components[componentKey8]|| scope['Link'];
if (!W8) {throw new Error('Cannot find the definition of component \\"' + componentKey8 + '\\"')}
w8 = new W8(parent, props8);
parent.__owl__.cmap[k9] = w8.__owl__.id;
w8.__owl__.slotId = 1;
let fiber = w8.__prepare(extra.fiber, utils.combine(context, scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let fiber = w8.__prepare(extra.fiber, Object.assign(Object.create(context), scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k9, hook: {remove() {},destroy(vn) {w8.destroy();}}});
c7.push(pvnode);
w8.__owl__.pvnode = pvnode;
@@ -468,14 +399,13 @@ exports[`t-slot directive slots are rendered with proper context, part 2 3`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_default_template\\"
let parent = extra.parent;
let scope = Object.create(context);
let h = this.h;
let c10 = extra.parentNode;
c10.push({text: \`User \`});
let _11 = scope['user'].name;
if (_11 != null) {
c10.push({text: _11});
let c7 = extra.parentNode;
c7.push({text: \`User \`});
let _10 = scope['user'].name;
if (_10 != null) {
c7.push({text: _10});
}
}"
`;
@@ -486,14 +416,14 @@ exports[`t-slot directive slots are rendered with proper context, part 3 1`] = `
// Template name: \\"Link\\"
let scope = Object.create(context);
let h = this.h;
let _12 = scope['props'].to;
let c13 = [], p13 = {key:13,attrs:{href: _12}};
let vn13 = h('a', p13, c13);
const slot14 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot14) {
slot14.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c13, parent: extra.parent || context}));
let _10 = scope['props'].to;
let c11 = [], p11 = {key:11,attrs:{href: _10}};
let vn11 = h('a', p11, c11);
const slot12 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot12) {
slot12.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c11, parent: extra.parent || context}));
}
return vn13;
return vn11;
}"
`;
@@ -513,8 +443,7 @@ exports[`t-slot directive slots are rendered with proper context, part 3 2`] = `
c1.push(vn2);
let _3 = scope['state'].users;
if (!_3) { throw new Error('QWeb error: Invalid loop expression')}
let _4 = _3;
let _5 = _3;
let _4 = _5 = _3;
if (!(_3 instanceof Array)) {
_4 = Object.keys(_3);
_5 = Object.values(_3);
@@ -542,17 +471,17 @@ exports[`t-slot directive slots are rendered with proper context, part 3 2`] = `
w8 = false;
}
if (w8) {
w8.__updateProps(props8, extra.fiber, utils.combine(context, scope));
w8.__updateProps(props8, extra.fiber, Object.assign(Object.create(context), scope));
let pvnode = w8.__owl__.pvnode;
c7.push(pvnode);
} else {
let componentKey8 = \`Link\`;
let W8 = scope['Link'] || context.constructor.components[componentKey8] || QWeb.components[componentKey8];
let W8 = context.constructor.components[componentKey8] || QWeb.components[componentKey8]|| scope['Link'];
if (!W8) {throw new Error('Cannot find the definition of component \\"' + componentKey8 + '\\"')}
w8 = new W8(parent, props8);
parent.__owl__.cmap[k9] = w8.__owl__.id;
w8.__owl__.slotId = 1;
let fiber = w8.__prepare(extra.fiber, utils.combine(context, scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let fiber = w8.__prepare(extra.fiber, Object.assign(Object.create(context), scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k9, hook: {remove() {},destroy(vn) {w8.destroy();}}});
c7.push(pvnode);
w8.__owl__.pvnode = pvnode;
@@ -568,13 +497,11 @@ exports[`t-slot directive slots are rendered with proper context, part 3 3`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_default_template\\"
let parent = extra.parent;
let scope = Object.create(context);
let h = this.h;
let c10 = extra.parentNode;
let _11 = scope['userdescr'];
if (_11 != null) {
c10.push({text: _11});
let c7 = extra.parentNode;
if (scope.userdescr != null) {
c7.push({text: scope.userdescr});
}
}"
`;
@@ -599,17 +526,17 @@ exports[`t-slot directive slots are rendered with proper context, part 4 1`] = `
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, utils.combine(context, scope));
w2.__updateProps(props2, extra.fiber, Object.assign(Object.create(context), scope));
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`Link\`;
let W2 = scope['Link'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2];
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Link'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
w2.__owl__.slotId = 1;
let fiber = w2.__prepare(extra.fiber, utils.combine(context, scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let fiber = w2.__prepare(extra.fiber, Object.assign(Object.create(context), scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
@@ -623,119 +550,27 @@ exports[`t-slot directive slots are rendered with proper context, part 4 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_default_template\\"
let parent = extra.parent;
let scope = Object.create(context);
let h = this.h;
let c4 = extra.parentNode;
let _5 = scope['userdescr'];
if (_5 != null) {
c4.push({text: _5});
let c1 = extra.parentNode;
if (scope.userdescr != null) {
c1.push({text: scope.userdescr});
}
}"
`;
exports[`t-slot directive slots in t-foreach in t-foreach 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__2\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
let _2 = scope['tree'];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _2;
let _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
}
let _length3 = _3.length;
let _origScope5 = scope;
scope = Object.create(scope);
for (let i1 = 0; i1 < _length3; i1++) {
scope.node1_first = i1 === 0
scope.node1_last = i1 === _length3 - 1
scope.node1_index = i1
scope.node1 = _3[i1]
scope.node1_value = _4[i1]
let key1 = scope['node1'].key;
let c6 = [], p6 = {key:\`\${key1}_6\`};
let vn6 = h('div', p6, c6);
c1.push(vn6);
let _7 = scope['node1'].value;
if (_7 != null) {
c6.push({text: _7});
}
let c8 = [], p8 = {key:\`\${key1}_8\`};
let vn8 = h('ul', p8, c8);
c1.push(vn8);
let _9 = scope['node1'].nodes;
if (!_9) { throw new Error('QWeb error: Invalid loop expression')}
let _10 = _9;
let _11 = _9;
if (!(_9 instanceof Array)) {
_10 = Object.keys(_9);
_11 = Object.values(_9);
}
let _length10 = _10.length;
let _origScope12 = scope;
scope = Object.create(scope);
for (let i2 = 0; i2 < _length10; i2++) {
scope.node2_first = i2 === 0
scope.node2_last = i2 === _length10 - 1
scope.node2_index = i2
scope.node2 = _10[i2]
scope.node2_value = _11[i2]
let key2 = scope['node2'].key;
// Component 'Child'
let k14 = \`__14__\${key1}__\${key2}__\`;
let w13 = k14 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k14]] : false;
let props13 = {};
if (w13 && w13.__owl__.currentFiber && !w13.__owl__.vnode) {
w13.destroy();
w13 = false;
}
if (w13) {
w13.__updateProps(props13, extra.fiber, utils.combine(context, scope));
let pvnode = w13.__owl__.pvnode;
c8.push(pvnode);
} else {
let componentKey13 = \`Child\`;
let W13 = scope['Child'] || context.constructor.components[componentKey13] || QWeb.components[componentKey13];
if (!W13) {throw new Error('Cannot find the definition of component \\"' + componentKey13 + '\\"')}
w13 = new W13(parent, props13);
parent.__owl__.cmap[k14] = w13.__owl__.id;
w13.__owl__.slotId = 1;
let fiber = w13.__prepare(extra.fiber, utils.combine(context, scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k14, hook: {remove() {},destroy(vn) {w13.destroy();}}});
c8.push(pvnode);
w13.__owl__.pvnode = pvnode;
}
w13.__owl__.parentLastFiberId = extra.fiber.id;
}
scope = _origScope12;
}
scope = _origScope5;
return vn1;
}"
`;
exports[`t-slot directive t-set t-value in a slot 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__1\\"
let h = this.h;
let c5 = [], p5 = {key:5};
let vn5 = h('span', p5, c5);
const slot6 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot6) {
slot6.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c5, parent: extra.parent || context}));
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
const slot5 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot5) {
slot5.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c4, parent: extra.parent || context}));
}
return vn5;
return vn4;
}"
`;
@@ -746,12 +581,12 @@ exports[`t-slot directive template can just return a slot 1`] = `
let utils = this.constructor.utils;
let result;
let h = this.h;
const slot7 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot7) {
let children8= []
const slot6 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot6) {
let children7= []
result = {}
slot7.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: children8, parent: extra.parent || context}));
utils.defineProxy(result, children8[0]);
slot6.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: children7, parent: extra.parent || context}));
utils.defineProxy(result, children7[0]);
}
return result;
}"
+5 -217
View File
@@ -1,4 +1,4 @@
import { Component, Env, STATUS } from "../../src/component/component";
import { Component, Env } from "../../src/component/component";
import { useState } from "../../src/hooks";
import { xml } from "../../src/tags";
import { makeDeferred, makeTestEnv, makeTestFixture, nextMicroTick, nextTick } from "../helpers";
@@ -40,14 +40,14 @@ describe("async rendering", () => {
}
}
const w = new W();
expect(w.__owl__.status).toBe(STATUS.CREATED);
w.mount(fixture);
expect(w.__owl__.status).toBe(STATUS.WILLSTARTED);
expect(w.__owl__.isDestroyed).toBe(false);
expect(w.__owl__.isMounted).toBe(false);
w.destroy();
expect(w.__owl__.status).toBe(STATUS.DESTROYED);
def.resolve();
await nextTick();
expect(w.__owl__.status).toBe(STATUS.DESTROYED);
expect(w.__owl__.isDestroyed).toBe(true);
expect(w.__owl__.isMounted).toBe(false);
});
test("destroying/recreating a subwidget with different props (if start is not over)", async () => {
@@ -1388,62 +1388,6 @@ describe("async rendering", () => {
expect(fixture.innerHTML).toBe("<span>4</span>");
});
test("calling render in destroy", async () => {
let a: any = null;
let c: any = null;
class C extends Component {
static template = xml`
<div>
<t t-esc="props.fromA"/>
</div>`;
}
let flag = false;
class B extends Component {
static template = xml`<C fromA="props.fromA"/>`;
static components = { C };
setup() {
c = this;
}
mounted() {
if (flag) {
this.render();
} else {
flag = true;
}
}
willUnmount() {
c.render();
}
}
class A extends Component {
static template = xml`<B t-key="key" fromA="state"/>`;
static components = { B };
state = "a";
key = 1;
setup() {
a = this;
}
}
const parent = new A();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div>a</div>");
a.state = "A";
a.key = 2;
await a.render();
// this nextTick is critical, otherwise jest may silently swallow errors
await nextTick();
expect(fixture.innerHTML).toBe("<div>A</div>");
});
test("change state and call manually render: no unnecessary rendering", async () => {
class Widget extends Component {
static template = xml`<div><t t-esc="state.val"/></div>`;
@@ -1461,160 +1405,4 @@ describe("async rendering", () => {
expect(fixture.innerHTML).toBe("<div>2</div>");
expect(Widget.prototype.__render).toHaveBeenCalledTimes(2);
});
test("components with shouldUpdate=false", async () => {
const state = { p: 1, cc: 10 };
class ChildChild extends Component {
static template = xml`
<div>
child child: <t t-esc="state.cc"/>
</div>`;
state = state;
shouldUpdate() {
return false;
}
}
class Child extends Component {
static components = { ChildChild };
static template = xml`
<div>
child
<ChildChild/>
</div>`;
shouldUpdate() {
return false;
}
}
let parent: any;
class Parent extends Component {
static components = { Child };
static template = xml`
<div>
parent: <t t-esc="state.p"/>
<Child/>
</div>`;
state = state;
constructor(a, b) {
super(a, b);
parent = this;
}
shouldUpdate() {
return false;
}
}
class App extends Component {
static components = { Parent };
static template = xml`
<div>
<Parent/>
</div>`;
}
var div = document.createElement("div");
fixture.appendChild(div);
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
"<div></div><div><div> parent: 1<div> child <div> child child: 10</div></div></div></div>"
);
app.mount(div);
// wait for rendering from second mount to go through parent
await Promise.resolve();
await Promise.resolve();
state.cc++;
state.p++;
parent.render();
await nextTick();
expect(fixture.innerHTML).toBe(
"<div><div><div> parent: 2<div> child <div> child child: 11</div></div></div></div></div>"
);
});
test("components with shouldUpdate=false, part 2", async () => {
const state = { p: 1, cc: 10 };
let shouldUpdate = true;
class ChildChild extends Component {
static template = xml`
<div>
child child: <t t-esc="state.cc"/>
</div>`;
state = state;
shouldUpdate() {
return shouldUpdate;
}
}
class Child extends Component {
static components = { ChildChild };
static template = xml`
<div>
child
<ChildChild/>
</div>`;
shouldUpdate() {
return shouldUpdate;
}
}
let parent: any;
class Parent extends Component {
static components = { Child };
static template = xml`
<div>
parent: <t t-esc="state.p"/>
<Child/>
</div>`;
state = state;
constructor(a, b) {
super(a, b);
parent = this;
}
shouldUpdate() {
return shouldUpdate;
}
}
class App extends Component {
static components = { Parent };
static template = xml`
<div>
<Parent/>
</div>`;
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
"<div><div> parent: 1<div> child <div> child child: 10</div></div></div></div>"
);
state.cc++;
state.p++;
app.render();
// wait for rendering to go through child
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
shouldUpdate = false;
parent.render();
await nextTick();
expect(fixture.innerHTML).toBe(
"<div><div> parent: 2<div> child <div> child child: 11</div></div></div></div>"
);
});
});
+1 -3
View File
@@ -265,9 +265,7 @@ describe("class and style attributes with t-component", () => {
error = e;
}
expect(error).toBeDefined();
const regexp =
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
expect(error.message).toMatch(regexp);
expect(error.message).toBe("Cannot read property 'crash' of undefined");
expect(fixture.innerHTML).toBe("");
});
});
+47 -376
View File
@@ -1,4 +1,4 @@
import { Component, Env, mount, STATUS } from "../../src/component/component";
import { Component, Env } from "../../src/component/component";
import { EventBus } from "../../src/core/event_bus";
import { useRef, useState } from "../../src/hooks";
import { QWeb } from "../../src/qweb/qweb";
@@ -69,23 +69,18 @@ describe("basic widget properties", () => {
class SomeWidget extends Component {
static template = xml`<div>content</div>`;
}
await mount(SomeWidget, { target: fixture });
const widget = new SomeWidget();
widget.mount(fixture);
await nextTick();
expect(fixture.innerHTML).toBe("<div>content</div>");
});
test("can be mounted with props", async () => {
class SomeWidget extends Component {
static template = xml`<div><t t-esc="props.content"/></div>`;
}
await mount(SomeWidget, { target: fixture, props: { content: "foo" } });
expect(fixture.innerHTML).toBe("<div>foo</div>");
});
test("can be mounted on a documentFragment", async () => {
class SomeWidget extends Component {
static template = xml`<div>content</div>`;
}
const widget = await mount(SomeWidget, { target: document.createDocumentFragment() });
const widget = new SomeWidget();
await widget.mount(document.createDocumentFragment());
expect(fixture.innerHTML).toBe("");
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div>content</div>");
@@ -95,9 +90,10 @@ describe("basic widget properties", () => {
class SomeWidget extends Component {
static template = xml`<div>content</div>`;
}
const widget = new SomeWidget();
let error;
try {
await mount(SomeWidget, { target: null as any });
await widget.mount(null as any);
} catch (e) {
error = e;
}
@@ -111,9 +107,10 @@ describe("basic widget properties", () => {
class SomeWidget extends Component {
static template = xml`<t/>`;
}
const widget = new SomeWidget();
let error;
try {
await mount(SomeWidget, { target: fixture });
await widget.mount(fixture);
} catch (e) {
error = e;
}
@@ -140,7 +137,8 @@ describe("basic widget properties", () => {
});
}
const counter = await mount(Counter, { target: fixture });
const counter = new Counter();
counter.mount(fixture);
await nextTick();
expect(fixture.innerHTML).toBe("<div>0<button>Inc</button></div>");
const button = (<HTMLElement>counter.el).getElementsByTagName("button")[0];
@@ -149,24 +147,6 @@ describe("basic widget properties", () => {
expect(fixture.innerHTML).toBe("<div>1<button>Inc</button></div>");
});
test("support for callable expression in event handler", async () => {
class Counter extends Component {
static template = xml`
<div><t t-esc="state.value"/><input type="text" t-on-input="obj.onInput"/></div>`;
state = useState({ value: "" });
obj = { onInput: (ev) => (this.state.value = ev.target.value) };
}
const counter = await mount(Counter, { target: fixture });
await nextTick();
expect(fixture.innerHTML).toBe(`<div><input type="text"></div>`);
const input = (<HTMLElement>counter.el).getElementsByTagName("input")[0];
input.value = "test";
input.dispatchEvent(new Event("input"));
await nextTick();
expect(fixture.innerHTML).toBe(`<div>test<input type="text"></div>`);
});
test("can handle empty props", async () => {
class Child extends Component {
static template = xml`<span><t t-esc="props.val"/></span>`;
@@ -176,12 +156,13 @@ describe("basic widget properties", () => {
static components = { Child };
}
await mount(Parent, { target: fixture });
const parent = new Parent();
await parent.mount(fixture);
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
expect(fixture.innerHTML).toBe("<div><span></span></div>");
});
test("can be clicked on and updated if not in DOM", async () => {
test("cannot be clicked on and updated if not in DOM", async () => {
class Counter extends Component {
static template = xml`
<div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`;
@@ -190,14 +171,15 @@ describe("basic widget properties", () => {
});
}
const counter = new Counter();
const target = document.createElement("div");
const counter = await mount(Counter, { target: target });
await counter.mount(target);
expect(target.innerHTML).toBe("<div>0<button>Inc</button></div>");
const button = (<HTMLElement>counter.el).getElementsByTagName("button")[0];
button.click();
await nextTick();
expect(target.innerHTML).toBe("<div>1<button>Inc</button></div>");
expect(counter.state.counter).toBe(1);
expect(target.innerHTML).toBe("<div>0<button>Inc</button></div>");
expect(counter.state.counter).toBe(0);
});
test("widget style and classname", async () => {
@@ -206,7 +188,8 @@ describe("basic widget properties", () => {
<div style="font-weight:bold;" class="some-class">world</div>
`;
}
await mount(StyledWidget, { target: fixture });
const widget = new StyledWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(`<div style="font-weight:bold;" class="some-class">world</div>`);
});
@@ -229,8 +212,8 @@ describe("basic widget properties", () => {
steps.push("patched");
}
}
await mount(TestW, { target: fixture });
const widget = new TestW();
await widget.mount(fixture);
expect(steps).toEqual(["__render", "mounted"]);
});
@@ -679,8 +662,9 @@ describe("lifecycle hooks", () => {
class ChildWidget extends Component {
static template = xml`<div/>`;
setup() {
steps.push("setup");
constructor(parent) {
super(parent);
steps.push("init");
}
async willStart() {
steps.push("willstart");
@@ -704,10 +688,10 @@ describe("lifecycle hooks", () => {
const widget = new ParentWidget();
await widget.mount(fixture);
expect(steps).toEqual(["setup", "willstart", "mounted"]);
expect(steps).toEqual(["init", "willstart", "mounted"]);
widget.state.ok = false;
await nextTick();
expect(steps).toEqual(["setup", "willstart", "mounted", "willunmount"]);
expect(steps).toEqual(["init", "willstart", "mounted", "willunmount"]);
});
test("components are unmounted and destroyed if no longer in DOM, even after updateprops", async () => {
@@ -755,7 +739,8 @@ describe("lifecycle hooks", () => {
class ChildWidget extends Component {
static template = xml`<div/>`;
setup() {
constructor(parent) {
super(parent);
steps.push("c init");
}
async willStart() {
@@ -771,7 +756,8 @@ describe("lifecycle hooks", () => {
class ParentWidget extends Component {
static template = xml`<div><t t-component="child"/></div>`;
static components = { child: ChildWidget };
setup() {
constructor(parent?) {
super(parent);
steps.push("p init");
}
async willStart() {
@@ -977,69 +963,6 @@ describe("lifecycle hooks", () => {
"parent:patched",
]);
});
test("willPatch/patched hook is not called if not mounted in DOM", async () => {
const steps: string[] = [];
class ChildWidget extends Component {
static template = xml`<div/>`;
constructor(parent, props) {
super(parent, props);
steps.push("child:constructor");
}
mounted() {
steps.push("child:mounted");
}
willPatch() {
steps.push("child:willPatch");
}
patched() {
steps.push("child:patched");
}
}
class ParentWidget extends Component {
static template = xml`
<div>
<t t-component="child" v="state.n"/>
</div>
`;
static components = { child: ChildWidget };
state = useState({ n: 1 });
constructor() {
super();
steps.push("parent:constructor");
}
mounted() {
steps.push("parent:mounted");
}
willPatch() {
steps.push("parent:willPatch");
}
patched() {
steps.push("parent:patched");
}
}
const div = document.createElement("div");
const widget = new ParentWidget();
await widget.mount(div);
expect(steps).toEqual(["parent:constructor", "child:constructor"]);
widget.state.n = 2;
await nextTick();
expect(steps).toEqual(["parent:constructor", "child:constructor"]);
// then we remount the component in the dom
await widget.mount(fixture);
expect(steps).toEqual([
"parent:constructor",
"child:constructor",
"child:mounted",
"parent:mounted",
]);
});
});
describe("destroy method", () => {
@@ -1053,7 +976,8 @@ describe("destroy method", () => {
expect(document.contains(widget.el)).toBe(true);
widget.destroy();
expect(document.contains(widget.el)).toBe(false);
expect(widget.__owl__.status).toBe(STATUS.DESTROYED);
expect(widget.__owl__.isMounted).toBe(false);
expect(widget.__owl__.isDestroyed).toBe(true);
});
test("destroying a parent also destroys its children", async () => {
@@ -1062,9 +986,9 @@ describe("destroy method", () => {
const child = children(parent)[0];
expect(child.__owl__.status).toBe(STATUS.MOUNTED);
expect(child.__owl__.isDestroyed).toBe(false);
parent.destroy();
expect(child.__owl__.status).toBe(STATUS.DESTROYED);
expect(child.__owl__.isDestroyed).toBe(true);
});
test("destroy remove the parent/children link", async () => {
@@ -1090,15 +1014,17 @@ describe("destroy method", () => {
}
expect(fixture.innerHTML).toBe("");
const widget = new DelayedWidget();
expect(widget.__owl__.status).toBe(STATUS.CREATED);
widget.mount(fixture);
expect(widget.__owl__.status).toBe(STATUS.WILLSTARTED);
expect(widget.__owl__.isMounted).toBe(false);
expect(widget.__owl__.isDestroyed).toBe(false);
widget.destroy();
expect(widget.__owl__.status).toBe(STATUS.DESTROYED);
expect(widget.__owl__.isMounted).toBe(false);
expect(widget.__owl__.isDestroyed).toBe(true);
def.resolve();
await nextTick();
expect(widget.__owl__.status).toBe(STATUS.DESTROYED);
expect(widget.__owl__.isMounted).toBe(false);
expect(widget.__owl__.isDestroyed).toBe(true);
expect(widget.__owl__.vnode).toBe(undefined);
expect(fixture.innerHTML).toBe("");
expect(isRendered).toBe(false);
@@ -1246,30 +1172,6 @@ describe("composition", () => {
expect(fixture.innerHTML).toBe("<div>child b</div>");
});
test("can switch between dynamic components without the need for a t-key", async () => {
class A extends Component {
static template = xml`<span>child a</span>`;
}
class B extends Component {
static template = xml`<span>child b</span>`;
}
class App extends Component {
static template = xml`
<div>
<t t-component="{{state.child}}"/>
</div>`;
static components = { A, B };
state = useState({ child: "A" });
}
const app = await mount(App, { target: fixture });
expect(fixture.innerHTML).toBe("<div><span>child a</span></div>");
app.state.child = "B";
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>child b</span></div>");
expect(QWeb.TEMPLATES[App.template].fn.toString()).toMatchSnapshot();
});
test("don't fallback to global registry if widget defined locally", async () => {
QWeb.registerComponent("WidgetB", WidgetB); // should not use this widget
env.qweb.addTemplate("ParentWidget", `<div><t t-component="WidgetB"/></div>`);
@@ -1284,23 +1186,6 @@ describe("composition", () => {
delete QWeb.components["WidgetB"];
});
test("don't fallback to global/component's registry if widget defined in the instance's context", async () => {
QWeb.registerComponent("WidgetB", WidgetB); // should not use this widget
env.qweb.addTemplate("ParentWidget", `<div><t t-component="WidgetB"/></div>`);
env.qweb.addTemplate("ComponentWidgetB", `<span>Belgium</span>`); // should not use this widget either
env.qweb.addTemplate("InstanceWidgetB", `<span>Chocolate</span>`); // should use this
class ComponentWidgetB extends Component {}
class InstanceWidgetB extends Component {}
class ParentWidget extends Component {
static components = { WidgetB: ComponentWidgetB };
WidgetB = InstanceWidgetB;
}
const widget = new ParentWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>Chocolate</span></div>");
delete QWeb.components["WidgetB"];
});
test("can define components in template without t-component", async () => {
env.qweb.addTemplates(`
<templates>
@@ -1595,7 +1480,7 @@ describe("composition", () => {
parent.state.flag = true;
await nextTick();
expect(children(parent)[0]).toBe(child);
expect(child.__owl__.status).toBe(STATUS.MOUNTED);
expect(child.__owl__.isDestroyed).toBe(false);
expect(normalize(fixture.innerHTML)).toBe(
normalize(`
<div>
@@ -1881,176 +1766,6 @@ describe("props evaluation ", () => {
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>&lt;p&gt;4343&lt;/p&gt;<p>43</p></span></div>");
});
test("arrow function prop captures component instance as 'this'", async () => {
expect.assertions(5);
let child, parent;
class Child extends Component {
setup() {
child = this;
}
}
env.qweb.addTemplate("Child", `<span><t t-esc="props.value"/></span>`);
class Parent extends Component {
static components = { Child };
state = useState({ val: 42 });
setup() {
parent = this;
}
setValue(value) {
expect(this).toBe(parent);
this.state.val = value;
}
}
env.qweb.addTemplate(
"Parent",
`<div>
<Child callback="value => this.setValue(value)" value="state.val"/>
</div>`
);
const widget = new Parent();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>42</span></div>");
child.props.callback(123);
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>123</span></div>");
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
expect(env.qweb.templates.Child.fn.toString()).toMatchSnapshot();
});
test("arrow function prop captures context component instance as 'this' inside slot", async () => {
expect.assertions(7);
let child, parent;
class Child extends Component {
setup() {
child = this;
}
}
env.qweb.addTemplate("Child", `<span><t t-esc="props.value"/></span>`);
class Wrapper extends Component {}
env.qweb.addTemplate("Wrapper", `<t t-slot="default"/>`);
class Parent extends Component {
static components = { Child, Wrapper };
state = useState({ val: 42 });
setup() {
parent = this;
}
setValue(value) {
expect(this).toBe(parent);
this.state.val = value;
}
}
env.qweb.addTemplate(
"Parent",
`<div>
<Wrapper>
<Child callback="value => this.setValue(value)" value="state.val"/>
</Wrapper>
</div>`
);
const widget = new Parent();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>42</span></div>");
child.props.callback(123);
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>123</span></div>");
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
expect(env.qweb.templates.Wrapper.fn.toString()).toMatchSnapshot();
expect(QWeb.slots["1_default"].toString()).toMatchSnapshot();
expect(env.qweb.templates.Child.fn.toString()).toMatchSnapshot();
});
test("arrow function prop captures context component instance as 'this' inside slot default content", async () => {
expect.assertions(6);
let child, wrapper;
class Child extends Component {
setup() {
child = this;
}
}
env.qweb.addTemplate("Child", `<span><t t-esc="props.value"/></span>`);
class Wrapper extends Component {
static components = { Child };
state = useState({ val: 42 });
setup() {
wrapper = this;
}
setValue(value) {
expect(this).toBe(wrapper);
this.state.val = value;
}
}
env.qweb.addTemplate(
"Wrapper",
`<t t-slot="default">
<Child callback="value => this.setValue(value)" value="state.val"/>
</t>`
);
class Parent extends Component {
static components = { Wrapper };
}
env.qweb.addTemplate(
"Parent",
`<div>
<Wrapper/>
</div>`
);
const widget = new Parent();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>42</span></div>");
child.props.callback(123);
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>123</span></div>");
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
expect(env.qweb.templates.Wrapper.fn.toString()).toMatchSnapshot();
expect(env.qweb.templates.Child.fn.toString()).toMatchSnapshot();
});
test("arrow function prop captures loop variables", async () => {
let children = [];
class Child extends Component {
setup() {
children.push(this);
}
}
env.qweb.addTemplate("Child", `<span><t t-esc="props.value"/></span>`);
class Parent extends Component {
static components = { Child };
state = useState({ val: 42 });
setValue(value) {
this.state.val = value;
}
}
env.qweb.addTemplate(
"Parent",
`<div>
<t t-foreach="[0, 1]" t-as="loopVar" t-key="loopVar">
<Child callback="() => this.setValue(loopVar)" value="state.val"/>
</t>
</div>`
);
const widget = new Parent();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>42</span><span>42</span></div>");
children[0].props.callback();
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>0</span><span>0</span></div>");
children[1].props.callback();
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>1</span><span>1</span></div>");
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
expect(env.qweb.templates.Child.fn.toString()).toMatchSnapshot();
});
});
describe("other directives with t-component", () => {
@@ -2513,29 +2228,9 @@ describe("other directives with t-component", () => {
el.click();
expect(steps).toEqual(["click"]);
parent.unmount();
expect(child.__owl__.status).toBe(STATUS.UNMOUNTED);
expect(child.__owl__.isMounted).toBe(false);
el.click();
expect(steps).toEqual(["click", "click"]);
});
test("triggering custom event on mounted components", async () => {
let value = false;
class Child extends Component {
static template = xml`<div/>`;
mounted() {
this.trigger("coucou");
}
}
class Parent extends Component {
static template = xml`<Child t-on-coucou="doSomething"/>`;
static components = { Child };
doSomething() {
value = true;
}
}
const parent = new Parent();
await parent.mount(fixture);
expect(value).toBe(true);
expect(steps).toEqual(["click"]);
});
test("t-on with .capture modifier", async () => {
@@ -2587,7 +2282,7 @@ describe("other directives with t-component", () => {
expect(steps).toEqual(["click"]);
parent.state.flag = false;
await nextTick();
expect(child.__owl__.status).toBe(STATUS.DESTROYED);
expect(child.__owl__.isDestroyed).toBe(true);
el.click();
expect(steps).toEqual(["click"]);
});
@@ -2622,7 +2317,7 @@ describe("other directives with t-component", () => {
expect(steps).toEqual(["click"]);
parent.state.flag = false;
await nextTick();
expect(child.__owl__.status).toBe(STATUS.DESTROYED);
expect(child.__owl__.isDestroyed).toBe(true);
el.click();
expect(steps).toEqual(["click"]);
});
@@ -4234,30 +3929,6 @@ describe("dynamic t-props", () => {
expect(fixture.innerHTML).toBe("<div><span>3</span></div>");
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
});
test("t-props with props", async () => {
expect.assertions(1);
class Child extends Component {
static template = xml`<div />`;
setup() {
expect(this.props).toEqual({ a: 1, b: 2, c: "c" });
}
}
class Parent extends Component {
static template = xml`
<div>
<Child t-props="props" a="1" b="2" />
</div>
`;
static components = { Child };
props = { a: "a", c: "c" };
}
const widget = new Parent();
await widget.mount(fixture);
});
});
describe("support svg components", () => {
+7 -104
View File
@@ -1,4 +1,4 @@
import { Component, Env, mount, STATUS } from "../../src/component/component";
import { Component, Env } from "../../src/component/component";
import { useState } from "../../src/hooks";
import { xml } from "../../src/tags";
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
@@ -95,9 +95,7 @@ describe("component error handling (catchError)", () => {
try {
await super.render();
} catch (e) {
expect(e.message).toMatch(
/Cannot read properties of undefined \(reading 'this'\)|Cannot read property 'this' of undefined/
);
expect(e.message).toBe("Cannot read property 'this' of undefined");
}
}
}
@@ -110,7 +108,7 @@ describe("component error handling (catchError)", () => {
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
expect(app.__owl__.status).toBe(STATUS.DESTROYED);
expect(app.__owl__.isDestroyed).toBe(true);
expect(handler).toBeCalledTimes(1);
});
@@ -361,9 +359,7 @@ describe("component error handling (catchError)", () => {
error = e;
}
expect(error).toBeDefined();
const regexp =
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
expect(error.message).toMatch(regexp);
expect(error.message).toBe("Cannot read property 'crash' of undefined");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
@@ -473,9 +469,7 @@ describe("component error handling (catchError)", () => {
error = e;
}
expect(error).toBeDefined();
const regexp =
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
expect(error.message).toMatch(regexp);
expect(error.message).toBe("Cannot read property 'crash' of undefined");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
@@ -501,9 +495,7 @@ describe("component error handling (catchError)", () => {
error = e;
}
expect(error).toBeDefined();
const regexp =
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
expect(error.message).toMatch(regexp);
expect(error.message).toBe("Cannot read property 'crash' of undefined");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
@@ -526,95 +518,6 @@ describe("component error handling (catchError)", () => {
error = e;
}
expect(error).toBeDefined();
const regexp =
/Cannot read properties of undefined \(reading 'y'\)|Cannot read property 'y' of undefined/g;
expect(error.message).toMatch(regexp);
});
test("simple catchError", async () => {
class Boom extends Component {
static template = xml`<div t-esc="a.b.c"/>`;
}
class Parent extends Component {
static template = xml`
<div>
<t t-if="error">Error</t>
<t t-else="">
<Boom />
</t>
</div>`;
static components = { Boom };
error = false;
catchError(error) {
this.error = error;
this.render();
}
}
await mount(Parent, { target: fixture });
expect(fixture.innerHTML).toBe("<div>Error</div>");
});
test("catchError in catchError", async () => {
class Boom extends Component {
static template = xml`<div t-esc="a.b.c"/>`;
}
class Child extends Component {
static template = xml`
<div>
<Boom />
</div>`;
static components = { Boom };
catchError(error) {
throw error;
}
}
class Parent extends Component {
static template = xml`
<div>
<t t-if="error">Error</t>
<t t-else="">
<Child />
</t>
</div>`;
static components = { Child };
error = false;
catchError(error) {
this.error = error;
this.render();
}
}
await mount(Parent, { target: fixture });
expect(fixture.innerHTML).toBe("<div>Error</div>");
});
test("errors in mounted and in willUnmount", async () => {
expect.assertions(1);
class Example extends Component {
static template = xml`<div/>`;
val;
mounted() {
throw new Error("Error in mounted");
this.val = { foo: "bar" };
}
willUnmount() {
console.log(this.val.foo);
}
}
try {
await mount(Example, { target: fixture });
} catch (e) {
expect(e.message).toBe("Error in mounted");
}
expect(error.message).toBe("Cannot read property 'y' of undefined");
});
});
+1 -319
View File
@@ -1,4 +1,4 @@
import { Component, Env, mount } from "../../src/component/component";
import { Component, Env } from "../../src/component/component";
import { QWeb } from "../../src/qweb/qweb";
import { xml } from "../../src/tags";
import { useState, useRef } from "../../src/hooks";
@@ -325,70 +325,6 @@ describe("t-slot directive", () => {
expect(QWeb.slots["1_default"].toString()).toMatchSnapshot();
});
test("slots in t-foreach in t-foreach", async () => {
class Child extends Component {
static template = xml`
<div><t t-slot="default" /></div>
`;
}
class App extends Component {
static template = xml`
<div>
<t t-foreach="tree" t-as="node1" t-key="node1.key">
<div t-esc="node1.value" />
<ul>
<t t-foreach="node1.nodes" t-as="node2" t-key="node2.key">
<Child>
<li t-esc="node1.value" />
</Child>
</t>
</ul>
</t>
</div>`;
static components = { Child };
tree = [
{
key: "a",
value: "A",
nodes: [
{
key: "1",
value: "A-1",
},
{
key: "2",
value: "A-2",
},
],
},
{
key: "b",
value: "B",
nodes: [
{
key: "1",
value: "B-1",
},
{
key: "2",
value: "B-2",
},
],
},
];
}
await mount(App, { target: fixture });
expect(fixture.innerHTML).toBe(
"<div><div>A</div><ul><div><li>A</li></div><div><li>A</li></div></ul><div>B</div><ul><div><li>B</li></div><div><li>B</li></div></ul></div>"
);
expect(env.qweb.templates[App.template].fn.toString()).toMatchSnapshot();
});
test("refs are properly bound in slots", async () => {
class Dialog extends Component {
static template = xml`<span><t t-slot="footer"/></span>`;
@@ -467,48 +403,6 @@ describe("t-slot directive", () => {
expect(QWeb.slots["1_default"].toString()).toMatchSnapshot();
});
test("default slot next to named slot, with default content", async () => {
class Dialog extends Component {
// We're using 2 slots here: a "default" one and a "footer",
// both having default children nodes.
static template = xml`
<div class="Dialog">
<div class="content">
<t t-slot="default">
Default content
</t>
</div>
<div class="footer">
<t t-slot="footer">
Default footer
</t>
</div>
</div>
`;
}
class App extends Component {
// Here we're trying to assign the "footer" slot with some content
static components = { Dialog };
static template = xml`
<div>
<Dialog>
<t t-set-slot="footer">
Overridden footer
</t>
</Dialog>
</div>
`;
}
await mount(App, { target: fixture });
expect(fixture.innerHTML).toBe(
'<div><div class="Dialog"><div class="content"> Default content </div><div class="footer"> Overridden footer </div></div></div>'
);
expect(QWeb.TEMPLATES[App.template].fn.toString()).toMatchSnapshot();
});
test("multiple roots are allowed in a named slot", async () => {
env.qweb.addTemplates(`
<templates>
@@ -1158,216 +1052,4 @@ describe("t-slot directive", () => {
"<div><child><p>Ablip</p>default2<child>default1<p>Bblip</p></child></child></div>"
);
});
test("named slot inside slot, part 3", async () => {
class Child extends Component {
static template = xml`
<div>
<t t-slot="brol"/>
<t t-slot="default"/>
</div>`;
}
class Parent extends Component {
static template = xml`
<div>
<Child>
<t t-set-slot="brol">
<p>A<t t-esc="value"/></p>
</t>
<Child>
<t>
<t t-set-slot="brol">
<p>B<t t-esc="value"/></p>
</t>
</t>
</Child>
</Child>
</div>`;
static components = { Child };
value = "blip";
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><p>Ablip</p><div><p>Bblip</p></div></div></div>");
});
test("dynamic t-slot call", async () => {
class Toggler extends Component {
static template = xml`<button t-on-click="toggle"><t t-slot="{{current.slot}}"/></button>`;
current = useState({ slot: "slot1" });
toggle() {
this.current.slot = this.current.slot === "slot1" ? "slot2" : "slot1";
}
}
class Parent extends Component {
static template = xml`
<div>
<Toggler>
<t t-set-slot="slot1"><p>slot1</p><span>content</span></t>
<t t-set-slot="slot2"><h1>slot2</h1></t>
</Toggler>
</div>`;
static components = { Toggler };
}
await mount(Parent, { target: fixture });
expect(fixture.innerHTML).toBe("<div><button><p>slot1</p><span>content</span></button></div>");
fixture.querySelector<HTMLElement>("button")!.click();
await nextTick();
expect(fixture.innerHTML).toBe("<div><button><h1>slot2</h1></button></div>");
fixture.querySelector<HTMLElement>("button")!.click();
await nextTick();
expect(fixture.innerHTML).toBe("<div><button><p>slot1</p><span>content</span></button></div>");
expect(env.qweb.templates[Toggler.template].fn.toString()).toMatchSnapshot();
});
test("t-slot within dynamic t-call", async () => {
let child;
class Child extends Component {
static template = xml`<div class="child"/>`;
constructor(...args) {
super(...args);
child = this;
}
}
class Slotted extends Component {
static template = xml`<div class="slotted"><t t-slot="default" /></div>`;
}
class UsingTcallInSlotted extends Component {
tcallTemplate = xml`<div class="slot"><Child/></div>`;
static template = xml`
<div>
<Slotted>
<t t-call="{{ tcallTemplate }}"/>
</Slotted>
</div>`;
static components = { Slotted, Child };
}
await mount(UsingTcallInSlotted, { target: fixture });
expect(child.__owl__.parent).toBeInstanceOf(Slotted);
expect(fixture.innerHTML).toBe(
`<div><div class="slotted"><div class="slot"><div class="child"></div></div></div></div>`
);
});
test("t-slot scope context", async () => {
expect.assertions(1);
class Wrapper extends Component {
static template = xml`<t t-slot="default"/>`;
}
let dialog;
class Dialog extends Component {
static template = xml`
<Wrapper>
<div t-on-click="onClick">
<t t-slot="default" />
</div>
</Wrapper>
`;
static components = { Wrapper };
setup() {
dialog = this;
}
onClick(ev) {
// we do not use expect(this).toBe(dialog) here because if it fails, it
// may blow up jest because it then tries to compute a diff, which is
// infinite if there is a cycle
expect(this === dialog).toBe(true);
}
}
class Parent extends Component {
static template = xml`
<Dialog>
<button>The Button</button>
</Dialog>`;
static components = { Dialog };
}
await mount(Parent, { target: fixture });
document.querySelector("button").click();
await nextTick();
});
test("t-slot in recursive templates", async () => {
QWeb.registerTemplate(
"_test_recursive_template",
`
<Wrapper>
<t t-esc="name" />
<t t-foreach="items" t-as="item">
<t t-if="!item.children.length">
<t t-esc="item.name" />
</t>
<t t-else="" t-call="_test_recursive_template">
<t t-set="name" t-value="item.name" />
<t t-set="items" t-value="item.children" />
</t>
</t>
</Wrapper>`
);
class Wrapper extends Component {
static template = xml`
<wrapper>
<t t-slot="default"/>
</wrapper>`;
}
class Parent extends Component {
static template = "_test_recursive_template";
static components = { Wrapper };
name = "foo";
items = [
{
name: "foo-0",
children: [
{ name: "foo-00", children: [] },
{
name: "foo-01",
children: [
{ name: "foo-010", children: [] },
{ name: "foo-011", children: [] },
{
name: "foo-012",
children: [
{ name: "foo-0120", children: [] },
{ name: "foo-0121", children: [] },
{ name: "foo-0122", children: [] },
],
},
],
},
{ name: "foo-02", children: [] },
],
},
{ name: "foo-1", children: [] },
{ name: "foo-2", children: [] },
];
}
await mount(Parent, { target: fixture });
expect(fixture.innerHTML).toBe(
"<wrapper>foo<wrapper>foo-0foo-00<wrapper>foo-01foo-010foo-011<wrapper>foo-012foo-0120foo-0121foo-0122</wrapper></wrapper>foo-02</wrapper>foo-1foo-2</wrapper>"
);
});
});
+21 -142
View File
@@ -1,4 +1,4 @@
import { Component, Env, mount } from "../../src/component/component";
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";
@@ -37,7 +37,8 @@ describe("mount targets", () => {
div.innerHTML = `<p>pre-existing</p>`;
fixture.appendChild(div);
const app = await mount(App, { target: div, position: "self" });
const app = new App();
await app.mount(div, { position: "self" });
expect(fixture.innerHTML).toBe(
`<div class="arbitrary custom"><p>pre-existing</p>app<p>another tag</p></div>`
@@ -65,9 +66,10 @@ describe("mount targets", () => {
const div = document.createElement("div");
fixture.appendChild(div);
const app = new App();
let error;
try {
await mount(App, { target: div, position: "self" });
await app.mount(div, { position: "self" });
} catch (e) {
error = e;
}
@@ -82,7 +84,8 @@ describe("mount targets", () => {
const span = document.createElement("span");
fixture.appendChild(span);
await mount(App, { target: fixture, position: "first-child" });
const app = new App();
await app.mount(fixture, { position: "first-child" });
expect(fixture.innerHTML).toBe("<div>app</div><span></span>");
});
@@ -93,7 +96,8 @@ describe("mount targets", () => {
const span = document.createElement("span");
fixture.appendChild(span);
await mount(App, { target: fixture, position: "last-child" });
const app = new App();
await app.mount(fixture, { position: "last-child" });
expect(fixture.innerHTML).toBe("<span></span><div>app</div>");
});
@@ -104,7 +108,8 @@ describe("mount targets", () => {
const span = document.createElement("span");
fixture.appendChild(span);
await mount(App, { target: fixture });
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<span></span><div>app</div>");
});
});
@@ -128,7 +133,8 @@ describe("unmounting and remounting", () => {
}
}
const w = await mount(MyWidget, { target: fixture });
const w = new MyWidget();
await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div>Hey</div>");
expect(steps).toEqual(["willstart", "mounted"]);
@@ -156,7 +162,8 @@ describe("unmounting and remounting", () => {
}
}
const w = await mount(MyWidget, { target: fixture });
const w = new MyWidget();
await w.mount(fixture);
await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div>Hey</div>");
expect(steps).toEqual(["willstart", "mounted"]);
@@ -196,7 +203,8 @@ describe("unmounting and remounting", () => {
state = useState({ val: 1, flag: true });
}
const widget = await mount(Parent, { target: fixture });
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;
@@ -323,54 +331,6 @@ describe("unmounting and remounting", () => {
expect(steps).toEqual([2, 2, 3]);
});
test("change state and render while mounted in detached dom", async () => {
class App extends Component {
static template = xml`<div><t t-esc="state.val"/></div>`;
state = useState({ val: 1 });
}
const detachedDiv = document.createElement("div");
const app = await mount(App, { target: detachedDiv });
expect(detachedDiv.innerHTML).toBe("<div>1</div>");
app.state.val = 2;
await nextTick();
expect(detachedDiv.innerHTML).toBe("<div>2</div>");
});
test("change state and render while not mounted ", async () => {
class App extends Component {
static template = xml`<div><t t-esc="state.val"/></div>`;
state = useState({ val: 1 });
}
const app = new App(null);
app.state.val = 2; // will call the render method (before being mounted)
await nextTick();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>2</div>");
});
test("destroy and change state after mounted in detached dom", async () => {
class App extends Component {
static template = xml`<div><t t-esc="state.val"/></div>`;
state = useState({ val: 1 });
}
const detachedDiv = document.createElement("div");
const app = await mount(App, { target: detachedDiv });
expect(detachedDiv.innerHTML).toBe("<div>1</div>");
app.destroy();
app.state.val = 2;
await nextTick();
expect(detachedDiv.innerHTML).toBe("");
});
test("change state while component is unmounted", async () => {
let child;
class Child extends Component {
@@ -404,40 +364,6 @@ describe("unmounting and remounting", () => {
expect(fixture.innerHTML).toBe("<div>P2<span>C2</span></div>");
});
test("change state while component is mounted in a fragment", async () => {
class Child1 extends Component {
static template = xml`<span>C1</span>`;
}
class Child2 extends Component {
static template = xml`<span>C2</span>`;
}
class Parent extends Component {
static components = { Child1, Child2 };
static template = xml`
<div>
<Child1 t-if="child == 'c1'"/>
<Child2 t-if="child == 'c2'"/>
</div>`;
child: string | false = false;
}
const fragment = document.createDocumentFragment();
const parent = new Parent();
await parent.mount(fragment);
expect(parent.el.outerHTML).toBe("<div></div>");
parent.child = "c1";
parent.render();
await Promise.resolve();
parent.child = "c2";
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>C2</span></div>");
});
test("unmount component during a re-rendering", async () => {
const def = makeDeferred();
class Child extends Component {
@@ -524,17 +450,17 @@ describe("unmounting and remounting", () => {
// one full tick.
await nextMicroTick();
await nextMicroTick();
expect(steps).toEqual([]);
expect(steps).toEqual(["1 catch"]);
await nextTick();
expect(fixture.innerHTML).toBe("<div></div><span></span>");
def.resolve();
await nextTick();
expect(steps).toEqual(["2 resolved"]);
expect(steps).toEqual(["1 catch", "2 resolved"]);
expect(fixture.innerHTML).toBe("<div></div><span><div>Hey</div></span>");
});
test("component can be mounted on same target, another situation", async () => {
test("widget can be mounted on same target, another situation", async () => {
const def = makeDeferred();
const steps: string[] = [];
@@ -562,8 +488,8 @@ describe("unmounting and remounting", () => {
def.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("<div>Hey</div>");
expect(steps).toEqual(["1 resolved", "2 resolved"]);
expect(fixture.innerHTML).toBe("<div>Hey</div>");
});
test("mounting a destroyed widget", async () => {
@@ -682,51 +608,4 @@ describe("unmounting and remounting", () => {
await parent.render();
expect(fixture.textContent).toBe("fixedsome text");
});
test("remounting component tree where a component implement shouldupdate", async () => {
let state: any;
const steps = [];
class Child extends Component {
static template = xml`<div><t t-esc="state.word"/><t t-esc="props.name"/></div>`;
state = useState({ word: "hello" });
constructor(parent, props) {
super(parent, props);
state = this.state;
}
patched() {
steps.push("patched");
}
mounted() {
steps.push("mounted");
}
willUnmount() {
steps.push("willUnmount");
}
shouldUpdate() {
return false;
}
}
class Parent extends Component {
static template = xml`<div><Child name="state.name"/></div>`;
static components = { Child };
state = useState({ name: "World" });
}
const parent = await mount(Parent, { target: fixture });
expect(fixture.innerHTML).toBe("<div><div>helloWorld</div></div>");
parent.unmount();
expect(fixture.innerHTML).toBe("");
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>helloWorld</div></div>");
state.word = "test";
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>testWorld</div></div>");
expect(steps).toEqual(["mounted", "willUnmount", "mounted", "patched"]);
});
});
+1 -20
View File
@@ -289,26 +289,7 @@ describe("Context", () => {
expect(testContext.subscriptions.update.length).toBe(0);
});
test.skip("concurrent renderings", async () => {
/**
* Note: this test is interesting, but sadly just an incomplete attempt at
* protecting users against themselves. With the context API, it is not
* possible for the framework to protect completely against crashes. Maybe
* like in this case, when a component is in a simple hierarchy where all
* renderings come from the context changes, but in a real case, where some
* code can trigger a rendering independently, it is insufficient.
*
* The main problem is that the sub component depends on some external state,
* which may be modified, and then incompatible with the component actual
* state (for example, if the sub component has an id key related to some
* object that has been removed from the context).
*
* For now, sadly, the only solution is that components that depends on external
* state should guarantee their own integrity themselves. Then maybe this
* could be solved at the level of a state management solution that has a
* more advanced API, to let components determine if they should be updated
* or not (so, something slightly more advanced that the useStore hook).
*/
test("concurrent renderings", async () => {
const testContext = new Context({ x: { n: 1 }, key: "x" });
const def = makeDeferred();
let stateC;
+5 -10
View File
@@ -1,4 +1,4 @@
import { Env, Component, STATUS } from "../src/component/component";
import { Env, Component } from "../src/component/component";
import { scheduler } from "../src/component/scheduler";
import { EvalContext, QWeb } from "../src/qweb/qweb";
import { CompilationContext } from "../src/qweb/compilation_context";
@@ -37,13 +37,8 @@ export function nextMicroTick(): Promise<void> {
}
export async function nextTick(): Promise<void> {
await new Promise((resolve) => scheduler.requestAnimationFrame(resolve));
await new Promise((resolve) => setTimeout(resolve));
await new Promise((resolve) => scheduler.requestAnimationFrame(resolve));
}
export async function nextFrame(): Promise<void> {
await new Promise((resolve) => scheduler.requestAnimationFrame(resolve));
await new Promise((resolve) => scheduler.requestAnimationFrame(resolve));
}
export function makeTestFixture() {
@@ -92,7 +87,7 @@ export function renderToDOM(
if (!context.__owl__) {
// we add `__owl__` to better simulate a component as context. This is
// particularly important for event handlers added with the `t-on` directive.
context.__owl__ = { status: STATUS.MOUNTED };
context.__owl__ = { isMounted: true };
}
const vnode = qweb.render(template, context, extra);
@@ -132,7 +127,7 @@ export function renderToString(
// is useful for animations tests, as we hook before repaints to trigger
// animations (thanks to requestAnimationFrame). Patching nextFrame allows to
// simulate calls to this hook. One must not forget to unpatch afterwards.
let _nextFrame = QWeb.utils.nextFrame;
let nextFrame = QWeb.utils.nextFrame;
export function patchNextFrame(f: Function) {
QWeb.utils.nextFrame = (cb: () => void) => {
setTimeout(() => f(cb));
@@ -140,7 +135,7 @@ export function patchNextFrame(f: Function) {
}
export function unpatchNextFrame() {
QWeb.utils.nextFrame = _nextFrame;
QWeb.utils.nextFrame = nextFrame;
}
export async function editInput(input: HTMLInputElement | HTMLTextAreaElement, value: string) {
+6 -28
View File
@@ -9,10 +9,8 @@ import {
onWillPatch,
onWillStart,
onWillUpdateProps,
useEnv,
useSubEnv,
useExternalListener,
useComponent,
} from "../src/hooks";
import { xml } from "../src/tags";
@@ -75,6 +73,8 @@ describe("hooks", () => {
}
const component = new MyComponent();
await component.mount(fixture);
expect(component).not.toHaveProperty("mounted");
expect(component).not.toHaveProperty("willUnmount");
expect(fixture.innerHTML).toBe("<div>hey</div>");
expect(steps).toEqual(["mounted"]);
component.unmount();
@@ -381,6 +381,8 @@ describe("hooks", () => {
const component = new MyComponent();
await component.mount(fixture);
expect(component).not.toHaveProperty("patched");
expect(component).not.toHaveProperty("willPatch");
expect(steps).toEqual([]);
expect(fixture.innerHTML).toBe("<div>hey</div>");
@@ -518,19 +520,6 @@ describe("hooks", () => {
});
});
test("can use useEnv", async () => {
expect.assertions(1);
class TestComponent extends Component {
static template = xml`<div><t t-esc="env.val"/></div>`;
constructor() {
super();
expect(useEnv()).toBe(env);
}
}
const component = new TestComponent();
await component.mount(fixture);
});
test("can use sub env", async () => {
class TestComponent extends Component {
static template = xml`<div><t t-esc="env.val"/></div>`;
@@ -546,19 +535,6 @@ describe("hooks", () => {
expect(component.env).toHaveProperty("val");
});
test("can use useComponent", async () => {
expect.assertions(1);
class TestComponent extends Component {
static template = xml`<div></div>`;
constructor() {
super();
expect(useComponent()).toBe(this);
}
}
const component = new TestComponent();
await component.mount(fixture);
});
test("parent and child env", async () => {
class Child extends Component {
static template = xml`<div><t t-esc="env.val"/></div>`;
@@ -626,6 +602,8 @@ describe("hooks", () => {
const app = new App();
await app.mount(fixture);
expect(app).not.toHaveProperty("willStart");
expect(app).not.toHaveProperty("willUpdateProps");
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
// NOTE: 'on2ndStart' appears first in the list even though
+1 -3
View File
@@ -546,9 +546,7 @@ describe("Portal: Basic use and DOM placement", () => {
error = e;
}
expect(error).toBeDefined();
const regexp =
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
expect(error.message).toMatch(regexp);
expect(error.message).toBe("Cannot read property 'crash' of undefined");
});
test("portal manual unmount", async () => {
File diff suppressed because it is too large Load Diff
@@ -1,37 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`memory t-foreach does not leak stuff in global scope 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('p', p1, c1);
let _2 = [3,2,1];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _2;
let _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
}
let _length3 = _3.length;
let _origScope5 = scope;
scope = Object.create(scope);
for (let i1 = 0; i1 < _length3; i1++) {
scope.item_first = i1 === 0
scope.item_last = i1 === _length3 - 1
scope.item_index = i1
scope.item = _3[i1]
scope.item_value = _4[i1]
let key1 = i1;
let _6 = scope['item'];
if (_6 != null) {
c1.push({text: _6});
}
}
scope = _origScope5;
return vn1;
}"
`;
@@ -1,57 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`qweb t-att t-att-class with multiple classes 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let _1 = utils.toClassObj({'a b c':scope['value']});
let c2 = [], p2 = {key:2,class:_1};
let vn2 = h('div', p2, c2);
return vn2;
}"
`;
exports[`qweb t-att t-att-class with multiple classes 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let _3 = utils.toClassObj({['a b c']:scope['value']});
let c4 = [], p4 = {key:4,class:_3};
let vn4 = h('div', p4, c4);
return vn4;
}"
`;
exports[`qweb t-att t-att-class with multiple classes, some of which are duplicate 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let _1 = utils.toClassObj({'a b c':scope['value'],'a b d':!scope['value']});
let c2 = [], p2 = {key:2,class:_1};
let vn2 = h('div', p2, c2);
return vn2;
}"
`;
exports[`qweb t-att t-att-class with multiple classes, some of which are duplicate 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let _3 = utils.toClassObj({'a b c':scope['value'],'a b d':!scope['value']});
let c4 = [], p4 = {key:4,class:_3};
let vn4 = h('div', p4, c4);
return vn4;
}"
`;
@@ -1,71 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`qweb t-tag simple usecases 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let scope = Object.create(context);
let result;
let h = this.h;
let c1 = [], p1 = {key:1};
let tag2 = 'div';
let vn1 = h(tag2, p1, c1);
result = vn1;
return result;
}"
`;
exports[`qweb t-tag simple usecases 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let scope = Object.create(context);
let result;
let h = this.h;
let c3 = [], p3 = {key:3};
let tag4 = scope['tag'];
let vn3 = h(tag4, p3, c3);
result = vn3;
c3.push({text: \`text\`});
return result;
}"
`;
exports[`qweb t-tag with multiple attributes 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let scope = Object.create(context);
let result;
let h = this.h;
let _2 = {'blueberry':true};
let _3 = 'raspberry';
let c4 = [], p4 = {key:4,attrs:{taste: _3},class:_2};
let tag5 = scope['tag'];
let vn4 = h(tag5, p4, c4);
result = vn4;
c4.push({text: \`gooseberry\`});
return result;
}"
`;
exports[`qweb t-tag with multiple child nodes 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let scope = Object.create(context);
let result;
let h = this.h;
let c1 = [], p1 = {key:1};
let tag2 = scope['tag'];
let vn1 = h(tag2, p1, c1);
result = vn1;
c1.push({text: \` pear \`});
let c3 = [], p3 = {key:3};
let vn3 = h('span', p3, c3);
c1.push(vn3);
c3.push({text: \`apple\`});
c1.push({text: \` strawberry \`});
return result;
}"
`;
+4 -111
View File
@@ -1,5 +1,4 @@
import { QWeb } from "../../src/qweb/index";
import { config } from "../../src/index";
import { nextTick, normalize, renderToDOM, renderToString, trim } from "../helpers";
import { patch } from "../../src/vdom";
@@ -14,7 +13,6 @@ let qweb: QWeb;
beforeEach(() => {
QWeb.TEMPLATES = {};
QWeb.nextId = 1;
qweb = new QWeb();
});
@@ -33,21 +31,6 @@ describe("static templates", () => {
expect(renderToString(qweb, "test", { text: "hello vdom" })).toBe("hello vdom");
});
test("inline template string in t-esc", () => {
qweb.addTemplate("test", '<t><t t-esc="`text`"/></t>');
expect(renderToString(qweb, "test")).toBe("text");
});
test("inline template string with content in t-esc", () => {
qweb.addTemplate("test", '<t><t t-set="v" t-value="1"/><t t-esc="`text${v}`"/></t>');
expect(renderToString(qweb, "test")).toBe("text1");
});
test("inline template string with variable in context", () => {
qweb.addTemplate("test", '<t><t t-esc="`text ${v}`"/></t>');
expect(renderToString(qweb, "test", { v: "from context" })).toBe("text from context");
});
test("simple string, with some dynamic value", () => {
qweb.addTemplate("test", '<t>hello <t t-esc="text"/></t>');
expect(renderToString(qweb, "test", { text: "vdom" })).toBe("hello vdom");
@@ -255,13 +238,6 @@ describe("t-raw", () => {
"<span><span>hello</span><ok>world</ok></span>"
);
});
test("t-raw with comment", () => {
qweb.addTemplate("test", `<span><t t-raw="var"/></span>`);
expect(renderToString(qweb, "test", { var: "<p>text<!-- top secret --></p>" })).toBe(
"<span><p>text<!-- top secret --></p></span>"
);
});
});
describe("t-set", () => {
@@ -1112,26 +1088,6 @@ describe("t-call (template calling", () => {
const expected = "<div><p>yip yip</p></div>";
expect(renderToString(qweb, "main")).toBe(expected);
});
test("t-call with body content as root of a template", () => {
qweb.addTemplate("antony", `<foo><t t-raw="0"/></foo>`);
qweb.addTemplate("main", `<t><t t-call="antony"><p>antony</p></t></t>`);
const expected = "<foo><p>antony</p></foo>";
expect(renderToString(qweb, "main")).toBe(expected);
});
test("dynamic t-call", () => {
qweb.addTemplate("foo", `<foo><t t-esc="val"/></foo>`);
qweb.addTemplate("bar", `<bar><t t-esc="val"/></bar>`);
qweb.addTemplate("main", `<div><t t-call="{{template}}"/></div>`);
const expected = "<div><foo>foo</foo></div>";
expect(renderToString(qweb, "main", { template: "foo", val: "foo" })).toBe(expected);
const expected2 = "<div><bar>quux</bar></div>";
expect(renderToString(qweb, "main", { template: "bar", val: "quux" })).toBe(expected2);
// duplicate call because there was a specific bug with some id that was
// incremented each rendering.
expect(renderToString(qweb, "main", { template: "bar", val: "quux" })).toBe(expected2);
});
});
describe("foreach", () => {
@@ -1864,7 +1820,8 @@ describe("t-ref", () => {
describe("loading templates", () => {
test("can initialize qweb with a string", () => {
const templates = `<?xml version="1.0" encoding="UTF-8"?>
const templates = `
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<div t-name="hey">jupiler</div>
</templates>`;
@@ -1873,7 +1830,8 @@ describe("loading templates", () => {
});
test("can load a few templates from a xml string", () => {
const data = `<?xml version="1.0" encoding="UTF-8"?>
const data = `
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="items"><li>ok</li><li>foo</li></t>
@@ -1946,49 +1904,6 @@ describe("special cases for some specific html attributes/properties", () => {
let elm = vnode2.elm as HTMLInputElement;
expect(elm.indeterminate).toBe(true);
});
test("textarea with t-att-value", () => {
// render input with initial value
qweb.addTemplate("test", `<textarea t-att-value="v"/>`);
const vnode1 = qweb.render("test", { v: "zucchini" });
const vnode2 = patch(document.createElement("textarea"), vnode1);
let elm = vnode2.elm as HTMLInputElement;
expect(elm.value).toBe("zucchini");
// change value manually in textarea, to simulate user textarea
elm.value = "tomato";
expect(elm.value).toBe("tomato");
// rerender with a different value, and patch actual dom, to check that
// textarea value was properly reset by owl
const vnode3 = qweb.render("test", { v: "potato" });
patch(vnode2, vnode3);
expect(elm.value).toBe("potato");
});
test("select with t-att-value", () => {
const template = `
<select t-att-value="value">
<option value="potato">Potato</option>
<option value="tomato">Tomato</option>
<option value="onion">Onion</option>
</select>`;
qweb.addTemplate("test", template);
const vnode1 = qweb.render("test", { value: "tomato" });
const vnode2 = patch(document.createElement("select"), vnode1);
let elm = vnode2.elm as HTMLSelectElement;
expect(elm.value).toBe("tomato");
elm.value = "potato";
expect(elm.value).toBe("potato");
// rerender with a different value, and patch actual dom, to check that
// select value was properly reset by owl
const vnode3 = qweb.render("test", { value: "onion" });
patch(vnode2, vnode3);
expect(elm.value).toBe("onion");
expect(qweb.templates.test.fn.toString()).toMatchSnapshot();
});
});
describe("whitespace handling", () => {
@@ -2212,28 +2127,6 @@ describe("translation support", () => {
'<div><p label="mot">mot</p><p title="mot">mot</p><p placeholder="mot">mot</p><p alt="mot">mot</p><p something="word">mot</p></div>'
);
});
test("can add additional attributes to the list of translatable attributes", () => {
const translations = {
word: "mot",
};
const translateFn = (expr) => translations[expr] || expr;
const qweb = new QWeb({ translateFn });
config.translatableAttributes.push("potato");
qweb.addTemplate("test", `<div tomato="word" potato="word">text</div>`);
expect(renderToString(qweb, "test")).toBe('<div tomato="word" potato="mot">text</div>');
});
test("translation is done on the trimmed text, with extra spaces readded after", () => {
const translations = {
word: "mot",
};
const translateFn = jest.fn((expr) => translations[expr] || expr);
const qweb = new QWeb({ translateFn });
qweb.addTemplate("test", "<div> word </div>");
expect(renderToString(qweb, "test")).toBe("<div> mot </div>");
expect(translateFn).toHaveBeenCalledWith("word");
});
});
describe("t-key tests", () => {
-27
View File
@@ -196,31 +196,4 @@ describe("expression evaluation", () => {
expect(compileExpr("f(...state.list)", {})).toBe("scope['f'](...scope['state'].list)");
expect(compileExpr("f([...list])", {})).toBe("scope['f']([...scope['list']])");
});
test("works with builtin properties", () => {
expect(compileExpr("state.constructor.name", {})).toBe("scope['state'].constructor.name");
});
test("works with shortcut object key description", () => {
expect(compileExpr("{a}", {})).toBe("{a:scope['a']}");
expect(compileExpr("{a,b}", {})).toBe("{a:scope['a'],b:scope['b']}");
expect(compileExpr("{a,b:3,c}", {})).toBe("{a:scope['a'],b:3,c:scope['c']}");
});
test("works with short object description and lists ", () => {
expect(compileExpr("[a, b]", {})).toBe("[scope['a'],scope['b']]");
expect(compileExpr("[a, b, c]", {})).toBe("[scope['a'],scope['b'],scope['c']]");
expect(compileExpr("[a, {b, c},d]", {})).toBe(
"[scope['a'],{b:scope['b'],c:scope['c']},scope['d']]"
);
expect(compileExpr("{a:[b, {c, d: e}]}", {})).toBe(
"{a:[scope['b'],{c:scope['c'],d:scope['e']}]}"
);
});
test("template strings", () => {
expect(compileExpr("`hey`", {})).toBe("`hey`");
expect(compileExpr("`hey ${you}`", {})).toBe("`hey ${scope['you']}`");
expect(compileExpr("`hey ${1 + 2}`", {})).toBe("`hey ${1+2}`");
});
});
-14
View File
@@ -1,14 +0,0 @@
import { QWeb } from "../../src/qweb/index";
import { renderToString } from "../helpers";
describe("memory", () => {
test("t-foreach does not leak stuff in global scope", () => {
let qweb = new QWeb();
const initialNumberOfGlobals = Object.keys(window).length;
qweb.addTemplate("test", `<p><t t-foreach="[3, 2, 1]" t-as="item"><t t-esc="item"/></t></p>`);
const result = renderToString(qweb, "test");
const expected = `<p>321</p>`;
expect(result).toBe(expected);
expect(Object.keys(window).length).toBe(initialNumberOfGlobals);
});
});
-36
View File
@@ -1,36 +0,0 @@
import { QWeb } from "../../src/qweb/index";
import { renderToString } from "../helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
function render(template, context = {}) {
const qweb = new QWeb();
qweb.addTemplate("test", template);
return renderToString(qweb, "test", context);
}
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("qweb t-att", () => {
test("t-att-class with multiple classes", () => {
expect(render(`<div t-att-class="{'a b c': value}" />`, { value: true })).toBe(
'<div class="a b c"></div>'
);
expect(render(`<div t-att-class="{['a b c']: value}" />`, { value: true })).toBe(
'<div class="a b c"></div>'
);
});
test("t-att-class with multiple classes, some of which are duplicate", () => {
expect(render(`<div t-att-class="{'a b c': value, 'a b d': !value}" />`, { value: true })).toBe(
'<div class="a b c"></div>'
);
expect(
render(`<div t-att-class="{'a b c': value, 'a b d': !value}" />`, { value: false })
).toBe('<div class="a b d"></div>');
});
});
-42
View File
@@ -1,42 +0,0 @@
import { QWeb } from "../../src/qweb/index";
import { renderToString } from "../helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
function render(template, context = {}) {
const qweb = new QWeb();
qweb.addTemplate("test", template);
return renderToString(qweb, "test", context);
}
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("qweb t-tag", () => {
test("simple usecases", () => {
expect(render(`<t t-tag="'div'"></t>`)).toBe("<div></div>");
expect(render(`<t t-tag="tag">text</t>`, { tag: "span" })).toBe("<span>text</span>");
});
test("with multiple child nodes", () => {
const template = `
<t t-tag="tag">
pear
<span>apple</span>
strawberry
</t>`;
expect(render(template, { tag: "div" })).toBe(
"<div> pear <span>apple</span> strawberry </div>"
);
});
test("with multiple attributes", () => {
const template = `
<t t-tag="tag" class="blueberry" taste="raspberry">gooseberry</t>`;
const expected = `<div taste=\"raspberry\" class=\"blueberry\">gooseberry</div>`;
expect(render(template, { tag: "div" })).toBe(expected);
});
});
+10 -10
View File
@@ -7,16 +7,16 @@ exports[`Link component can render simple cases 1`] = `
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let _5 = utils.toClassObj({'router-link-active':scope['isActive']});
let _6 = scope['href'];
let c7 = [], p7 = {key:7,attrs:{href: _6},class:_5,on:{}};
let vn7 = h('a', p7, c7);
extra.handlers['click__8__'] = extra.handlers['click__8__'] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['navigate'](e);};
p7.on['click'] = extra.handlers['click__8__'];
const slot9 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot9) {
slot9.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c7, parent: extra.parent || context}));
let _4 = utils.toObj({'router-link-active':scope['isActive']});
let _5 = scope['href'];
let c6 = [], p6 = {key:6,attrs:{href: _5},class:_4,on:{}};
let vn6 = h('a', p6, c6);
extra.handlers['click__7__'] = extra.handlers['click__7__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['navigate'](e);};
p6.on['click'] = extra.handlers['click__7__'];
const slot8 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot8) {
slot8.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c6, parent: extra.parent || context}));
}
return vn7;
return vn6;
}"
`;
@@ -18,7 +18,7 @@ exports[`RouteComponent can render simple cases 1`] = `
let w4 = k5 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k5]] : false;
let vn6 = {};
result = vn6;
let props4 = Object.assign({}, scope['env'].router.currentParams, {});
let props4 = Object.assign({}, scope['env'].router.currentParams);
if (w4 && w4.__owl__.currentFiber && !w4.__owl__.vnode) {
w4.destroy();
w4 = false;
@@ -29,7 +29,7 @@ exports[`RouteComponent can render simple cases 1`] = `
utils.defineProxy(vn6, pvnode);
} else {
let componentKey4 = \`routeComponent\`;
let W4 = scope['routeComponent'] || context.constructor.components[componentKey4] || QWeb.components[componentKey4];
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4]|| scope['routeComponent'];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(parent, props4);
parent.__owl__.cmap[k5] = w4.__owl__.id;
+1 -1
View File
@@ -80,7 +80,7 @@ describe("Link component", () => {
await app.mount(fixture);
expect(window.location.pathname).toBe("/users");
var evt = new MouseEvent("contextmenu", {
var evt = new MouseEvent("click", {
button: 1,
});
-24
View File
@@ -103,28 +103,4 @@ describe("RouteComponent", () => {
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>Book 1984|124</span></div>");
});
test("can render parameterized route where params are not separated by slashes", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="App">
<RouteComponent />
</div>
<span t-name="Book">Book <t t-esc="props.title"/>|<t t-esc="props.val"/></span>
</templates>
`);
class Book extends Component {}
class App extends Component {
static components = { RouteComponent };
}
const routes = [
{ name: "book", path: "/#title={{title}}&val={{val.number}}", component: Book },
];
router = new TestRouter(env, routes, { mode: "hash" });
await router.navigate({ to: "book", params: { title: "1984", val: "123" } });
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>Book 1984|123</span></div>");
});
});
+23 -39
View File
@@ -1,6 +1,6 @@
import { Destination, RouterEnv, Route } from "../../src/router/router";
import { makeTestEnv, nextTick } from "../helpers";
import { TestRouter, getRouteParams } from "./test_router";
import { TestRouter } from "./test_router";
let env: RouterEnv;
let router: TestRouter | null = null;
@@ -60,13 +60,6 @@ describe("router miscellaneous", () => {
await router.navigate({ to: "users", params: { id: 3 } });
expect(window.location.href).toBe("http://localhost/test.html#/users/3");
});
test("navigate using path and query string should preserve query string", async () => {
router = new TestRouter(env, [{ name: "users", path: "/users/{{id}}" }]);
await router.navigate({ path: "/users/3?test=1" });
expect(window.location.pathname).toBe("/users/3");
expect(window.location.search).toBe("?test=1");
});
});
describe("routeToPath", () => {
@@ -114,69 +107,60 @@ describe("destToPath", () => {
describe("getRouteParams", () => {
test("properly match simple routes", () => {
router = new TestRouter(env, []);
// simple route
expect(getRouteParams({ path: "/home" }, "/home")).toEqual({});
expect(router["getRouteParams"]({ path: "/home" } as Route, "/home")).toEqual({});
// no match
expect(getRouteParams({ path: "/home" }, "/otherpath")).toEqual(false);
expect(router["getRouteParams"]({ path: "/home" } as Route, "/otherpath")).toEqual(false);
// fallback route
expect(getRouteParams({ path: "*" }, "somepath")).toEqual({});
});
test("properly match routes with query params", () => {
expect(getRouteParams({ path: "/home" }, "/home?test=1")).toEqual({});
expect(getRouteParams({ path: "/home" }, "/home?test1=1&test2=2")).toEqual({});
expect(router["getRouteParams"]({ path: "*" } as Route, "somepath")).toEqual({});
});
test("properly match simple routes, mode hash", () => {
router = new TestRouter(env, [], { mode: "hash" });
// simple route
expect(getRouteParams({ path: "/home" }, "#/home")).toEqual({});
expect(router["getRouteParams"]({ path: "/home" } as Route, "#/home")).toEqual({});
// no match
expect(getRouteParams({ path: "/home" }, "#/otherpath")).toEqual(false);
expect(router["getRouteParams"]({ path: "/home" } as Route, "#/otherpath")).toEqual(false);
// fallback route
expect(getRouteParams({ path: "*" }, "#/somepath")).toEqual({});
expect(router["getRouteParams"]({ path: "*" } as Route, "#/somepath")).toEqual({});
});
test("match some parameterized routes", () => {
expect(getRouteParams({ path: "/invoices/{{id}}" }, "/invoices/3")).toEqual({
router = new TestRouter(env, []);
expect(router["getRouteParams"]({ path: "/invoices/{{id}}" } as Route, "/invoices/3")).toEqual({
id: "3",
});
});
test("match some parameterized routes, mode hash", () => {
expect(getRouteParams({ path: "/invoices/{{id}}" }, "#/invoices/3")).toEqual({
id: "3",
});
router = new TestRouter(env, [], { mode: "hash" });
expect(router["getRouteParams"]({ path: "/invoices/{{id}}" } as Route, "#/invoices/3")).toEqual(
{
id: "3",
}
);
});
test("can convert to number if needed", () => {
expect(getRouteParams({ path: "/invoices/{{id.number}}" }, "/invoices/3")).toEqual({
router = new TestRouter(env, []);
expect(
router["getRouteParams"]({ path: "/invoices/{{id.number}}" } as Route, "/invoices/3")
).toEqual({
id: 3,
});
});
test("can convert to number if needed, mode: hash", () => {
expect(getRouteParams({ path: "/invoices/{{id.number}}" }, "#/invoices/3")).toEqual({
id: 3,
});
});
test("can extract params not separated by slashes", () => {
expect(getRouteParams({ path: "/books/{{id.number}}-{{name}}" }, "/books/3-1984")).toEqual({
id: 3,
name: "1984",
});
});
test("can extract params not separated by slashes, mode: hash", () => {
router = new TestRouter(env, [], { mode: "hash" });
expect(
getRouteParams({ path: "books&id={{id.number}}&name={{name}}" }, "#books&id=3&name=1984")
router["getRouteParams"]({ path: "/invoices/{{id.number}}" } as Route, "#/invoices/3")
).toEqual({
id: 3,
name: "1984",
});
});
});
+1 -12
View File
@@ -1,5 +1,4 @@
import { Router, Route, RouterEnv } from "../../src/router/router";
import { makeTestEnv } from "../helpers";
import { Router } from "../../src/router/router";
import { QWeb } from "../../src/qweb/index";
export class TestRouter extends Router {
@@ -14,13 +13,3 @@ export class TestRouter extends Router {
}
}
}
export function getRouteParams(route: Partial<Route>, path: string) {
const env = <RouterEnv>makeTestEnv();
const router = new TestRouter(env, [route]);
const {
routeIds: [routeId],
routes,
} = router;
return router["getRouteParams"](routes[routeId], path);
}
+3 -161
View File
@@ -1,4 +1,4 @@
import { Component, Env, mount } from "../src/component/component";
import { Component, Env } from "../src/component/component";
import { Store, useStore, useDispatch, useGetters, EnvWithStore } from "../src/store";
import { useState } from "../src/hooks";
import { xml } from "../src/tags";
@@ -571,12 +571,12 @@ describe("connecting a component to store", () => {
app.state.beerId = 2;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>kwak</span></div>");
expect(counter).toBe(0);
expect(counter).toBe(1);
store.dispatch("renameBeer", { id: 2, name: "orval" });
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>orval</span></div>");
expect(counter).toBe(1);
expect(counter).toBe(2);
});
test("connected component is properly cleaned up on destroy", async () => {
@@ -1241,162 +1241,4 @@ describe("various scenarios", () => {
await nextTick();
expect(fixture.innerHTML).toMatchSnapshot();
});
test("component with store, useState and shouldUpdate=false", async () => {
let state: any;
const store = new Store({ state: { rev: 0 } });
class Child extends Component {
static template = xml`<div><t t-esc="state.word"/><t t-esc="props.name"/></div>`;
state = useState({ word: "hello" });
constructor(parent, props) {
super(parent, props);
state = this.state;
useStore((props) => {
return 1;
});
}
shouldUpdate() {
return false;
}
}
class Parent extends Component {
static template = xml`<div><Child name="state.name"/></div>`;
static components = { Child };
state = useState({ name: "World" });
constructor(parent, props) {
super(parent, props);
useStore((props) => store.state.rev);
}
}
(env as any).store = store;
await mount(Parent, { target: fixture, env });
expect(fixture.innerHTML).toBe("<div><div>helloWorld</div></div>");
store.state.rev++;
// this is the key to the bug, it makes Parent be in "render" state but not
// yet rendered while the change of state happens
await Promise.resolve();
state.word = "test";
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>testWorld</div></div>");
});
test("component with store, useState, shouldUpdate=false and child with shouldupdate false", async () => {
let state: any;
const store = new Store({ state: { rev: 0 } });
class ChildChild extends Component {
static template = xml`<div><t t-esc="props.value"/></div>`;
shouldUpdate() {
return false;
}
}
class Child extends Component {
static template = xml`<div><t t-esc="state.word"/><t t-esc="props.name"/><ChildChild value="state.value"/></div>`;
static components = { ChildChild };
state = useState({ word: "hello", value: 3 });
constructor(parent, props) {
super(parent, props);
state = this.state;
useStore((props) => {
return 1;
});
}
shouldUpdate() {
return false;
}
}
class Parent extends Component {
static template = xml`<div><Child name="state.name"/></div>`;
static components = { Child };
state = useState({ name: "World" });
constructor(parent, props) {
super(parent, props);
useStore((props) => store.state.rev);
}
}
(env as any).store = store;
await mount(Parent, { target: fixture, env });
expect(fixture.innerHTML).toBe("<div><div>helloWorld<div>3</div></div></div>");
store.state.rev++;
// this is the key to the bug, it makes Parent be in "render" state but not
// yet rendered while the change of state happens
await Promise.resolve();
state.word = "test";
state.value = 44;
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>testWorld<div>3</div></div></div>");
});
test("parent/children with store, parent is remounted", async () => {
const store = new Store({ state: { a: 1, b: 1 } });
class Child extends Component {
static template = xml`<div><t t-esc="a"/></div>`;
a: any;
constructor(parent, props) {
super(parent, props);
this.a = useStore(
(state, props) => {
return state.a;
},
{
onUpdate: (a) => {
this.a = a;
},
}
);
}
}
class Parent extends Component {
static template = xml`
<div>
parent: <t t-esc="b"/>
<Child/>
</div>`;
static components = { Child };
b: any;
constructor(parent, props) {
super(parent, props);
this.b = useStore((state, props) => {
return state.b;
});
}
}
(env as any).store = store;
const div = document.createElement("div");
fixture.appendChild(div);
// initial mounting
const parent = await mount(Parent, { target: fixture, env });
expect(fixture.innerHTML).toBe("<div></div><div> parent: 1<div>1</div></div>");
// remounting component, then immediately update store.state
parent.mount(div);
store.state.a++;
await nextTick();
expect(fixture.innerHTML).toBe("<div><div> parent: 1<div>2</div></div></div>");
});
});
+2 -6
View File
@@ -8,7 +8,7 @@ import * as owl from "../../src/index";
import { Component, Env } from "../../src/component/component";
import { xml } from "../../src/tags";
import { makeTestFixture, makeTestEnv, nextTick } from "../helpers";
import { makeTestFixture, makeTestEnv } from "../helpers";
let fixture: HTMLElement = makeTestFixture();
let env: Env = makeTestEnv();
@@ -31,7 +31,6 @@ test("log a specific message for render method calls if component is not mounted
parent.unmount();
parent.state.value = 2;
await nextTick();
expect(steps).toEqual([
"[OWL_DEBUG] Parent<id=1> constructor, props={}",
"[OWL_DEBUG] Parent<id=1> mount",
@@ -41,10 +40,7 @@ test("log a specific message for render method calls if component is not mounted
"[OWL_DEBUG] Parent<id=1> mounted",
"[OWL_DEBUG] scheduler: stop running tasks queue",
"[OWL_DEBUG] Parent<id=1> willUnmount",
"[OWL_DEBUG] Parent<id=1> render (warning: component is not mounted)",
"[OWL_DEBUG] scheduler: start running tasks queue",
"[OWL_DEBUG] Parent<id=1> rendering template",
"[OWL_DEBUG] scheduler: stop running tasks queue",
"[OWL_DEBUG] Parent<id=1> render (warning: component is not mounted, this render has no effect)",
]);
console.log = log;
});
+2 -2
View File
@@ -101,8 +101,8 @@
component.render = function(...args) {
const __owl__ = component.__owl__;
let msg = `render`;
if (__owl__.status !== 3 /* mounted */ && !__owl__.currentFiber) {
msg += ` (warning: component is not mounted)`;
if (!__owl__.isMounted && !__owl__.currentFiber) {
msg += ` (warning: component is not mounted, this render has no effect)`;
}
log(msg);
return render(...args);
+256 -285
View File
@@ -1,24 +1,23 @@
import { SAMPLES } from "./samples.js";
const { mount, hooks } = owl;
const { useState, useRef, onMounted, onWillUnmount } = hooks;
import { SAMPLES as samples } from "./samples.js" ;
const { useState, useRef, onMounted, onWillUnmount } = owl.hooks;
//------------------------------------------------------------------------------
// Constants, helpers, utils
//------------------------------------------------------------------------------
let owlJS;
async function owlSourceCode() {
if (owlJS) {
if (owlJS) {
return owlJS;
}
const result = await fetch("../owl.js");
owlJS = await result.text();
return owlJS;
}
const result = await fetch("../owl.js");
owlJS = await result.text();
return owlJS;
}
const MODES = {
js: "ace/mode/javascript",
css: "ace/mode/css",
xml: "ace/mode/xml"
js: "ace/mode/javascript",
css: "ace/mode/css",
xml: "ace/mode/xml"
};
const DEFAULT_XML = `<templates>
@@ -72,22 +71,23 @@ if __name__ == "__main__":
* Make an iframe, with all the js, css and xml properly injected.
*/
function makeCodeIframe(js, css, xml) {
const sanitizedXML = xml.replace(/<!--[\s\S]*?-->/g, "").replace(/`/g, '\\\`');
const sanitizedXML = xml.replace(/<!--[\s\S]*?-->/g, "");
// create iframe
const iframe = document.createElement("iframe");
// create iframe
const iframe = document.createElement("iframe");
iframe.className += "h-full w-full";
iframe.onload = () => {
const doc = iframe.contentDocument;
// inject js
const owlScript = doc.createElement("script");
owlScript.type = "text/javascript";
owlScript.src = "../owl.js";
owlScript.addEventListener("load", () => {
const script = doc.createElement("script");
script.type = "text/javascript";
const content = `
iframe.onload = () => {
const doc = iframe.contentDocument;
// inject js
const owlScript = doc.createElement("script");
owlScript.type = "text/javascript";
owlScript.src = "../owl.js";
owlScript.addEventListener("load", () => {
const script = doc.createElement("script");
script.type = "text/javascript";
const content = `
{
owl.config.mode = 'dev';
let templates = \`${sanitizedXML}\`;
@@ -95,32 +95,32 @@ function makeCodeIframe(js, css, xml) {
owl.Component.env = { qweb };
}
${js}`;
script.innerHTML = content;
doc.body.appendChild(script);
});
doc.head.appendChild(owlScript);
script.innerHTML = content;
doc.body.appendChild(script);
});
doc.head.appendChild(owlScript);
// inject css
const style = document.createElement("style");
style.innerHTML = css;
doc.head.appendChild(style);
};
return iframe;
// inject css
const style = document.createElement("style");
style.innerHTML = css;
doc.head.appendChild(style);
};
return iframe;
}
/**
* Make a zip file containing a functioning application
*/
async function makeApp(js, css, xml) {
await owl.utils.loadJS("libs/jszip.min.js");
await owl.utils.loadJS("libs/jszip.min.js");
const zip = new JSZip();
const processedJS = js
.split("\n")
.map(l => (l === "" ? "" : " " + l))
.join("\n");
const zip = new JSZip();
const processedJS = js
.split("\n")
.map(l => (l === "" ? "" : " " + l))
.join("\n");
const JS = `
const JS = `
/**
* This is the javascript code defined in the playground.
* In a larger application, this code should probably be moved in different
@@ -151,277 +151,248 @@ async function start() {
start();
`;
zip.file("app.js", JS);
zip.file("app.css", css);
zip.file("app.py", APP_PY);
zip.file("app.xml", xml);
zip.file("index.html", DEFAULT_HTML);
zip.file("owl.js", owlSourceCode());
return zip.generateAsync({ type: "blob" });
zip.file("app.js", JS);
zip.file("app.css", css);
zip.file("app.py", APP_PY);
zip.file("app.xml", xml);
zip.file("index.html", DEFAULT_HTML);
zip.file("owl.js", owlSourceCode());
return zip.generateAsync({ type: "blob" });
}
//------------------------------------------------------------------------------
// SAMPLES
//------------------------------------------------------------------------------
function loadSamples() {
let result = SAMPLES.slice();
const localSample = localStorage.getItem("owl-playground-local-sample");
if (localSample) {
const { js, css, xml } = JSON.parse(localSample);
result.unshift({
description: "Local Storage Code",
code: js,
xml,
css
});
}
return result;
let result = samples.slice();
const localSample = localStorage.getItem("owl-playground-local-sample");
if (localSample) {
const { js, css, xml } = JSON.parse(localSample);
result.unshift({
description: "Local Storage Code",
code: js,
xml,
css
});
}
return result;
}
function saveLocalSample(js, css, xml) {
const str = JSON.stringify({ js, css, xml });
localStorage.setItem("owl-playground-local-sample", str);
const str = JSON.stringify({ js, css, xml });
localStorage.setItem("owl-playground-local-sample", str);
}
function deleteLocalSample() {
localStorage.removeItem("owl-playground-local-sample");
localStorage.removeItem("owl-playground-local-sample");
}
function useSamples() {
const samples = loadSamples();
const component = owl.Component.current;
let interval;
onMounted(() => {
const state = component.state;
interval = setInterval(() => {
if (component.isDirty) {
saveLocalSample(state.js, state.css, state.xml);
}
}, 1000);
});
onWillUnmount(() => {
clearInterval(interval);
});
return samples;
}
//------------------------------------------------------------------------------
// Tabbed editor
//------------------------------------------------------------------------------
class TabbedEditor extends owl.Component {
constructor(parent, props) {
super(parent, props);
this.state = useState({
currentTab: props.js !== false ? "js" : props.xml ? "xml" : "css"
});
this.setTab = owl.utils.debounce(this.setTab, 250, true);
this.sessions = {};
this._setupSessions(props);
this.editorNode = useRef("editor");
this._updateCode = this._updateCode.bind(this);
}
mounted() {
this.editor = this.editor || ace.edit(this.editorNode.el);
this.editor.setValue(this.props[this.state.currentTab], -1);
this.editor.setFontSize("12px");
this.editor.setTheme("ace/theme/monokai");
this.editor.setSession(this.sessions[this.state.currentTab]);
const tabSize = this.state.currentTab === "xml" ? 2 : 4;
this.editor.session.setOption("tabSize", tabSize);
this.editor.on("blur", this._updateCode);
this.interval = setInterval(this._updateCode, 3000);
}
willUnmount() {
clearInterval(this.interval);
this.editor.off("blur", this._updateCode);
}
willUpdateProps(nextProps) {
this._setupSessions(nextProps);
}
patched() {
const session = this.sessions[this.state.currentTab];
let content = this.props[this.state.currentTab];
if (content === false) {
const tab = this.props.js !== false ? "js" : this.props.xml ? "xml" : "css";
content = this.props[tab];
this.state.currentTab = tab;
}
if (this.editor.getValue() !== content) {
session.setValue(content, -1);
this.editor.setSession(session);
this.editor.resize();
}
}
setTab(tab) {
if (this.state.currentTab !== tab) {
this.state.currentTab = tab;
const session = this.sessions[this.state.currentTab];
session.doc.setValue(this.props[tab], -1);
this.editor.setSession(session);
}
}
onMouseDown(ev) {
if (ev.target.tagName === "DIV") {
let y = ev.clientY;
const resizer = ev => {
const delta = ev.clientY - y;
y = ev.clientY;
this.trigger("updatePanelHeight", { delta });
};
document.body.addEventListener("mousemove", resizer);
document.body.addEventListener("mouseup", () => {
document.body.removeEventListener("mousemove", resizer);
});
}
}
_setupSessions(props) {
for (let tab of ["js", "xml", "css"]) {
if (props[tab] !== false && !this.sessions[tab]) {
this.sessions[tab] = new ace.EditSession(props[tab], MODES[tab]);
this.sessions[tab].setOption("useWorker", false);
const tabSize = tab === "xml" ? 2 : 4;
this.sessions[tab].setOption("tabSize", tabSize);
this.sessions[tab].setUndoManager(new ace.UndoManager());
}
}
}
_updateCode() {
const editorValue = this.editor.getValue();
const propsValue = this.props[this.state.currentTab];
if (editorValue !== propsValue) {
this.trigger("updateCode", {
type: this.state.currentTab,
value: editorValue
});
}
}
return loadSamples();
}
//------------------------------------------------------------------------------
// MAIN APP
//------------------------------------------------------------------------------
class App extends owl.Component {
constructor(...args) {
super(...args);
this.version = owl.__info__.version;
this.SAMPLES = useSamples();
this.isDirty = false;
this.state = useState({
js: this.SAMPLES[0].code,
css: this.SAMPLES[0].css || "",
xml: this.SAMPLES[0].xml || DEFAULT_XML,
displayWelcome: true,
splitLayout: true,
leftPaneWidth: Math.ceil(window.innerWidth / 2),
topPanelHeight: null
});
this.toggleLayout = owl.utils.debounce(this.toggleLayout, 250, true);
this.runCode = owl.utils.debounce(this.runCode, 250, true);
this.downloadCode = owl.utils.debounce(this.downloadCode, 250, true);
this.content = useRef("content");
}
runCode() {
this.content.el.innerHTML = "";
this.state.displayWelcome = false;
const { js, css, xml } = this.state;
const subiframe = makeCodeIframe(js, css, xml);
this.content.el.appendChild(subiframe);
}
setSample(ev) {
const sample = this.SAMPLES.find(s => s.description === ev.target.value);
this.state.js = sample.code;
this.state.css = sample.css || "";
this.state.xml = sample.xml || DEFAULT_XML;
deleteLocalSample();
this.isDirty = false;
}
get leftPaneStyle() {
return `width:${this.state.leftPaneWidth}px`;
}
get rightPaneStyle() {
return `width:${window.innerWidth - 6 - this.state.leftPaneWidth}px`;
}
get topEditorStyle() {
return `flex: 0 0 ${this.state.topPanelHeight}px`;
}
onMouseDown() {
const resizer = ev => {
this.state.leftPaneWidth = ev.clientX;
};
document.body.addEventListener("mousemove", resizer);
for (let iframe of document.getElementsByTagName("iframe")) {
iframe.classList.add("disabled");
}
document.body.addEventListener("mouseup", () => {
document.body.removeEventListener("mousemove", resizer);
for (let iframe of document.getElementsByTagName("iframe")) {
iframe.classList.remove("disabled");
}
});
}
updateCode(ev) {
if (this.state[ev.detail.type] !== ev.detail.value) {
this.state[ev.detail.type] = ev.detail.value;
this.isDirty = true;
}
}
toggleLayout() {
this.state.splitLayout = !this.state.splitLayout;
}
updatePanelHeight(ev) {
if (!ev.detail.delta) {
return;
}
let height = this.state.topPanelHeight;
if (!height) {
height = document.getElementsByClassName("tabbed-editor")[0].clientHeight;
}
this.state.topPanelHeight = height + ev.detail.delta;
}
async downloadCode() {
const { js, css, xml } = this.state;
const content = await makeApp(js, css, xml);
await owl.utils.loadJS("libs/FileSaver.min.js");
saveAs(content, "app.zip");
}
class Tab extends owl.Component {
}
App.components = { TabbedEditor };
class Editor extends owl.Component {
static props = ['js', 'xml', 'css']
state = useState({
currentExtension: 'js',
js: this.props.js,
css: this.props.css,
xml: this.props.xml,
})
jsModel = null;
cssModel = null;
xmlModel = null;
editor = null;
willUpdateProps(nextProps) {
console.log(nextProps);
this.state.js = nextProps.js;
this.state.css = nextProps.css;
this.state.xml = nextProps.xml;
this.jsModel = null;
this.cssModel = null;
this.xmlModel = null;
this._openFile(this.state.currentExtension, false)
}
_run(ev) {
this._saveCodeState();
this.trigger('run-project', {
js: this.state.js,
xml: this.state.xml,
css: this.state.css
})
}
_openFile(extension, save = true) {
if (save) this._saveCodeState();
this.state.currentExtension = extension;
let language = null;
let model = null;
if (extension === 'js') {
console.log("here")
if (this.jsModel) {
model = this.jsModel;
}
else {
language = 'javascript'
}
}
if (extension === 'xml') {
if (this.xmlModel) {
model = this.xmlModel;
}
else {
language = 'xml'
}
}
if (extension === 'css') {
if (this.cssModel) {
model = this.cssModel;
}
else {
language = 'css'
}
}
console.log(model)
if (!model) {
model = monaco.editor.createModel(
this.state[this.state.currentExtension],
language,
null
)
if (extension === 'js') {
this.jsModel = model;
}
if (extension === 'xml') {
this.xmlModel = model;
}
if (extension === 'css') {
this.cssModel = model;
}
}
this.editor.setModel(model)
}
mounted() {
this.editor = monaco.editor.create(document.getElementById('container'), {
value: this.state[this.state.currentExtension],
language: 'javascript',
automaticLayout: true,
minimap: {
enabled: false
}
});
}
_saveCodeState() {
this.state[this.state.currentExtension] = this.editor.getValue();
}
_openCSS(ev) {
this._openFile('css')
}
_openXML(ev) {
this._openFile('xml')
}
_openJS(ev) {
this._openFile('js')
}
}
Editor.components = { Tab }
class App extends owl.Component {
samples = [];
constructor(...args) {
super(...args);
this.version = owl.__info__.version;
this.samples = useSamples();
this.state = useState({
js: this.samples[0].code,
css: this.samples[0].css || "",
xml: this.samples[0].xml || DEFAULT_XML,
});
this.runCode = owl.utils.debounce(this.runCode, 250, true);
this.downloadCode = owl.utils.debounce(this.downloadCode, 250, true);
this.content = useRef("iframe-container");
}
_run(ev) {
this.content.el.innerHTML = "";
const { js, css, xml } = ev.detail;
const subiframe = makeCodeIframe(js, css, xml);
this.content.el.appendChild(subiframe);
}
_setSample(ev) {
const sample = this.samples.find(s => s.description === ev.target.value);
this.state.js = sample.code;
this.state.css = sample.css || "";
this.state.xml = sample.xml || DEFAULT_XML;
deleteLocalSample();
}
}
App.components = { Editor }
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
document.title = `${document.title} (v${owl.__info__.version})`;
const commit = `https://github.com/odoo/owl/commit/${owl.__info__.hash}`;
console.info(`This application is using Owl built with the following commit:`, commit);
const [templates] = await Promise.all([
owl.utils.loadFile("templates.xml"),
owl.utils.whenReady()
]);
const qweb = new owl.QWeb({ templates });
const env = { qweb };
await mount(App, {target: document.body, env});
document.title = `${document.title} (v${owl.__info__.version})`;
const commit = `https://github.com/odoo/owl/commit/${owl.__info__.hash}`;
console.info(`This application is using Owl built with the following commit:`, commit);
const [templates] = await Promise.all([
owl.utils.loadFile("templates.xml"),
owl.utils.whenReady()
]);
const qweb = new owl.QWeb({ templates });
owl.Component.env = { qweb };
const app = new App();
app.mount(document.body);
}
start();
+21 -15
View File
@@ -1,20 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>OWL Playground</title>
<link rel="icon" href="data:,">
<script src="libs/ace.js" type="text/javascript" charset="utf-8"></script>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<link
rel="stylesheet"
data-name="vs/editor/editor.main"
href="../../node_modules/monaco-editor/min/vs/editor/editor.main.css"
/>
<script src="../owl.js"></script>
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/solid.css" integrity="sha384-QokYePQSOwpBDuhlHOsX0ymF6R/vLk/UQVz3WHa6wygxI5oGTmDTv8wahFOSspdm" crossorigin="anonymous">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/fontawesome.css" integrity="sha384-vd1e11sR28tEK9YANUtpIOdjGW14pS87bUBuOIoBILVWLFnS+MCX9T6MMf0VdPGq" crossorigin="anonymous">
<!-- Application JS/CSS -->
<link rel="stylesheet" href="playground.css">
<script type="module" src="app.js"></script>
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
</head>
<body class="h-screen bg-gray-200">
</head>
<body>
</body>
<script>
var require = { paths: { vs: '../../node_modules/monaco-editor/min/vs' } };
</script>
<script src="../../node_modules/monaco-editor/min/vs/loader.js"></script>
<script src="../../node_modules/monaco-editor/min/vs/editor/editor.main.nls.js"></script>
<script src="../../node_modules/monaco-editor/min/vs/editor/editor.main.js"></script>
</body>
</html>
+427
View File
@@ -0,0 +1,427 @@
import { SAMPLES } from "./samples.js";
const { useState, useRef, onMounted, onWillUnmount } = owl.hooks;
//------------------------------------------------------------------------------
// Constants, helpers, utils
//------------------------------------------------------------------------------
let owlJS;
async function owlSourceCode() {
if (owlJS) {
return owlJS;
}
const result = await fetch("../owl.js");
owlJS = await result.text();
return owlJS;
}
const MODES = {
js: "ace/mode/javascript",
css: "ace/mode/css",
xml: "ace/mode/xml"
};
const DEFAULT_XML = `<templates>
</templates>`;
const DEFAULT_HTML = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>OWL App</title>
<link rel="icon" href="data:,">
<script src="owl.js"></script>
<link rel="stylesheet" href="app.css">
<script src="app.js"></script>
</head>
<body>
</body>
</html>
`;
const APP_PY = `#!/usr/bin/env python3
import threading
import time
from http.server import SimpleHTTPRequestHandler, HTTPServer
def start_server():
SimpleHTTPRequestHandler.extensions_map['.js'] = 'application/javascript'
httpd = HTTPServer(('0.0.0.0', 3600), SimpleHTTPRequestHandler)
httpd.serve_forever()
url = 'http://127.0.0.1:3600'
if __name__ == "__main__":
print("Owl Application")
print("---------------")
print("Server running on: {}".format(url))
threading.Thread(target=start_server, daemon=True).start()
while True:
try:
time.sleep(1)
except KeyboardInterrupt:
httpd.server_close()
quit(0)
`;
/**
* Make an iframe, with all the js, css and xml properly injected.
*/
function makeCodeIframe(js, css, xml) {
const sanitizedXML = xml.replace(/<!--[\s\S]*?-->/g, "");
// create iframe
const iframe = document.createElement("iframe");
iframe.onload = () => {
const doc = iframe.contentDocument;
// inject js
const owlScript = doc.createElement("script");
owlScript.type = "text/javascript";
owlScript.src = "../owl.js";
owlScript.addEventListener("load", () => {
const script = doc.createElement("script");
script.type = "text/javascript";
const content = `
{
owl.config.mode = 'dev';
let templates = \`${sanitizedXML}\`;
const qweb = new owl.QWeb({ templates });
owl.Component.env = { qweb };
}
${js}`;
script.innerHTML = content;
doc.body.appendChild(script);
});
doc.head.appendChild(owlScript);
// inject css
const style = document.createElement("style");
style.innerHTML = css;
doc.head.appendChild(style);
};
return iframe;
}
/**
* Make a zip file containing a functioning application
*/
async function makeApp(js, css, xml) {
await owl.utils.loadJS("libs/jszip.min.js");
const zip = new JSZip();
const processedJS = js
.split("\n")
.map(l => (l === "" ? "" : " " + l))
.join("\n");
const JS = `
/**
* This is the javascript code defined in the playground.
* In a larger application, this code should probably be moved in different
* sub files.
*/
function app() {
${processedJS}
}
/**
* Initialization code
* This code load templates, and make sure everything is properly connected.
*/
async function start() {
let templates;
try {
templates = await owl.utils.loadFile('app.xml');
} catch(e) {
console.error(\`This app requires a static server. If you have python installed, try 'python app.py'\`);
return;
}
const env = { qweb: new owl.QWeb({templates})};
owl.Component.env = env;
await owl.utils.whenReady();
app();
}
start();
`;
zip.file("app.js", JS);
zip.file("app.css", css);
zip.file("app.py", APP_PY);
zip.file("app.xml", xml);
zip.file("index.html", DEFAULT_HTML);
zip.file("owl.js", owlSourceCode());
return zip.generateAsync({ type: "blob" });
}
//------------------------------------------------------------------------------
// SAMPLES
//------------------------------------------------------------------------------
function loadSamples() {
let result = SAMPLES.slice();
const localSample = localStorage.getItem("owl-playground-local-sample");
if (localSample) {
const { js, css, xml } = JSON.parse(localSample);
result.unshift({
description: "Local Storage Code",
code: js,
xml,
css
});
}
return result;
}
function saveLocalSample(js, css, xml) {
const str = JSON.stringify({ js, css, xml });
localStorage.setItem("owl-playground-local-sample", str);
}
function deleteLocalSample() {
localStorage.removeItem("owl-playground-local-sample");
}
function useSamples() {
const samples = loadSamples();
const component = owl.Component.current;
let interval;
onMounted(() => {
const state = component.state;
interval = setInterval(() => {
if (component.isDirty) {
saveLocalSample(state.js, state.css, state.xml);
}
}, 1000);
});
onWillUnmount(() => {
clearInterval(interval);
});
return samples;
}
//------------------------------------------------------------------------------
// Tabbed editor
//------------------------------------------------------------------------------
class TabbedEditor extends owl.Component {
constructor(parent, props) {
super(parent, props);
this.state = useState({
currentTab: props.js !== false ? "js" : props.xml ? "xml" : "css"
});
this.setTab = owl.utils.debounce(this.setTab, 250, true);
this.sessions = {};
this._setupSessions(props);
this.editorNode = useRef("editor");
this._updateCode = this._updateCode.bind(this);
}
mounted() {
this.editor = this.editor || ace.edit(this.editorNode.el);
this.editor.setValue(this.props[this.state.currentTab], -1);
this.editor.setFontSize("12px");
this.editor.setTheme("ace/theme/monokai");
this.editor.setSession(this.sessions[this.state.currentTab]);
const tabSize = this.state.currentTab === "xml" ? 2 : 4;
this.editor.session.setOption("tabSize", tabSize);
this.editor.on("blur", this._updateCode);
this.interval = setInterval(this._updateCode, 3000);
}
willUnmount() {
clearInterval(this.interval);
this.editor.off("blur", this._updateCode);
}
willUpdateProps(nextProps) {
this._setupSessions(nextProps);
}
patched() {
const session = this.sessions[this.state.currentTab];
let content = this.props[this.state.currentTab];
if (content === false) {
const tab = this.props.js !== false ? "js" : this.props.xml ? "xml" : "css";
content = this.props[tab];
this.state.currentTab = tab;
}
if (this.editor.getValue() !== content) {
session.setValue(content, -1);
this.editor.setSession(session);
this.editor.resize();
}
}
setTab(tab) {
if (this.state.currentTab !== tab) {
this.state.currentTab = tab;
const session = this.sessions[this.state.currentTab];
session.doc.setValue(this.props[tab], -1);
this.editor.setSession(session);
}
}
onMouseDown(ev) {
if (ev.target.tagName === "DIV") {
let y = ev.clientY;
const resizer = ev => {
const delta = ev.clientY - y;
y = ev.clientY;
this.trigger("updatePanelHeight", { delta });
};
document.body.addEventListener("mousemove", resizer);
document.body.addEventListener("mouseup", () => {
document.body.removeEventListener("mousemove", resizer);
});
}
}
_setupSessions(props) {
for (let tab of ["js", "xml", "css"]) {
if (props[tab] !== false && !this.sessions[tab]) {
this.sessions[tab] = new ace.EditSession(props[tab], MODES[tab]);
this.sessions[tab].setOption("useWorker", false);
const tabSize = tab === "xml" ? 2 : 4;
this.sessions[tab].setOption("tabSize", tabSize);
this.sessions[tab].setUndoManager(new ace.UndoManager());
}
}
}
_updateCode() {
const editorValue = this.editor.getValue();
const propsValue = this.props[this.state.currentTab];
if (editorValue !== propsValue) {
this.trigger("updateCode", {
type: this.state.currentTab,
value: editorValue
});
}
}
}
//------------------------------------------------------------------------------
// MAIN APP
//------------------------------------------------------------------------------
class App extends owl.Component {
constructor(...args) {
super(...args);
this.version = owl.__info__.version;
this.SAMPLES = useSamples();
this.isDirty = false;
this.state = useState({
js: this.SAMPLES[0].code,
css: this.SAMPLES[0].css || "",
xml: this.SAMPLES[0].xml || DEFAULT_XML,
displayWelcome: true,
splitLayout: true,
leftPaneWidth: Math.ceil(window.innerWidth / 2),
topPanelHeight: null
});
this.toggleLayout = owl.utils.debounce(this.toggleLayout, 250, true);
this.runCode = owl.utils.debounce(this.runCode, 250, true);
this.downloadCode = owl.utils.debounce(this.downloadCode, 250, true);
this.content = useRef("content");
}
runCode() {
this.content.el.innerHTML = "";
this.state.displayWelcome = false;
const { js, css, xml } = this.state;
const subiframe = makeCodeIframe(js, css, xml);
this.content.el.appendChild(subiframe);
}
setSample(ev) {
const sample = this.SAMPLES.find(s => s.description === ev.target.value);
this.state.js = sample.code;
this.state.css = sample.css || "";
this.state.xml = sample.xml || DEFAULT_XML;
deleteLocalSample();
this.isDirty = false;
}
get leftPaneStyle() {
return `width:${this.state.leftPaneWidth}px`;
}
get rightPaneStyle() {
return `width:${window.innerWidth - 6 - this.state.leftPaneWidth}px`;
}
get topEditorStyle() {
return `flex: 0 0 ${this.state.topPanelHeight}px`;
}
onMouseDown() {
const resizer = ev => {
this.state.leftPaneWidth = ev.clientX;
};
document.body.addEventListener("mousemove", resizer);
for (let iframe of document.getElementsByTagName("iframe")) {
iframe.classList.add("disabled");
}
document.body.addEventListener("mouseup", () => {
document.body.removeEventListener("mousemove", resizer);
for (let iframe of document.getElementsByTagName("iframe")) {
iframe.classList.remove("disabled");
}
});
}
updateCode(ev) {
if (this.state[ev.detail.type] !== ev.detail.value) {
this.state[ev.detail.type] = ev.detail.value;
this.isDirty = true;
}
}
toggleLayout() {
this.state.splitLayout = !this.state.splitLayout;
}
updatePanelHeight(ev) {
if (!ev.detail.delta) {
return;
}
let height = this.state.topPanelHeight;
if (!height) {
height = document.getElementsByClassName("tabbed-editor")[0].clientHeight;
}
this.state.topPanelHeight = height + ev.detail.delta;
}
async downloadCode() {
const { js, css, xml } = this.state;
const content = await makeApp(js, css, xml);
await owl.utils.loadJS("libs/FileSaver.min.js");
saveAs(content, "app.zip");
}
}
App.components = { TabbedEditor };
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
document.title = `${document.title} (v${owl.__info__.version})`;
const commit = `https://github.com/odoo/owl/commit/${owl.__info__.hash}`;
console.info(`This application is using Owl built with the following commit:`, commit);
const [templates] = await Promise.all([
owl.utils.loadFile("templates.xml"),
owl.utils.whenReady()
]);
const qweb = new owl.QWeb({ templates });
owl.Component.env = { qweb };
const app = new App();
app.mount(document.body);
}
start();
+21
View File
@@ -0,0 +1,21 @@
<!---->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>OWL Playground</title>
<link rel="icon" href="data:,">
<script src="libs/ace.js" type="text/javascript" charset="utf-8"></script>
<script src="../owl.js"></script>
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/solid.css" integrity="sha384-QokYePQSOwpBDuhlHOsX0ymF6R/vLk/UQVz3WHa6wygxI5oGTmDTv8wahFOSspdm" crossorigin="anonymous">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/fontawesome.css" integrity="sha384-vd1e11sR28tEK9YANUtpIOdjGW14pS87bUBuOIoBILVWLFnS+MCX9T6MMf0VdPGq" crossorigin="anonymous">
<!-- Application JS/CSS -->
<link rel="stylesheet" href="playground.css">
<script type="module" src="app.js"></script>
</head>
<body>
</body>
</html>
+57
View File
@@ -0,0 +1,57 @@
<templates>
<div t-name="TabbedEditor" class="tabbed-editor">
<div class="tabBar" t-att-class="{resizeable: props.resizeable}" t-on-mousedown="onMouseDown">
<t t-foreach="['js', 'xml', 'css']" t-as="tab">
<a t-ref="{{tab}}" t-if="props[tab] !== false" t-key="tab" class="tab flash" t-att-class="{active: state.currentTab===tab}" t-on-click="setTab(tab)">
<t t-esc="tab"/>
</a>
</t>
</div>
<div class="code-editor" t-ref="editor"></div>
</div>
<div t-name="App" class="playground">
<div class="left-bar" t-att-class="{split: state.splitLayout}"
t-att-style="leftPaneStyle"
t-on-updateCode="updateCode">
<div class="menubar">
<a class="btn run-code flash" t-on-click="runCode" title="Execute this Code">▶ Run</a>
<select t-on-change="setSample">
<option t-foreach="SAMPLES" t-as="sample" t-key="sample_index">
<t t-esc="sample.description"/>
</option>
</select>
<a class="btn flash" t-on-click="downloadCode" title="Download a Zip with this Code"><i class="fas fa-download"></i></a>
<a class="layout-selector flash" t-on-click="toggleLayout" title="Toggle Layout"><i class="fas" t-att-class="state.splitLayout ? 'fa-toggle-on' : 'fa-toggle-off'"></i></a>
</div>
<TabbedEditor
js="state.js"
css="!state.splitLayout and state.css"
xml="!state.splitLayout and state.xml"
t-att-style="topEditorStyle"/>
<t t-if="state.splitLayout">
<div class="separator horizontal"/>
<TabbedEditor
js="false"
css="state.css"
xml="state.xml"
resizeable="true"
t-on-updatePanelHeight="updatePanelHeight"/>
</t>
</div>
<div class="separator vertical" t-on-mousedown="onMouseDown"/>
<div class="right-pane" t-att-style="rightPaneStyle">
<div class="welcome" t-if="state.displayWelcome">
<div>🦉 Odoo Web Library 🦉</div>
<div>v<t t-esc="version"/></div>
<div class="url"><a href="https://github.com/odoo/owl">https://github.com/odoo/owl</a></div>
<div class="note">
<p>Note: these examples are using recent features of Javascript, and require a recent browser to work without a transpilation step!
For example, it makes use of class fields and class static fields. These examples should work in a recent Chrome version.
</p>
</div>
</div>
<div class="content" t-ref="content"/>
</div>
</div>
</templates>
+79 -49
View File
@@ -1,8 +1,9 @@
const COMPONENTS = `// In this example, we show how components can be defined and created.
const { Component, useState, mount } = owl;
const { Component, useState } = owl;
class Greeter extends Component {
setup() {
constructor() {
super(...arguments);
this.state = useState({ word: 'Hello' });
}
@@ -13,14 +14,16 @@ class Greeter extends Component {
// Main root component
class App extends Component {
setup() {
this.state = useState({ name: 'World'});
}
constructor() {
super(...arguments);
this.state = useState({ name: 'World'});
}
}
App.components = { Greeter };
// Application setup
mount(App, { target: document.body });
const app = new App();
app.mount(document.body);
`;
const COMPONENTS_XML = `<templates>
@@ -47,10 +50,11 @@ const COMPONENTS_CSS = `.greeter {
const ANIMATION = `// The goal of this component is to see how the t-transition directive can be
// used to generate simple transition effects.
const { Component, useState, mount } = owl;
const { Component, useState } = owl;
class Counter extends Component {
setup() {
constructor() {
super(...arguments);
this.state = useState({ value: 0 });
}
@@ -60,7 +64,8 @@ class Counter extends Component {
}
class App extends Component {
setup() {
constructor() {
super(...arguments);
this.state = useState({ flag: false, componentFlag: false, numbers: [] });
}
@@ -75,7 +80,8 @@ class App extends Component {
}
App.components = { Counter };
mount(App, { target: document.body });
const app = new App();
app.mount(document.body);
`;
const ANIMATION_XML = `<templates>
@@ -187,12 +193,13 @@ const LIFECYCLE_DEMO = `// This example shows all the possible lifecycle hooks
// methods in the console. Try modifying its state by clicking on it, or by
// clicking on the two main buttons, and look into the console to see what
// happens.
const { Component, useState, mount } = owl;
const { Component, useState } = owl;
class DemoComponent extends Component {
setup() {
constructor() {
super(...arguments);
this.state = useState({ n: 0 });
console.log("setup");
console.log("constructor");
}
async willStart() {
console.log("willstart");
@@ -218,7 +225,8 @@ class DemoComponent extends Component {
}
class App extends Component {
setup() {
constructor() {
super(...arguments);
this.state = useState({ n: 0, flag: true });
}
@@ -232,7 +240,8 @@ class App extends Component {
}
App.components = { DemoComponent };
mount(App, { target: document.body });
const app = new App();
app.mount(document.body);
`;
const LIFECYCLE_DEMO_XML = `<templates>
@@ -264,8 +273,7 @@ const LIFECYCLE_CSS = `button {
}`;
const HOOKS_DEMO = `// In this example, we show how hooks can be used or defined.
const { hooks, mount } = owl;
const {useState, onMounted, onWillUnmount} = hooks;
const {useState, onMounted, onWillUnmount} = owl.hooks;
// We define here a custom behaviour: this hook tracks the state of the mouse
// position
@@ -289,7 +297,8 @@ function useMouse() {
// Main root component
class App extends owl.Component {
setup() {
constructor() {
super(...arguments);
// simple state hook (reactive object)
this.counter = useState({ value: 0 });
@@ -303,7 +312,8 @@ class App extends owl.Component {
}
// Application setup
mount(App, { target: document.body });
const app = new App();
app.mount(document.body);
`;
const HOOKS_DEMO_XML = `<templates>
@@ -322,11 +332,12 @@ const HOOKS_CSS = `button {
const CONTEXT_JS = `// In this example, we show how components can use the Context and 'useContext'
// hook to share information between them.
const { Component, Context, mount } = owl;
const { Component, Context } = owl;
const { useContext } = owl.hooks;
class ToolbarButton extends Component {
setup() {
constructor() {
super(...arguments);
this.theme = useContext(this.env.themeContext);
}
@@ -356,7 +367,8 @@ const themeContext = new Context({
});
// Add the themeContext the environment to make it available to all components
App.env.themeContext = themeContext;
mount(App, { target: document.body });
const app = new App();
app.mount(document.body);
`;
const CONTEXT_XML = `<templates>
@@ -383,7 +395,7 @@ const TODO_APP_STORE = `// This example is an implementation of the TodoList app
//
// In this implementation, we use the owl Store class to manage the state. It
// is very similar to the VueX store.
const { Component, useState, mount } = owl;
const { Component, useState } = owl;
const { useRef, useStore, useDispatch, onPatched, onMounted } = owl.hooks;
//------------------------------------------------------------------------------
@@ -460,7 +472,8 @@ const actions = {
// TodoItem
//------------------------------------------------------------------------------
class TodoItem extends Component {
setup() {
constructor() {
super(...arguments);
useAutofocus("input");
this.state = useState({ isEditing: false });
this.dispatch = useDispatch();
@@ -490,7 +503,8 @@ class TodoItem extends Component {
// TodoApp
//------------------------------------------------------------------------------
class TodoApp extends Component {
setup() {
constructor() {
super(...arguments);
this.state = useState({ filter: "all" });
this.todos = useStore(state => state.todos);
this.dispatch = useDispatch();
@@ -554,7 +568,8 @@ function makeStore() {
}
TodoApp.env.store = makeStore();
mount(TodoApp, { target: document.body });
const app = new TodoApp();
app.mount(document.body);
`;
const TODO_APP_STORE_XML = `<templates>
@@ -1016,7 +1031,8 @@ class FormView extends owl.Component {}
FormView.components = { AdvancedComponent };
class Chatter extends owl.Component {
setup() {
constructor() {
super(...arguments);
this.messages = Array.from(Array(100).keys());
}
}
@@ -1044,7 +1060,8 @@ function setupResponsivePlugin(env) {
//------------------------------------------------------------------------------
setupResponsivePlugin(App.env);
owl.mount(App, { target: document.body });
const app = new App();
app.mount(document.body);
`;
const RESPONSIVE_XML = `<templates>
@@ -1156,10 +1173,11 @@ const SLOTS = `// We show here how slots can be used to create generic component
//
// Note that the t-on-click event, defined in the App template, is executed in
// the context of the App component, even though it is inside the Card component
const { Component, useState, mount } = owl;
const { Component, useState } = owl;
class Card extends Component {
setup() {
constructor() {
super(...arguments);
this.state = useState({ showContent: true });
}
@@ -1169,7 +1187,8 @@ class Card extends Component {
}
class Counter extends Component {
setup() {
constructor() {
super(...arguments);
this.state = useState({val: 1});
}
@@ -1180,7 +1199,8 @@ class Counter extends Component {
// Main root component
class App extends Component {
setup() {
constructor() {
super(...arguments);
this.state = useState({a: 1, b: 3});
}
@@ -1191,8 +1211,8 @@ class App extends Component {
App.components = {Card, Counter};
// Application setup
mount(App, { target: document.body });
`;
const app = new App();
app.mount(document.body);`;
const SLOTS_XML = `<templates>
<div t-name="Card" class="card" t-att-class="state.showContent ? 'full' : 'small'">
@@ -1278,7 +1298,7 @@ const ASYNC_COMPONENTS = `// This example will not work if your browser does not
// However, we don't want renderings of the other sub component to be delayed
// because of the slow component. We use the AsyncRoot component for this
// purpose. Try removing it to see the difference.
const { Component, useState, mount } = owl;
const { Component, useState } = owl;
const { AsyncRoot } = owl.misc;
class SlowComponent extends Component {
@@ -1292,7 +1312,8 @@ class SlowComponent extends Component {
class NotificationList extends Component {}
class App extends Component {
setup() {
constructor() {
super(...arguments);
this.state = useState({ value: 0, notifs: [] });
}
@@ -1308,7 +1329,8 @@ class App extends Component {
}
App.components = {SlowComponent, NotificationList, AsyncRoot};
mount(App, { target: document.body });
const app = new App();
app.mount(document.body);
`;
const ASYNC_COMPONENTS_XML = `<templates>
@@ -1363,10 +1385,11 @@ const FORM = `// This example illustrate how the t-model directive can be used t
// data between html inputs (and select/textareas) and the state of a component.
// Note that there are two controls with t-model="color": they are totally
// synchronized.
const { Component, useState, mount } = owl;
const { Component, useState } = owl;
class Form extends Component {
setup() {
constructor() {
super(...arguments);
this.state = useState({
text: "",
othertext: "",
@@ -1378,7 +1401,8 @@ class Form extends Component {
}
// Application setup
mount(Form, { target: document.body });
const form = new Form();
form.mount(document.body);
`;
const FORM_XML = `<templates>
@@ -1424,7 +1448,7 @@ const PORTAL_COMPONENTS = `
// This shows the expected use case of Portal
// which is to implement something similar
// to bootstrap modal
const { Component, useState, mount } = owl;
const { Component, useState } = owl;
const { Portal } = owl.misc;
class Modal extends Component {}
@@ -1446,7 +1470,8 @@ class App extends Component {
App.components = { Dialog , Interstellar };
// Application setup
mount(App, { target: document.body });
const app = new App();
app.mount(document.body);
`;
const PORTAL_XML = `
@@ -1534,13 +1559,14 @@ const WMS = `// This example is slightly more complex than usual. We demonstrate
// - minimal width/height
// - better heuristic for initial window position
// - ...
const { Component, useState, mount } = owl;
const { Component, useState } = owl;
const { useRef } = owl.hooks;
class HelloWorld extends Component {}
class Counter extends Component {
setup() {
constructor() {
super(...arguments);
this.state = useState({ value: 0 });
}
@@ -1593,7 +1619,8 @@ class Window extends Component {
}
class WindowManager extends Component {
setup() {
constructor() {
super(...arguments);
this.windows = [];
this.nextId = 1;
this.currentZindex = 1;
@@ -1643,7 +1670,8 @@ class WindowManager extends Component {
WindowManager.components = { Window };
class App extends Component {
setup() {
constructor() {
super(...arguments);
this.wmRef = useRef("wm");
}
@@ -1671,7 +1699,8 @@ const windows = [
];
App.env.windows = windows;
mount(App, { target: document.body });
const app = new App();
app.mount(document.body);
`;
const WMS_XML = `<templates>
@@ -1789,7 +1818,7 @@ const SFC = `// This example illustrates how Owl enables single file components,
// Note that this example has no external xml or css file, everything is
// contained in a single js file.
const { Component, useState, tags, mount } = owl;
const { Component, useState, tags } = owl;
const { xml, css } = tags;
// Counter component
@@ -1821,7 +1850,8 @@ App.template = APP_TEMPLATE;
App.components = { Counter };
// Application setup
mount(App, { target: document.body });
const app = new App();
app.mount(document.body);
`;
export const SAMPLES = [
+74 -52
View File
@@ -1,57 +1,79 @@
<templates>
<div t-name="TabbedEditor" class="tabbed-editor">
<div class="tabBar" t-att-class="{resizeable: props.resizeable}" t-on-mousedown="onMouseDown">
<t t-foreach="['js', 'xml', 'css']" t-as="tab">
<a t-ref="{{tab}}" t-if="props[tab] !== false" t-key="tab" class="tab flash" t-att-class="{active: state.currentTab===tab}" t-on-click="setTab(tab)">
<t t-esc="tab"/>
</a>
</t>
</div>
<div class="code-editor" t-ref="editor"></div>
</div>
<div t-name="App" class="playground">
<div class="left-bar" t-att-class="{split: state.splitLayout}"
t-att-style="leftPaneStyle"
t-on-updateCode="updateCode">
<div class="menubar">
<a class="btn run-code flash" t-on-click="runCode" title="Execute this Code">▶ Run</a>
<select t-on-change="setSample">
<option t-foreach="SAMPLES" t-as="sample" t-key="sample_index">
<t t-esc="sample.description"/>
</option>
</select>
<a class="btn flash" t-on-click="downloadCode" title="Download a Zip with this Code"><i class="fas fa-download"></i></a>
<a class="layout-selector flash" t-on-click="toggleLayout" title="Toggle Layout"><i class="fas" t-att-class="state.splitLayout ? 'fa-toggle-on' : 'fa-toggle-off'"></i></a>
<div t-name="App" t-on-run-project="_run" class="h-full flex">
<Editor js="state.js" xml="state.xml" css="state.css" />
<div class="flex flex-col w-1/2 h-full">
<div class="bg-purple-300 mx-2 my-1 p-2 rounded-sm" style="background-color: #865A7B">
<div class="flex items-center">
<div class="relative">
<select t-on-change="_setSample" class="block appearance-none w-full bg-gray-200 border border-gray-200 text-gray-700 py-1 px-2 pr-8 rounded-sm leading-tight focus:outline-none focus:bg-white focus:border-gray-500">
<t t-foreach="samples" t-as="sample" t-key="sample_index">
<option>
<t t-esc="sample.description"/>
</option>
</t>
</select>
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700">
<svg class="fill-current h-4 w-4"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"></path>
</svg>
</div>
</div>
</div>
</div>
<div t-ref="iframe-container" class="bg-white flex-grow m-2 rounded-sm shadow"></div>
</div>
<TabbedEditor
js="state.js"
css="!state.splitLayout and state.css"
xml="!state.splitLayout and state.xml"
t-att-style="topEditorStyle"/>
<t t-if="state.splitLayout">
<div class="separator horizontal"/>
<TabbedEditor
js="false"
css="state.css"
xml="state.xml"
resizeable="true"
t-on-updatePanelHeight="updatePanelHeight"/>
</t>
</div>
<div class="separator vertical" t-on-mousedown="onMouseDown"/>
<div class="right-pane" t-att-style="rightPaneStyle">
<div class="welcome" t-if="state.displayWelcome">
<div>🦉 Odoo Web Library 🦉</div>
<div>v<t t-esc="version"/></div>
<div class="url"><a href="https://github.com/odoo/owl">https://github.com/odoo/owl</a></div>
<div class="note">
<p>Note: these examples are using recent features of Javascript, and require a recent browser to work without a transpilation step!
For example, it makes use of class fields and class static fields. These examples should work in a recent Chrome version.
</p>
</div>
</div>
<t t-name="Editor" t-on-open-file="_openFile">
<div class="resize-x w-1/2">
<div class="flex justify-between items-center" style="background-color: #865A7B">
<ul class="flex justify-start">
<li>
<Tab t-on-click="_openXML">
xml
</Tab>
</li>
<li>
<Tab t-on-click="_openJS">
js
</Tab>
</li>
<li>
<Tab t-on-click="_openCSS">
css
</Tab>
</li>
</ul>
<ul class="flex justify-end m-2">
<li>
<button class="h-6 w-6 bg-white rounded-full p-1 shadow" t-on-click="_run">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</button>
</li>
</ul>
</div>
<div id="container" class="h-full"></div>
</div>
<div class="content" t-ref="content"/>
</div>
</div>
</t>
<t t-name="Tab">
<button class="bg-white hover:bg-gray-300 uppercase rounded-sm px-2 shadow mx-1">
<t t-slot="default"/>
</button>
</t>
</templates>
+65 -86
View File
@@ -3,11 +3,10 @@ const readline = require("readline");
const fs = require("fs");
const exec = require("child_process").exec;
const chalk = require("chalk");
const branchName = require('current-git-branch');
const GitHub = require("github-api");
const REL_NOTES_FILE = `release-notes.md`;
const STEPS = 8;
const branch = "master";
const STEPS = 10;
const rl = readline.createInterface({
input: process.stdin,
@@ -23,27 +22,13 @@ startRelease().then(() => {
// -----------------------------------------------------------------------------
async function startRelease() {
// First check we are on master
if (branchName() !== branch) {
logError(`You shall not pass! You are not on the ${branch} branch!`)
return;
}
log("Check if code formatting is right...")
const checkFormatting = await execCommand("npm run check-formatting");
if (checkFormatting !== 0) {
logError("Prettier format validation failed. Aborting.");
return;
}
log(`*** Owl release script ***`);
log(`Current Version: ${package.version}`);
// ---------------------------------------------------------------------------
log(`Step 1/${STEPS}: collecting info...`);
const current = package.version;
let next = await ask("Next version: ");
if (next[0] === 'v') next = next.substring(1);
const next = await ask("Next version: ");
let file = await ask(`Release notes (${REL_NOTES_FILE}): `);
file = file || REL_NOTES_FILE;
let content;
@@ -51,108 +36,93 @@ async function startRelease() {
content = await readFile("./" + file);
} catch (e) {
logSubContent(e.message);
logError("Cannot find release notes... Aborting");
log("Cannot find release notes... Aborting");
return;
}
let shouldBeDraft = await ask(`Should be a draft [y/n] ? (n)`);
let draft = ""
if (shouldBeDraft.toLowerCase() === 'y')
{
draft = "--draft";
}
let shouldUploadPlayground = await ask(`Should this release be uploaded on the playground [y/n] ? (y)`);
shouldUploadPlayground = shouldUploadPlayground.toLowerCase() !== 'n';
// 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) {
logError("Test suite does not pass. Aborting.");
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 escapedContent = content.replace(/\"/g, '\\\"').replace(/\`/g, '\\\`');
const gitResult = await execCommand(`git commit -am "[REL] v${next}\n\n${escapedContent}"`);
const gitResult = await execCommand(`git commit -am "[REL] v${next}\n\n${content}"`);
if (gitResult !== 0) {
logError("Git commit failed. Aborting.");
log("Git commit failed. Aborting.");
return;
}
await ask("Ready for next step...");
// ----------------------------------------------------------------------------
log(`Step 5/${STEPS}: building owl...`);
await execCommand("rm -rf dist/");
// -------------------j--------------------------------------------------------
log(`Step 5/${STEPS}: building owl (iife version)...`);
const buildResult = await execCommand("npm run build");
if (buildResult !== 0) {
logError("Build failed. Aborting.");
log("Build failed. Aborting.");
return;
}
await ask("Ready for next step...");
// ---------------------------------------------------------------------------
log(`Step 6/${STEPS}: pushing on github...`);
const pushResult = await execCommand("git push origin " + branch);
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) {
logError("git push failed. Aborting.");
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);
await ask("Ready for next step...");
log(`Step 7/${STEPS}: Creating the release...`);
const relaseResult = await execCommand(`gh release create v${next} dist/*.js ${draft} -F release-notes.md`);
if (relaseResult !== 0) {
logError("github release failed. Aborting.");
return;
}
// ---------------------------------------------------------------------------
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 8/${STEPS}: publishing module on npm...`);
// ---------------------------------------------------------------------------
log(`Step 10/${STEPS}: publishing module on npm...`);
await execCommand("npm run publish");
log("Owl Release process completed! Thank you for your patience");
await execCommand(`gh release view`);
await execCommand(`gh release view -w`);
if (shouldUploadPlayground) {
log(`Bonus step: publishing new release on playground...`);
let owl_code = null;
status = 0
try {
owl_code = await readFile("dist/owl.iife.js");
} catch (e) {
logSubContent(e.message);
logError("Cannot read owl.iife.js... Aborting");
return;
}
status += await execCommand("git checkout gh-pages");
if (status !== 0) {
logError("Couldn't switch to gh-pages branch")
return;
}
try {
fs.writeFileSync('owl.js', owl_code)
} catch (err) {
logError(err)
return;
}
status += await execCommand(`git commit -am "[IMP] update owl to v${next}"`);
status += await execCommand(`git push origin gh-pages`);
status += await execCommand("git checkout -");
if (status !== 0) {
logError("Something went wrong for the playground update.")
}
}
}
// -----------------------------------------------------------------------------
@@ -163,10 +133,6 @@ function log(text) {
console.log(chalk.yellow(formatLog(text)));
}
function logError(text) {
console.log(chalk.red(formatLog(text)));
}
function formatLog(text) {
return `[REL] ${text}`;
}
@@ -233,3 +199,16 @@ async function replaceInFile(file, from, to) {
});
});
}
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);
}
});
});
}
+2 -2
View File
@@ -14,9 +14,9 @@ URL = 'http://{0}:{1}/tools'.format(HOST, PORT)
# With this, we can simply copy the playground folder as is in the gh-page when
# we want to update the playground.
class OWLHandler(SimpleHTTPRequestHandler):
def do_GET(self):
def do_GET(self):
if self.path == '/tools/owl.js':
self.path = '/dist/owl.iife.js'
self.path = '/dist/owl.js'
return SimpleHTTPRequestHandler.do_GET(self)
def end_headers(self):
+17 -104
View File
@@ -1,107 +1,20 @@
{
/**
** Commented-out options have their default values.
**/
"include": [
"src/**/*.ts",
"src/*.ts"
],
                                                              // "exclude": [],
// "files": [],                   // A list of relative or absolute file paths to include.
// "extends": "",                   // A string containing a path to another configuration file to inherit from.
// "references": [],                   // An array of objects `{"path": "./to/dirOrConfig"}` that specifies projects to reference.
// "compileOnSave": false,                   // Signals to the IDE to generate all files for a given tsconfig.json upon saving.
"compilerOptions": {
                                                            // Main options
"target": "es2017",                                         // Specify ECMAScript target version: 'es3' (default), 'es5', 'es2015', 'es2016', 'es2017','es2018' or 'esnext'.
"module": "es6",                                         // Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'.
// "lib": ["esnext", "dom"],                 // Specify library files to be included in the compilation.
// "allowJs": false,                 // Allow javascript files to be compiled.
// "checkJs": false,                 // Report errors in .js files.
// "outFile": "./",                 // Concatenate and emit output to single file.
"outDir": "dist",                                           // Redirect output structure to the directory.
// "rootDir": "./",                 // Specify the root directory of input files. Use to control the output directory structure with `--outDir`.
// "project": "",                 // Compile a project given a valid configuration file.
                                                            // Compilation options
// "composite": true,                 // Enable project compilation
// "diagnostics": false,                 // Show diagnostic information.
// "incremental": true,                 // Enable incremental compilation by reading/writing information from prior compilations to a file on disk.
// "isolatedModules": false,                 // Transpile each file as a separate module (similar to 'ts.transpileModule').
// "listEmittedFiles": false,                 // Print names of generated files part of the compilation.
// "listFiles": true,                 // Print names of files part of the compilation.
// "noErrorTruncation": false,                 // Do not truncate error messages.
// "preserveWatchOutput": false,                 // Keep outdated console output in watch mode instead of clearing the screen.
// "traceResolution": false,                 // Enable tracing of the name resolution process.
// "tsBuildInfoFile": ".tsbuildinfo",                 // Specify file to store incremental compilation information.
                                                            // Strict typechecking options
// "strict": false,                                         // Enable all strict type-checking options.
// "noImplicitAny": true,                 // Raise error on expressions and declarations with an implied 'any' type.
// "noImplicitThis": true,                 // Raise error on 'this' expressions with an implied 'any' type.
// "strictBindCallApply": true,                 // Enable stricter checking of of the `bind`, `call`, and `apply` methods on functions.
// "strictFunctionTypes": true,                 // Disable bivariant parameter checking for function types.
// "strictNullChecks": true,                 // In strict null checking mode, the null and undefined values are not in the domain of every type and are only assignable to themselves and any.
// "strictPropertyInitialization": true,                 // Ensure non-undefined class properties are initialized in the constructor. This option requires `--strictNullChecks` be enabled in order to take effect.
// "alwaysStrict": true,                 // Parse in strict mode and emit "use strict" for each source file.
                                                            // Additional checks
// "allowUnreachableCode": false,                 // Do not report errors on unreachable code.
// "allowUnusedLabels": false,                 // Do not report errors on unused labels.
"forceConsistentCasingInFileNames": true,                   // Disallow inconsistently-cased references to the same file.
// "noStrictGenericChecks": false,                 // Disable strict checking of generic signatures in function types.
"noUnusedLocals": true,                                     // Report errors on unused locals.
"noUnusedParameters": false,                                // Report errors on unused parameters.
"noImplicitReturns": true,                                  // Report error when not all code paths in function return a value.
"noFallthroughCasesInSwitch": true,                         // Report errors for fallthrough cases in switch statement.
// "skipLibCheck": false,                 // Skip type checking of all declaration files (*.d.ts).
// "suppressExcessPropertyErrors": false,                 // Suppress excess property checks for object literals.
// "suppressImplicitAnyIndexErrors": false,                 // Suppress noImplicitAny errors for indexing objects lacking index signatures.
                                                            // Module resolution options
"moduleResolution": "node",                                 // Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6).
// "baseUrl": "./",                 // Base directory to resolve non-absolute module names.
// "paths": {},                 // A series of entries which re-map imports to lookup locations relative to the 'baseUrl'.
// "rootDirs": [],                 // List of root folders whose combined content represents the structure of the project at runtime.
// "typeRoots": [],                 // List of folders to include type definitions from.
"types": [
"jest",
"node"
],                                                // Type declaration files to be included in compilation.
// "allowSyntheticDefaultImports": false                    // Allow default imports from modules with no default export. This does not affect code emit, just typechecking.
"esModuleInterop": true,                  // Emit '__importStar' and '__importDefault' helpers for runtime babel ecosystem compatibility and enable '--allowSyntheticDefaultImports' for typesystem compatibility.
// "maxNodeModuleJsDepth": 0,                 // The maximum dependency depth to search under node_modules and load JavaScript files. Only applicable with --allowJs.
// "preserveSymlinks": false,                 // Do not resolve the real path of symlinks.
"resolveJsonModule": true,                                  // Include modules imported with '.json' extension.
                                                            // Emit options
"declaration": true,                                        // Generates corresponding '.d.ts' file.
"declarationDir": "dist/types",                             // Output directory for generated declaration files.
// "declarationMap": false,                 // Generates a sourcemap for each corresponding '.d.ts' file.
// "emitBOM": false,                 // Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files.
// "emitDeclarationOnly": false,                 // Only emit .d.ts declaration files.
// "importHelpers": false,                 // Import emit helpers from 'tslib'.
// "newLine": "LF",                 // Use the specified end of line sequence to be used when emitting files: "crlf" (windows) or "lf" (unix).
// "noEmit": true,                 // Do not emit outputs.
// "noEmitHelpers": false,                 // Do not generate custom helper functions like __extends in compiled output.
// "noEmitOnError": false,                 // Do not emit outputs if any errors were reported.
// "noImplicitUseStrict": false,                 // Do not emit "use strict" directives in module output.
// "noResolve": false,                 // Do not add triple-slash references or module import targets to the list of compiled files.
"preserveConstEnums": true,                                 // Do not erase const enum declarations in generated code.
// "removeComments": false,                 // Remove all comments except copy-right header comments beginning with
// "experimentalDecorators": true,                 // Enables experimental support for ES7 decorators.
// "emitDecoratorMetadata": true,                 // Enables experimental support for emitting type metadata for decorators.
                                                            // Source map options
// "sourceMap": false,                 // Generates corresponding '.map' file.
// "sourceRoot": "",                 // Specify the location where debugger should locate TypeScript files instead of source locations.
// "mapRoot": "",                 // Specify the location where debugger should locate map files instead of generated locations.
// "inlineSourceMap": true,                 // Emit a single file with source maps instead of having a separate file.
// "inlineSources": true,                 // Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set.
                                                            // JSX options
// "jsx": "preserve",                 // Specify JSX code generation: 'preserve', 'react-native', or 'react'.
// "jsxFactory": "React.createElement",                 // Specify the JSX factory function to use when targeting react JSX emit, e.g. 'React.createElement' or 'h'.
                                                            // Other options
// "allowUmdGlobalAccess": true,                 // Allow accessing UMD globals from modules.
// "charset": "utf8",                 // The character set of the input files.
// "downlevelIteration": false,                 // Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'.
// "disableSizeLimit": false,                 // Disable size limitation on JavaScript project.
// "keyofStringsOnly": false,                 // Resolve 'keyof' to string valued property names only (no numbers or symbols).
// "noLib": false,                 // Do not include the default library file (lib.d.ts).
// "pretty": true,                 // Stylize errors and messages using color and context.
}
"module": "commonjs",
"preserveConstEnums": true,
"noImplicitThis": true,
"removeComments": false,
"declaration": true,
"target": "esnext",
"outDir": "dist",
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": false,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"strictPropertyInitialization": true,
"strictNullChecks": true,
"declarationDir": "dist/types"
},
"include": ["src/**/*.ts","src/*.ts"]
}