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
61 changed files with 1630 additions and 1958 deletions
+1 -2
View File
@@ -23,5 +23,4 @@ jobs:
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- run: npm install - run: npm install
- run: npm run test - run: npm test
- run: npm run prettier
-2
View File
@@ -27,5 +27,3 @@ node_modules
/tools/owl.js /tools/owl.js
release-notes.md release-notes.md
.rpt2_cache
+5 -3
View File
@@ -41,7 +41,7 @@ find some more additional information [here](doc/miscellaneous/comparison.md).
Here is a short example to illustrate interactive components: Here is a short example to illustrate interactive components:
```javascript ```javascript
const { Component, useState, mount } = owl; const { Component, useState } = owl;
const { xml } = owl.tags; const { xml } = owl.tags;
class Counter extends Component { class Counter extends Component {
@@ -63,7 +63,8 @@ class App extends Component {
static components = { Counter }; 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). Note that the counter component is made reactive with the [`useState` hook](doc/reference/hooks.md#usestate).
@@ -120,7 +121,8 @@ npm install @odoo/owl
If you want to use a simple `<script>` tag, the last release can be downloaded here: If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-1.2.0](https://github.com/odoo/owl/releases/tag/v1.2.0) - [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 ## License
+2 -1
View File
@@ -85,7 +85,8 @@ afterEach(() => {
describe("SomeComponent", () => { describe("SomeComponent", () => {
test("component behaves as expected", async () => { test("component behaves as expected", async () => {
const props = {...}; // depends on the component 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 // do some assertions
expect(...).toBe(...); expect(...).toBe(...);
+1 -1
View File
@@ -96,7 +96,7 @@ class OrderLine extends Component {
</div>`; </div>`;
add() { 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: And `app.js` should look like this:
```js ```js
const { Component, mount } = owl; const { Component } = owl;
const { xml } = owl.tags; const { xml } = owl.tags;
const { whenReady } = owl.utils; const { whenReady } = owl.utils;
@@ -65,7 +65,8 @@ class App extends Component {
// Setup code // Setup code
function setup() { function setup() {
mount(App, target: { document.body }) const app = new App();
app.mount(document.body);
} }
whenReady(setup); whenReady(setup);
@@ -123,7 +124,7 @@ Here is the content of `app.js` and `main.js`:
```js ```js
// app.js ---------------------------------------------------------------------- // app.js ----------------------------------------------------------------------
const { Component, mount } = owl; const { Component } = owl;
const { xml } = owl.tags; const { xml } = owl.tags;
export class App extends Component { export class App extends Component {
@@ -134,7 +135,8 @@ export class App extends Component {
import { App } from "./app.js"; import { App } from "./app.js";
function setup() { function setup() {
mount(App, { target: document.body }); const app = new App();
app.mount(document.body);
} }
owl.utils.whenReady(setup); owl.utils.whenReady(setup);
@@ -238,11 +240,12 @@ export class App extends Component {
} }
// src/main.js ----------------------------------------------------------------- // src/main.js -----------------------------------------------------------------
import { utils, mount } from "@odoo/owl"; import { utils } from "@odoo/owl";
import { App } from "./components/App"; import { App } from "./components/App";
function setup() { function setup() {
mount(App, { target: document.body }); const app = new App();
app.mount(document.body);
} }
utils.whenReady(setup); utils.whenReady(setup);
@@ -250,7 +253,6 @@ utils.whenReady(setup);
// tests/components/App.test.js ------------------------------------------------ // tests/components/App.test.js ------------------------------------------------
import { App } from "../../src/components/App"; import { App } from "../../src/components/App";
import { makeTestFixture, nextTick, click } from "../helpers"; import { makeTestFixture, nextTick, click } from "../helpers";
import { mount } from "@odoo/owl";
let fixture; let fixture;
@@ -264,7 +266,8 @@ afterEach(() => {
describe("App", () => { describe("App", () => {
test("Works as expected...", async () => { 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>"); expect(fixture.innerHTML).toBe("<div>Hello Owl</div>");
click(fixture, "div"); click(fixture, "div");
+13 -8
View File
@@ -83,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: content of the function in `app.js` by the following code:
```js ```js
const { Component, mount } = owl; const { Component } = owl;
const { xml } = owl.tags; const { xml } = owl.tags;
const { whenReady } = owl.utils; const { whenReady } = owl.utils;
@@ -94,7 +94,8 @@ class App extends Component {
// Setup code // Setup code
function setup() { function setup() {
mount(App, { target: document.body }); const app = new App();
app.mount(document.body);
} }
whenReady(setup); whenReady(setup);
@@ -278,7 +279,8 @@ class App extends Component {
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
function setup() { function setup() {
owl.config.mode = "dev"; owl.config.mode = "dev";
mount(App, { target: document.body }); const app = new App();
app.mount(document.body);
} }
whenReady(setup); whenReady(setup);
@@ -637,7 +639,8 @@ function setup() {
owl.config.mode = "dev"; owl.config.mode = "dev";
const store = new Store({ actions, state: initialState }); const store = new Store({ actions, state: initialState });
App.env.store = store; App.env.store = store;
mount(App, { target: document.body }); const app = new App();
app.mount(document.body);
} }
whenReady(setup); whenReady(setup);
@@ -663,8 +666,9 @@ function makeStore() {
function setup() { function setup() {
owl.config.mode = "dev"; owl.config.mode = "dev";
const env = {store = makeStore()}; App.env.store = makeStore();
mount(App, { target: document.body, env }); const app = new App();
app.mount(document.body);
} }
``` ```
@@ -939,8 +943,9 @@ For reference, here is the final code:
function setup() { function setup() {
owl.config.mode = "dev"; owl.config.mode = "dev";
const env = {store = makeStore()}; App.env.store = makeStore();
mount(App, { target: document.body, env }); const app = new App();
app.mount(document.body);
} }
whenReady(setup); whenReady(setup);
+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 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: 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 since the assets are totally dynamic (and could change whenever the user install
or removes an addon), we need to have all that kind of tooling on the production or remove an addon), we need to have all that kind of tooling on the production
servers. This is certainly not ideal. servers. This is certainly not ideal.
Also, this makes it very complicated to setup Vue or React tools: Odoo code is 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 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 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 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!) (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 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 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 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. in any html page, with a simple `<script>` tag.
## Template based ## 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 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. important feature of odoo, and one of the key to Odoo modularity.
@@ -104,12 +104,12 @@ awkward, and very confusing.
## Developer Experience ## Developer Experience
This brings us to the following point: developer experience. We see this choice 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. easy as possible.
While many javascript professionals clearly think that react/vue is not difficult 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 (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, 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 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 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. 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. 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 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 transform a view description into a QWeb template, then compile that and use it
immediately. 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 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. 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 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 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. is a net performance loss, both for the CPU and the memory.
## Concurrency ## Concurrency
Many applications are happy to simply display a spinner whenever a new asynchronous 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 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 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). 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. fine, because they each chose a different set of tradeoffs.
However, we feel that there is still room in the framework world for something 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 🦉. 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. provided by Owl.
- [Animations](reference/animations.md) - [Animations](reference/animations.md)
- [Browser](reference/browser.md)
- [Component](reference/component.md) - [Component](reference/component.md)
- [Content](reference/content.md) - [Content](reference/content.md)
- [Concurrency Model](reference/concurrency_model.md) - [Concurrency Model](reference/concurrency_model.md)
@@ -28,7 +27,6 @@ provided by Owl.
- [Event Handling](reference/event_handling.md) - [Event Handling](reference/event_handling.md)
- [Error Handling](reference/error_handling.md) - [Error Handling](reference/error_handling.md)
- [Hooks](reference/hooks.md) - [Hooks](reference/hooks.md)
- [Mounting a component](reference/mounting.md)
- [Miscellaneous Components](reference/misc.md) - [Miscellaneous Components](reference/misc.md)
- [Observer](reference/observer.md) - [Observer](reference/observer.md)
- [Props](reference/props.md) - [Props](reference/props.md)
+9 -41
View File
@@ -52,20 +52,21 @@ sequence of events will happen:
At node insertion: At node insertion:
- the css classes `name-enter` and `name-enter-active` will be added directly - the css classes `name-enter` and `name-enter-active` will be added directly
when the node is inserted into the DOM. when the node is inserted into the DOM,
- on the next animation frame: the css class `name-enter` will be removed and the - on the next animation frame: the css class `name-enter` will be removed and the
class `name-enter-to` will be added (so they can be used to trigger css class `name-enter-to` will be added (so they can be used to trigger css
transition effects). transition effects),
- at the end of the transition, `name-enter-to` and `name-enter-active` will be removed. - the css class `name-enter-active` will be removed whenever a css transition
ends.
At node destruction: At node destruction:
- the css classes `name-leave` and `name-leave-active` will be added before the - the css classes `name-leave` and `name-leave-active` will be added before the
node is removed to the DOM. node is removed to the DOM,
- on the next animation frame: the css class `name-leave` will be removed and the - the css class `name-leave` will be removed on the next animation frame (so it
class `name-leave-to` will be added (so they can be used to trigger css can be used to trigger css transition effects),
transition effects). - the css class `name-leave-active` will be removed whenever a css transition
- at the end of the transition, `name-leave-to` and `name-leave-active` will be removed. ends. Only then will the element be removed from the DOM.
For example, a simple fade in/out effect can be done with this: For example, a simple fade in/out effect can be done with this:
@@ -92,36 +93,3 @@ Notes:
Owl does not support more than one transition on a single node, so the Owl does not support more than one transition on a single node, so the
`t-transition` expression must be a single value (i.e. no space allowed). `t-transition` expression must be a single value (i.e. no space allowed).
## SCSS Mixins
If you use SCSS, you can use mixins to make generic animations. Here is an exemple with a fade in / fade out animation:
```scss
@mixin animation-fade($time, $name) {
.#{$name}_fade-enter-active,
.#{$name}_fade-active {
transition: all $time;
}
.#{$name}_fade-enter {
opacity: 0;
}
.#{$name}_fade-leave-to {
opacity: 0;
}
}
```
Usage:
```scss
@include animation-fade(0.5s, "o_notification");
```
You can now have in your template:
```xml
<SomeTag t-transition="o_notification_fade"/>
```
-33
View File
@@ -1,33 +0,0 @@
# 🦉 Browser 🦉
## Content
- [Overview](#overview)
- [Browser Content](#browser-content)
## Overview
The browser object contains some browser native APIs, such as `setTimeout`, that
are used by Owl and its utility functions. They are exposed with the intent of
making them mockable if necessary.
```js
owl.browser.setTimeout === window.setTimeout; // return true
```
For now, this object contains some functions that are not used by Owl. They
will eventually be removed in Owl 2.0.
## Browser Content
More specifically, the `browser` object contains the following methods and objects:
- `setTimeout`
- `clearTimeout`
- `setInterval`
- `clearInterval`
- `requestAnimationFrame`
- `random`
- `Date`
- `fetch`
- `localStorage`
-4
View File
@@ -270,10 +270,6 @@ We explain here all the public methods of the `Component` class.
// app is now visible // 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 * **`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 method can be used. Most applications should not call `unmount`, this is more
useful to the underlying component system. useful to the underlying component system.
+2 -19
View File
@@ -2,10 +2,9 @@
The Owl framework is designed to work in many situations. However, it is 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 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`), - [`mode`](#mode).
- [`enableTransitions`](#enabletransitions) (default value: `true`).
## 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 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. 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`. exported as `owl.core.EventBus`.
``` ```
browser
Component misc Component misc
Context AsyncRoot Context AsyncRoot
QWeb Portal QWeb Portal
mount router Store router
Store Link useState Link
useState RouteComponent config RouteComponent
config Router mode Router
mode
core tags core tags
EventBus css EventBus css
Observer xml Observer xml
@@ -29,8 +27,6 @@ hooks utils
useContext useContext
useState useState
useRef useRef
useComponent
useEnv
useSubEnv useSubEnv
useStore useStore
useDispatch 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: component class, before the first component is created:
```js ```js
const env = { App.env = {
_t: myTranslateFunction, _t: myTranslateFunction,
user: {...}, user: {...},
services: { 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, It is also possible to simply share an environment between all root components,
@@ -120,8 +121,9 @@ async function myEnv() {
} }
async function start() { async function start() {
const env = await myEnv(); App.env = await myEnv();
mount(App, { target: document.body, env }); 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 - `qweb` will be set to an empty `QWeb` instance. This is absolutely necessary
for Owl to be able to render anything for Owl to be able to render anything
- `browser`: this is an object that contains some common access points to the - `browser`: this is an object that contains some common access points to the
browser methods with a side effect. See [browser](browser.md) for more information. Note that the browser object will be removed from the environment in Owl 2.0. browser methods with a side effect. This is particularly useful when one want
to test more advanced components, and be able to mock those methods.
More specifically, the `browser` object contains the following methods and objects:
- `setTimeout`
- `clearTimeout`
- `setInterval`
- `clearInterval`
- `requestAnimationFrame`
- `random`
- `Date`
- `fetch`
- `localStorage`
-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 Here, `expr` is a valid Owl expression, so it could be `true` or some variable
from the rendering context. 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 ## Inline Event Handlers
One can also directly specify inline statements. For example, One can also directly specify inline statements. For example,
+4 -14
View File
@@ -21,8 +21,6 @@
- [`useStore`](#usestore) - [`useStore`](#usestore)
- [`useDispatch`](#usedispatch) - [`useDispatch`](#usedispatch)
- [`useGetters`](#usegetters) - [`useGetters`](#usegetters)
- [`useComponent`](#usecomponent)
- [`useEnv`](#useenv)
- [Making customized hooks](#making-customized-hooks) - [Making customized hooks](#making-customized-hooks)
## Overview ## Overview
@@ -383,16 +381,6 @@ The `useDispatch` hook is the way for components to get a reference to the store
The `useGetters` hook is the way for components to get a reference to the store The `useGetters` hook is the way for components to get a reference to the store
getters. See the [store documentation](store.md) for more information. getters. See the [store documentation](store.md) for more information.
### `useComponent`
The `useComponent` hook is useful as a building block for some customized hooks,
that may need a reference to the component calling them.
### `useEnv`
The `useEnv` hook is useful as a building block for some customized hooks,
that may need a reference to the env of the component calling them.
### Making customized hooks ### Making customized hooks
Hooks are a wonderful way to organize the code of a complex component by feature Hooks are a wonderful way to organize the code of a complex component by feature
@@ -447,11 +435,13 @@ not the solution to every problem.
```js ```js
function useRouter() { function useRouter() {
const env = useEnv(); return Component.current.env.router;
return env.router;
} }
``` ```
This means that we give control to the application developer to create the This means that we give control to the application developer to create the
router, which is good, so they can set it up, subclass it, ... And then, to router, which is good, so they can set it up, subclass it, ... And then, to
test our components, we can just add a mock router in the environment. test our components, we can just add a mock router in the environment.
Note: the code above makes use of the `Component.current` property. This is the
way hooks are able to get a reference to the component currently being created.
+3 -2
View File
@@ -43,7 +43,7 @@ workflow to help the user put in some data, which it could use later on.
JavaScript: JavaScript:
```js ```js
const { Component, mount } = owl; const { Component } = owl;
const { Portal } = owl.misc; const { Portal } = owl.misc;
class TeleportedComponent extends Component {} class TeleportedComponent extends Component {}
@@ -51,7 +51,8 @@ class App extends Component {
static components = { Portal, TeleportedComponent }; static components = { Portal, TeleportedComponent };
} }
mount(App, { target: document.body }); const app = new App();
app.mount(document.body);
``` ```
XML: 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.
+1 -1
View File
@@ -18,7 +18,7 @@ class Child extends Component {
} }
class Parent 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 }; static components = { Child };
state = useState({ a: "fromparent" }); state = useState({ a: "fromparent" });
} }
-28
View File
@@ -15,7 +15,6 @@
- [Dynamic Attributes](#dynamic-attributes) - [Dynamic Attributes](#dynamic-attributes)
- [Loops](#loops) - [Loops](#loops)
- [Rendering Sub Templates](#rendering-sub-templates) - [Rendering Sub Templates](#rendering-sub-templates)
- [Dynamic Sub Templates](#dynamic-sub-templates)
- [Translations](#translations) - [Translations](#translations)
- [Debugging](#debugging) - [Debugging](#debugging)
@@ -453,17 +452,6 @@ are all equivalent:
If there is no `t-key` directive, Owl will use the index as a default key. 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 ### Rendering Sub Templates
QWeb templates can be used for top level rendering, but they can also be used QWeb templates can be used for top level rendering, but they can also be used
@@ -521,22 +509,6 @@ This can be used to define variables scoped to a sub template:
<!-- "var" does not exist here --> <!-- "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 ### Translations
By default, QWeb specify that templates should be translated. If this behaviour By default, QWeb specify that templates should be translated. If this behaviour
+2 -17
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 will need to render some content, some footer, but with the parent as the
rendering context. rendering context.
Slots are inserted with the `t-slot` directive:
```xml ```xml
<div t-name="Dialog" class="modal"> <div t-name="Dialog" class="modal">
<div class="modal-title"><t t-esc="props.title"/></div> <div class="modal-title"><t t-esc="props.title"/></div>
@@ -64,9 +62,7 @@ This is deprecated and should no longer be used in new code.
## Reference ## Reference
### Default Slot Default slot: the first element inside the component which is not a named slot will
The first element inside the component which is not a named slot will
be considered the `default` slot. For example: be considered the `default` slot. For example:
```xml ```xml
@@ -81,9 +77,7 @@ be considered the `default` slot. For example:
</div> </div>
``` ```
### Default content Default content: slots can define a default content, in case the parent did not define them:
Slots can define a default content, in case the parent did not define them:
```xml ```xml
<div t-name="Parent"> <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 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 positioned. This allows the user to define event handlers that will be bound
to the correct component (usually, the grandparent of the slot content). 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}}" />
```
+4 -4
View File
@@ -21,8 +21,8 @@ argument, it executes it as soon as the DOM ready (or directly).
```js ```js
Promise.all([loadFile("templates.xml"), owl.utils.whenReady()]).then(function ([templates]) { Promise.all([loadFile("templates.xml"), owl.utils.whenReady()]).then(function ([templates]) {
const qweb = new owl.QWeb({ templates }); const qweb = new owl.QWeb({ templates });
const env = { qweb }; const app = new App({ qweb });
await mount(App, { env, target: document.body }); app.mount(document.body);
}); });
``` ```
@@ -31,8 +31,8 @@ or alternatively:
```js ```js
owl.utils.whenReady(function () { owl.utils.whenReady(function () {
const qweb = new owl.QWeb(); const qweb = new owl.QWeb();
const env = { qweb }; const app = new App({ qweb });
await mount(App, { env, target: document.body }); app.mount(document.body);
}); });
``` ```
+14 -11
View File
@@ -1,20 +1,23 @@
{ {
"name": "@odoo/owl", "name": "@odoo/owl",
"version": "1.2.0", "version": "1.0.11",
"description": "Odoo Web Library (OWL)", "description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js", "main": "dist/owl.js",
"browser": "dist/owl.iife.js",
"module": "dist/owl.es.js",
"types": "dist/types/index.d.ts", "types": "dist/types/index.d.ts",
"files": [ "files": [
"dist" "dist/types/",
"dist/owl.js",
"dist/owl-iife.js"
], ],
"engines": { "engines": {
"node": ">=10.15.3" "node": ">=10.15.3"
}, },
"scripts": { "scripts": {
"build:js": "tsc --target esnext --module es6 --outDir dist/owl",
"build:bundle": "rollup -c", "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": "jest",
"test:watch": "jest --watch", "test:watch": "jest --watch",
"tools:serve": "python3 tools/server.py || python tools/server.py", "tools:serve": "python3 tools/server.py || python tools/server.py",
@@ -22,7 +25,7 @@
"pretools:watch": "npm run build", "pretools:watch": "npm run build",
"tools:watch": "npm-run-all --parallel tools:serve \"build:* -- --watch\"", "tools:watch": "npm-run-all --parallel tools:serve \"build:* -- --watch\"",
"prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --write", "prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --write",
"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" "release": "node tools/release.js"
}, },
"repository": { "repository": {
@@ -35,9 +38,9 @@
"url": "https://github.com/odoo/owl/issues" "url": "https://github.com/odoo/owl/issues"
}, },
"homepage": "https://github.com/odoo/owl#readme", "homepage": "https://github.com/odoo/owl#readme",
"dependencies": {},
"devDependencies": { "devDependencies": {
"@types/jest": "^23.3.14", "@types/jest": "^23.3.12",
"@types/node": "^14.11.8",
"chalk": "^3.0.0", "chalk": "^3.0.0",
"cpx": "^1.5.0", "cpx": "^1.5.0",
"git-rev-sync": "^1.12.0", "git-rev-sync": "^1.12.0",
@@ -45,11 +48,11 @@
"jest": "^23.6.0", "jest": "^23.6.0",
"jest-environment-jsdom": "^24.7.1", "jest-environment-jsdom": "^24.7.1",
"live-server": "^1.2.1", "live-server": "^1.2.1",
"monaco-editor": "^0.21.2",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^2.0.4", "prettier": "^2.0.4",
"rollup": "^1.6.0", "rollup": "^1.6.0",
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-typescript2": "^0.20.1",
"rollup-plugin-typescript2": "^0.27.3",
"sass": "^1.16.1", "sass": "^1.16.1",
"source-map-support": "^0.5.10", "source-map-support": "^0.5.10",
"ts-jest": "^23.10.5", "ts-jest": "^23.10.5",
+1 -1
View File
@@ -1,6 +1,6 @@
# 🦉 OWL Roadmap 🦉 # 🦉 OWL Roadmap 🦉
- Current version: 1.2.0 - Current version: 1.0.11
- Status: stable - Status: stable
This roadmap is only an attempt at predicting Owl's future. Everything may This roadmap is only an attempt at predicting Owl's future. Everything may
+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 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 { export default {
input: "src/index.ts", input: "dist/owl/index.js",
output: [ output: {
file: "dist/owl.js",
/** format: "iife",
* Read about module formats: name: "owl",
* https://auth0.com/blog/javascript-module-systems-showdown/ extend: true,
* https://medium.com/@kelin2025/so-you-wanna-use-es6-modules-714f48b3a953 outro: `exports.__info__.version = '${version}';\nexports.__info__.date = '${new Date().toISOString()}';\nexports.__info__.hash = '${git.short()}';\nexports.__info__.url = 'https://github.com/odoo/owl';`
*/ }
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
}),
]
}; };
+1 -8
View File
@@ -10,8 +10,6 @@ export interface Browser {
localStorage: Window["localStorage"]; localStorage: Window["localStorage"];
} }
let localStorage: Window["localStorage"] | null = null;
export const browser: Browser = { export const browser: Browser = {
setTimeout: window.setTimeout.bind(window), setTimeout: window.setTimeout.bind(window),
clearTimeout: window.clearTimeout.bind(window), clearTimeout: window.clearTimeout.bind(window),
@@ -21,10 +19,5 @@ export const browser: Browser = {
random: Math.random, random: Math.random,
Date: window.Date, Date: window.Date,
fetch: (window.fetch || (() => {})).bind(window), fetch: (window.fetch || (() => {})).bind(window),
get localStorage() { localStorage: window.localStorage,
return localStorage || window.localStorage;
},
set localStorage(newLocalStorage: Window["localStorage"]) {
localStorage = newLocalStorage;
},
}; };
+5 -37
View File
@@ -159,7 +159,6 @@ export class Component<Props extends {} = any, T extends Env = Env> {
if (!this.env.qweb) { if (!this.env.qweb) {
this.env.qweb = new QWeb(); this.env.qweb = new QWeb();
} }
// TODO: remove this in owl 2.0
if (!this.env.browser) { if (!this.env.browser) {
this.env.browser = browser; this.env.browser = browser;
} }
@@ -333,7 +332,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
message += `\nMaybe the DOM is not ready yet? (in that case, you can use owl.utils.whenReady)`; message += `\nMaybe the DOM is not ready yet? (in that case, you can use owl.utils.whenReady)`;
throw new Error(message); throw new Error(message);
} }
const fiber = new Fiber(null, this, true, target, position); const fiber = new Fiber(null, this, false, target, position);
fiber.shouldPatch = false; fiber.shouldPatch = false;
if (!__owl__.vnode) { if (!__owl__.vnode) {
this.__prepareAndRender(fiber, () => {}); this.__prepareAndRender(fiber, () => {});
@@ -434,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 * up to the parent DOM nodes. Thus, it must be called between mounted() and
* willUnmount(). * willUnmount().
*/ */
trigger<T = any>(eventType: string, payload?: T) { trigger(eventType: string, payload?: any) {
this.__trigger<T>(this, eventType, payload); this.__trigger(this, eventType, payload);
} }
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
@@ -513,9 +512,9 @@ export class Component<Props extends {} = any, T extends Env = Env> {
* Private trigger method, allows to choose the component which triggered * Private trigger method, allows to choose the component which triggered
* the event in the first place * the event in the first place
*/ */
__trigger<T>(component: Component, eventType: string, payload?: T) { __trigger(component: Component, eventType: string, payload?: any) {
if (this.el) { if (this.el) {
const ev = new OwlEvent<T>(component, eventType, { const ev = new OwlEvent(component, eventType, {
bubbles: true, bubbles: true,
cancelable: true, cancelable: true,
detail: payload, detail: payload,
@@ -730,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;
}
+3 -6
View File
@@ -227,9 +227,7 @@ QWeb.addDirective({
if (name.startsWith("t-on-")) { if (name.startsWith("t-on-")) {
events.push([name, value]); events.push([name, value]);
} else if (name === "t-transition") { } else if (name === "t-transition") {
if (QWeb.enableTransitions) { transition = value;
transition = value;
}
} else if (!name.startsWith("t-")) { } else if (!name.startsWith("t-")) {
if (name !== "class" && name !== "style") { if (name !== "class" && name !== "style") {
// this is a prop! // this is a prop!
@@ -436,7 +434,6 @@ QWeb.addDirective({
isInSubComponent = true; isInSubComponent = true;
break; break;
} }
el = el.parentElement;
} }
if (isInSubComponent) { if (isInSubComponent) {
continue; continue;
@@ -449,7 +446,7 @@ QWeb.addDirective({
slotNode.removeAttribute("t-set-slot"); slotNode.removeAttribute("t-set-slot");
slotNode.parentElement!.removeChild(slotNode); 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; QWeb.slots[`${slotId}_${key}`] = slotFn;
} }
} }
@@ -458,7 +455,7 @@ QWeb.addDirective({
for (let child of Object.values(clone.childNodes)) { for (let child of Object.values(clone.childNodes)) {
t.appendChild(child); t.appendChild(child);
} }
const slotFn = qweb._compile(`slot_default_template`, { elem: t, hasParent: true }); const slotFn = qweb._compile(`slot_default_template`, t, ctx);
QWeb.slots[`${slotId}_default`] = slotFn; QWeb.slots[`${slotId}_default`] = slotFn;
} }
} }
-10
View File
@@ -8,7 +8,6 @@ import { QWeb } from "./qweb/index";
interface Config { interface Config {
mode: string; mode: string;
enableTransitions: boolean;
} }
export const config = {} as Config; export const config = {} as Config;
@@ -29,12 +28,3 @@ Object.defineProperty(config, "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 = new Observer();
__owl__.observer.notifyCB = component.render.bind(component); __owl__.observer.notifyCB = component.render.bind(component);
} }
const currentCB = __owl__.observer.notifyCB;
__owl__.observer.notifyCB = function () {
if (ctx.rev > mapping[id]) {
// in this case, the context has been updated since we were rendering
// last, and we do not need to render here with the observer. A
// rendering is coming anyway, with the correct props.
return;
}
currentCB();
};
mapping[id] = 0; mapping[id] = 0;
const renderFn = __owl__.renderFn; const renderFn = __owl__.renderFn;
+1 -21
View File
@@ -1,4 +1,4 @@
import { Component, Env } from "./component/component"; import { Component } from "./component/component";
import { Observer } from "./core/observer"; import { Observer } from "./core/observer";
/** /**
@@ -118,26 +118,6 @@ export function useRef<C extends Component = Component>(name: string): Ref<C> {
}; };
} }
// -----------------------------------------------------------------------------
// "Builder" hooks
// -----------------------------------------------------------------------------
/**
* This hook is useful as a building block for some customized hooks, that may
* need a reference to the component calling them.
*/
export function useComponent<P, E extends Env>(): Component<P, E> {
return Component.current as any;
}
/**
* This hook is useful as a building block for some customized hooks, that may
* need a reference to the env of the component calling them.
*/
export function useEnv<E extends Env>(): E {
return Component.current.env as any;
}
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// useSubEnv // useSubEnv
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
+1 -3
View File
@@ -19,10 +19,9 @@ import { Link } from "./router/link";
import { RouteComponent } from "./router/route_component"; import { RouteComponent } from "./router/route_component";
import { Router } from "./router/router"; import { Router } from "./router/router";
export { Component, mount } from "./component/component"; export { Component } from "./component/component";
export { QWeb }; export { QWeb };
export { config }; export { config };
export { browser } from "./browser";
export const Context = _context.Context; export const Context = _context.Context;
export const useState = _hooks.useState; export const useState = _hooks.useState;
@@ -38,5 +37,4 @@ export const hooks = Object.assign({}, _hooks, {
useGetters: _store.useGetters, useGetters: _store.useGetters,
useStore: _store.useStore, useStore: _store.useStore,
}); });
export const __info__ = {}; export const __info__ = {};
+14 -28
View File
@@ -1,4 +1,4 @@
import { CompilationContext, INTERP_REGEXP } from "./compilation_context"; import { CompilationContext } from "./compilation_context";
import { QWeb } from "./qweb"; import { QWeb } from "./qweb";
import { htmlToVDOM } from "../vdom/html_to_vdom"; import { htmlToVDOM } from "../vdom/html_to_vdom";
import { QWebVar } from "./expression_parser"; import { QWebVar } from "./expression_parser";
@@ -224,36 +224,19 @@ QWeb.addDirective({
ctx.rootContext.shouldDefineScope = true; ctx.rootContext.shouldDefineScope = true;
ctx.rootContext.shouldDefineUtils = true; ctx.rootContext.shouldDefineUtils = true;
const subTemplate = node.getAttribute("t-call")!; const subTemplate = node.getAttribute("t-call")!;
const isDynamic = INTERP_REGEXP.test(subTemplate);
const nodeTemplate = qweb.templates[subTemplate]; const nodeTemplate = qweb.templates[subTemplate];
if (!isDynamic && !nodeTemplate) { if (!nodeTemplate) {
throw new Error(`Cannot find template "${subTemplate}" (t-call)`); throw new Error(`Cannot find template "${subTemplate}" (t-call)`);
} }
// Step 2: compile target template in sub templates // Step 2: compile target template in sub templates
// ------------------------------------------------ // ------------------------------------------------
let subIdstr: string; let subId = qweb.subTemplates[subTemplate];
if (isDynamic) { if (!subId) {
const _id = ctx.generateID(); subId = QWeb.nextId++;
ctx.addLine(`let tname${_id} = ${ctx.interpolate(subTemplate)};`); qweb.subTemplates[subTemplate] = subId;
ctx.addLine(`let tid${_id} = this.subTemplates[tname${_id}];`); const subTemplateFn = qweb._compile(subTemplate, nodeTemplate.elem, ctx, true);
ctx.addIf(`!tid${_id}`); QWeb.subTemplates[subId] = subTemplateFn;
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}'`;
} }
// Step 3: compile t-call body if necessary // Step 3: compile t-call body if necessary
@@ -268,13 +251,16 @@ QWeb.addDirective({
for (let attr of ["t-if", "t-else", "t-elif", "t-call"]) { for (let attr of ["t-if", "t-else", "t-elif", "t-call"]) {
nodeCopy.removeAttribute(attr); nodeCopy.removeAttribute(attr);
} }
const parentNode = ctx.parentNode;
ctx.parentNode = "__0";
// this local scope is intended to trap c__0 // this local scope is intended to trap c__0
ctx.addLine(`{`); ctx.addLine(`{`);
ctx.indent(); ctx.indent();
ctx.addLine("let c__0 = [];"); ctx.addLine("let c__0 = [];");
qweb._compileNode(nodeCopy, ctx.subContext("parentNode", "__0")); qweb._compileNode(nodeCopy, ctx);
ctx.rootContext.shouldDefineUtils = true; ctx.rootContext.shouldDefineUtils = true;
ctx.addLine("scope[utils.zero] = c__0;"); ctx.addLine("scope[utils.zero] = c__0;");
ctx.parentNode = parentNode;
ctx.dedent(); ctx.dedent();
ctx.addLine(`}`); ctx.addLine(`}`);
} }
@@ -286,12 +272,12 @@ QWeb.addDirective({
const parentNode = ctx.parentNode ? `c${ctx.parentNode}` : "result"; const parentNode = ctx.parentNode ? `c${ctx.parentNode}` : "result";
const extra = `Object.assign({}, extra, {parentNode: ${parentNode}, parent: ${parentComponent}, key: ${key}})`; const extra = `Object.assign({}, extra, {parentNode: ${parentNode}, parent: ${parentComponent}, key: ${key}})`;
if (ctx.parentNode) { if (ctx.parentNode) {
ctx.addLine(`this.constructor.subTemplates[${subIdstr}].call(this, scope, ${extra});`); ctx.addLine(`this.constructor.subTemplates['${subId}'].call(this, scope, ${extra});`);
} else { } else {
// this is a t-call with no parentnode, we need to extract the result // this is a t-call with no parentnode, we need to extract the result
ctx.rootContext.shouldDefineResult = true; ctx.rootContext.shouldDefineResult = true;
ctx.addLine(`result = []`); 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]`); ctx.addLine(`result = result[0]`);
} }
+1 -6
View File
@@ -1,5 +1,4 @@
import { VNode } from "../vdom/index"; import { VNode } from "../vdom/index";
import { INTERP_REGEXP } from "./compilation_context";
import { QWeb } from "./qweb"; import { QWeb } from "./qweb";
/** /**
@@ -206,9 +205,6 @@ QWeb.addDirective({
name: "transition", name: "transition",
priority: 96, priority: 96,
atNodeCreation({ ctx, value, addNodeHook }) { atNodeCreation({ ctx, value, addNodeHook }) {
if (!QWeb.enableTransitions) {
return;
}
ctx.rootContext.shouldDefineUtils = true; ctx.rootContext.shouldDefineUtils = true;
let name = value; let name = value;
const hooks = { const hooks = {
@@ -229,9 +225,8 @@ QWeb.addDirective({
priority: 80, priority: 80,
atNodeEncounter({ ctx, value, node, qweb }): boolean { atNodeEncounter({ ctx, value, node, qweb }): boolean {
const slotKey = ctx.generateID(); const slotKey = ctx.generateID();
const valueExpr = value.match(INTERP_REGEXP) ? ctx.interpolate(value) : `'${value}'`;
ctx.addLine( 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}`); ctx.addIf(`slot${slotKey}`);
let parentNode = `c${ctx.parentNode}`; let parentNode = `c${ctx.parentNode}`;
+10 -16
View File
@@ -70,7 +70,6 @@ const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
const lineBreakRE = /[\r\n]/; const lineBreakRE = /[\r\n]/;
const whitespaceRE = /\s+/g; const whitespaceRE = /\s+/g;
const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
const NODE_HOOKS_PARAMS = { const NODE_HOOKS_PARAMS = {
create: "(_, n)", create: "(_, n)",
@@ -213,7 +212,6 @@ export class QWeb extends EventBus {
h = h; h = h;
// dev mode enables better error messages or more costly validations // dev mode enables better error messages or more costly validations
static dev: boolean = false; static dev: boolean = false;
static enableTransitions: boolean = true;
// slots contains sub templates defined with t-set inside t-component nodes, and // slots contains sub templates defined with t-set inside t-component nodes, and
// are meant to be used by the t-slot directive. // are meant to be used by the t-slot directive.
@@ -314,7 +312,7 @@ export class QWeb extends EventBus {
const template = { const template = {
elem, elem,
fn: function (this: QWeb, context, extra) { fn: function (this: QWeb, context, extra) {
const compiledFunction = this._compile(name); const compiledFunction = this._compile(name, elem);
template.fn = compiledFunction; template.fn = compiledFunction;
return compiledFunction.call(this, context, extra); return compiledFunction.call(this, context, extra);
}, },
@@ -419,33 +417,30 @@ export class QWeb extends EventBus {
_compile( _compile(
name: string, name: string,
options: { elem: Element,
elem?: Element; parentContext?: CompilationContext,
hasParent?: boolean; defineKey?: boolean
defineKey?: boolean;
} = {}
): CompiledTemplate { ): CompiledTemplate {
const elem = options.elem || this.templates[name].elem;
const isDebug = elem.attributes.hasOwnProperty("t-debug"); const isDebug = elem.attributes.hasOwnProperty("t-debug");
const ctx = new CompilationContext(name); const ctx = new CompilationContext(name);
if (elem.tagName !== "t") { if (elem.tagName !== "t") {
ctx.shouldDefineResult = false; ctx.shouldDefineResult = false;
} }
if (options.hasParent) { if (parentContext) {
ctx.variables = Object.create(null); ctx.variables = Object.create(parentContext.variables);
ctx.parentNode = ctx.generateID(); ctx.parentNode = parentContext.parentNode || ctx.generateID();
ctx.allowMultipleRoots = true; ctx.allowMultipleRoots = true;
ctx.hasParentWidget = true; ctx.hasParentWidget = true;
ctx.shouldDefineResult = false; ctx.shouldDefineResult = false;
ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`); ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`);
if (options.defineKey) { if (defineKey) {
ctx.addLine(`let key0 = extra.key || "";`); ctx.addLine(`let key0 = extra.key || "";`);
ctx.hasKey0 = true; ctx.hasKey0 = true;
} }
} }
this._compileNode(elem, ctx); this._compileNode(elem, ctx);
if (!options.hasParent) { if (!parentContext) {
if (ctx.shouldDefineResult) { if (ctx.shouldDefineResult) {
ctx.addLine(`return result;`); ctx.addLine(`return result;`);
} else { } else {
@@ -497,8 +492,7 @@ export class QWeb extends EventBus {
} }
if (this.translateFn) { if (this.translateFn) {
if ((node.parentNode as any).getAttribute("t-translation") !== "off") { if ((node.parentNode as any).getAttribute("t-translation") !== "off") {
const match = translationRE.exec(text); text = this.translateFn(text);
text = match[1] + this.translateFn(match[2]) + match[3];
} }
} }
if (ctx.parentNode) { if (ctx.parentNode) {
+2 -1
View File
@@ -1,4 +1,5 @@
import { Component, Env } from "./component/component"; import { Component } from "./component/component";
import { Env } from "./component/component";
import { Context, useContextWithCB } from "./context"; import { Context, useContextWithCB } from "./context";
import { onWillUpdateProps } from "./hooks"; import { onWillUpdateProps } from "./hooks";
-3
View File
@@ -13,9 +13,6 @@ export function htmlToVDOM(html: string): VNode[] {
function htmlToVNode(node: ChildNode): VNode { function htmlToVNode(node: ChildNode): VNode {
if (!(node instanceof Element)) { if (!(node instanceof Element)) {
if (node instanceof Comment) {
return h("!", node.textContent);
}
return { text: node.textContent! } as VNode; return { text: node.textContent! } as VNode;
} }
const attrs = {}; const attrs = {};
+8 -8
View File
@@ -1,15 +1,15 @@
import { Component, Env } from "../src/component/component"; import { Component, Env } from "../src/component/component";
import { useRef, useState } from "../src/hooks";
import { QWeb } from "../src/qweb/index"; import { QWeb } from "../src/qweb/index";
import { useState, useRef } from "../src/hooks";
import { xml } from "../src/tags"; import { xml } from "../src/tags";
import { import {
makeDeferred, makeDeferred,
makeTestEnv,
makeTestFixture, makeTestFixture,
nextFrame, makeTestEnv,
patchNextFrame, patchNextFrame,
renderToDOM, renderToDOM,
unpatchNextFrame, unpatchNextFrame,
nextTick,
} from "./helpers"; } from "./helpers";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -420,29 +420,29 @@ describe("animations", () => {
widget.state.flag = true; widget.state.flag = true;
await nextFrame(); await nextTick();
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend"));
expect(fixture.innerHTML).toBe('<div><span class="">blue</span></div>'); expect(fixture.innerHTML).toBe('<div><span class="">blue</span></div>');
expect(QWeb.utils.transitionInsert).toBeCalledTimes(1); expect(QWeb.utils.transitionInsert).toBeCalledTimes(1);
widget.state.flag = false; widget.state.flag = false;
await nextFrame(); await nextTick();
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe(
'<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="__3__">blue</span></div>' '<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="__3__">blue</span></div>'
); );
expect(QWeb.utils.transitionInsert).toBeCalledTimes(1); expect(QWeb.utils.transitionInsert).toBeCalledTimes(1);
widget.state.flag = true; widget.state.flag = true;
await nextFrame(); await nextTick();
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe(
'<div><span class="chimay-enter-active chimay-enter-to" data-owl-key="__3__">blue</span></div>' '<div><span class="chimay-enter-active chimay-enter-to" data-owl-key="__3__">blue</span></div>'
); );
expect(QWeb.utils.transitionInsert).toBeCalledTimes(2); expect(QWeb.utils.transitionInsert).toBeCalledTimes(2);
widget.state.flag = false; widget.state.flag = false;
await nextFrame(); await nextTick();
widget.state.flag = true; widget.state.flag = true;
await nextFrame(); await nextTick();
expect(QWeb.utils.transitionInsert).toBeCalledTimes(3); expect(QWeb.utils.transitionInsert).toBeCalledTimes(3);
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend"));
@@ -534,11 +534,11 @@ exports[`other directives with t-component slot setted value (with t-set) not ac
w3.__owl__.pvnode = pvnode; w3.__owl__.pvnode = pvnode;
} }
w3.__owl__.parentLastFiberId = extra.fiber.id; w3.__owl__.parentLastFiberId = extra.fiber.id;
let c6 = [], p6 = {key:6}; let c5 = [], p5 = {key:5};
let vn6 = h('p', p6, c6); let vn5 = h('p', p5, c5);
c1.push(vn6); c1.push(vn5);
if (scope.iter != null) { if (scope.iter != null) {
c6.push({text: scope.iter}); c5.push({text: scope.iter});
} }
return vn1; return vn1;
}" }"
@@ -1253,16 +1253,16 @@ exports[`other directives with t-component t-set can't alter from within callee
if (scope.iter != null) { if (scope.iter != null) {
c2.push({text: scope.iter}); c2.push({text: scope.iter});
} }
let _origScope6 = scope; let _origScope4 = scope;
scope = Object.create(scope); scope = Object.create(scope);
scope.__access_mode__ = 'ro'; scope.__access_mode__ = 'ro';
this.constructor.subTemplates['2'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__7__'})); this.constructor.subTemplates['2'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__5__'}));
scope = _origScope6; scope = _origScope4;
let c8 = [], p8 = {key:8}; let c6 = [], p6 = {key:6};
let vn8 = h('p', p8, c8); let vn6 = h('p', p6, c6);
c1.push(vn8); c1.push(vn6);
if (scope.iter != null) { if (scope.iter != null) {
c8.push({text: scope.iter}); c6.push({text: scope.iter});
} }
return vn1; return vn1;
}" }"
@@ -1284,7 +1284,7 @@ exports[`other directives with t-component t-set can't alter in t-call body 1`]
if (scope.iter != null) { if (scope.iter != null) {
c2.push({text: scope.iter}); c2.push({text: scope.iter});
} }
let _origScope6 = scope; let _origScope4 = scope;
scope = Object.create(scope); scope = Object.create(scope);
scope.__access_mode__ = 'ro'; scope.__access_mode__ = 'ro';
{ {
@@ -1293,14 +1293,14 @@ exports[`other directives with t-component t-set can't alter in t-call body 1`]
utils.getScope(scope, 'iter').iter = 'inCall'; utils.getScope(scope, 'iter').iter = 'inCall';
scope[utils.zero] = c__0; scope[utils.zero] = c__0;
} }
this.constructor.subTemplates['2'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__7__'})); this.constructor.subTemplates['2'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__5__'}));
} }
scope = _origScope6; scope = _origScope4;
let c8 = [], p8 = {key:8}; let c6 = [], p6 = {key:6};
let vn8 = h('p', p8, c8); let vn6 = h('p', p6, c6);
c1.push(vn8); c1.push(vn6);
if (scope.iter != null) { if (scope.iter != null) {
c8.push({text: scope.iter}); c6.push({text: scope.iter});
} }
return vn1; return vn1;
}" }"
@@ -1455,11 +1455,11 @@ exports[`random stuff/miscellaneous can inject values in tagged templates 1`] =
let h = this.h; let h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1); let vn1 = h('div', p1, c1);
let _origScope5 = scope; let _origScope4 = scope;
scope = Object.create(scope); scope = Object.create(scope);
scope.__access_mode__ = 'ro'; scope.__access_mode__ = 'ro';
this.constructor.subTemplates['3'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__6__'})); this.constructor.subTemplates['3'].call(this, scope, Object.assign({}, extra, {parentNode: c1, parent: utils.getComponent(context), key: '__5__'}));
scope = _origScope5; scope = _origScope4;
return vn1; return vn1;
}" }"
`; `;
@@ -1574,15 +1574,15 @@ exports[`t-call handlers are properly bound through a t-call 1`] = `
// Template name: \\"sub\\" // Template name: \\"sub\\"
let utils = this.constructor.utils; let utils = this.constructor.utils;
let h = this.h; let h = this.h;
let c2 = extra.parentNode; let c1 = extra.parentNode;
let key0 = extra.key || \\"\\"; let key0 = extra.key || \\"\\";
let c3 = [], p3 = {key:\`\${key0}_3\`,on:{}}; let c2 = [], p2 = {key:\`\${key0}_2\`,on:{}};
let vn3 = h('p', p3, c3); let vn2 = h('p', p2, c2);
c2.push(vn3); c1.push(vn2);
let k4 = \`click__4__\${key0}__\`; let k3 = \`click__3__\${key0}__\`;
extra.handlers[k4] = extra.handlers[k4] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['update'](e);}; extra.handlers[k3] = extra.handlers[k3] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['update'](e);};
p3.on['click'] = extra.handlers[k4]; p2.on['click'] = extra.handlers[k3];
c3.push({text: \`lucas\`}); c2.push({text: \`lucas\`});
}" }"
`; `;
@@ -1593,14 +1593,14 @@ exports[`t-call handlers with arguments are properly bound through a t-call 1`]
let utils = this.constructor.utils; let utils = this.constructor.utils;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let c2 = extra.parentNode; let c1 = extra.parentNode;
let key0 = extra.key || \\"\\"; let key0 = extra.key || \\"\\";
let c3 = [], p3 = {key:\`\${key0}_3\`,on:{}}; let c2 = [], p2 = {key:\`\${key0}_2\`,on:{}};
let vn3 = h('p', p3, c3); let vn2 = h('p', p2, c2);
c2.push(vn3); c1.push(vn2);
let args4 = [scope['a']]; let args3 = [scope['a']];
p3.on['click'] = function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['update'](...args4, e);}; p2.on['click'] = function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['update'](...args3, e);};
c3.push({text: \`lucas\`}); c2.push({text: \`lucas\`});
}" }"
`; `;
@@ -1613,32 +1613,32 @@ exports[`t-call parent is set within t-call 1`] = `
let parent = extra.parent; let parent = extra.parent;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let c2 = extra.parentNode; let c1 = extra.parentNode;
let key0 = extra.key || \\"\\"; let key0 = extra.key || \\"\\";
// Component 'Child' // Component 'Child'
let k4 = \`__4__\${key0}__\`; let k3 = \`__3__\${key0}__\`;
let w3 = k4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k4]] : false; let w2 = k3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k3]] : false;
let props3 = {}; let props2 = {};
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) { if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w3.destroy(); w2.destroy();
w3 = false; w2 = false;
} }
if (w3) { if (w2) {
w3.__updateProps(props3, extra.fiber, undefined); w2.__updateProps(props2, extra.fiber, undefined);
let pvnode = w3.__owl__.pvnode; let pvnode = w2.__owl__.pvnode;
c2.push(pvnode); c1.push(pvnode);
} else { } else {
let componentKey3 = \`Child\`; let componentKey2 = \`Child\`;
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| scope['Child']; let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')} if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w3 = new W3(parent, props3); w2 = new W2(parent, props2);
parent.__owl__.cmap[k4] = w3.__owl__.id; parent.__owl__.cmap[k3] = w2.__owl__.id;
let fiber = w3.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k4, hook: {remove() {},destroy(vn) {w3.destroy();}}}); let pvnode = h('dummy', {key: k3, hook: {remove() {},destroy(vn) {w2.destroy();}}});
c2.push(pvnode); c1.push(pvnode);
w3.__owl__.pvnode = pvnode; w2.__owl__.pvnode = pvnode;
} }
w3.__owl__.parentLastFiberId = extra.fiber.id; w2.__owl__.parentLastFiberId = extra.fiber.id;
}" }"
`; `;
+123 -144
View File
@@ -44,23 +44,23 @@ exports[`t-slot directive can define and call slots 2`] = `
) { ) {
// Template name: \\"Dialog\\" // Template name: \\"Dialog\\"
let h = this.h; let h = this.h;
let 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 c9 = [], p9 = {key:9};
let vn9 = h('div', p9, c9); let vn9 = h('div', p9, c9);
let c10 = [], p10 = {key:10}; c6.push(vn9);
let vn10 = h('div', p10, c10); const slot10 = this.constructor.slots[context.__owl__.slotId + '_' + 'footer'];
c9.push(vn10); if (slot10) {
const slot11 = this.constructor.slots[context.__owl__.slotId + '_' + 'header']; slot10.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c9, parent: extra.parent || context}));
if (slot11) {
slot11.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c10, parent: extra.parent || context}));
} }
let c12 = [], p12 = {key:12}; return vn6;
let vn12 = h('div', p12, c12);
c9.push(vn12);
const slot13 = this.constructor.slots[context.__owl__.slotId + '_' + 'footer'];
if (slot13) {
slot13.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c12, parent: extra.parent || context}));
}
return vn9;
}" }"
`; `;
@@ -69,11 +69,11 @@ exports[`t-slot directive can define and call slots 3`] = `
) { ) {
// Template name: \\"slot_header_template\\" // Template name: \\"slot_header_template\\"
let h = this.h; let h = this.h;
let c4 = extra.parentNode; let c1 = extra.parentNode;
let c5 = [], p5 = {key:5}; let c4 = [], p4 = {key:4};
let vn5 = h('span', p5, c5); let vn4 = h('span', p4, c4);
c4.push(vn5); c1.push(vn4);
c5.push({text: \`header\`}); c4.push({text: \`header\`});
}" }"
`; `;
@@ -82,11 +82,11 @@ exports[`t-slot directive can define and call slots 4`] = `
) { ) {
// Template name: \\"slot_footer_template\\" // Template name: \\"slot_footer_template\\"
let h = this.h; let h = this.h;
let c6 = extra.parentNode; let c1 = extra.parentNode;
let c7 = [], p7 = {key:7}; let c5 = [], p5 = {key:5};
let vn7 = h('span', p7, c7); let vn5 = h('span', p5, c5);
c6.push(vn7); c1.push(vn5);
c7.push({text: \`footer\`}); c5.push({text: \`footer\`});
}" }"
`; `;
@@ -134,23 +134,23 @@ exports[`t-slot directive can define and call slots using old t-set keyword 2`]
) { ) {
// Template name: \\"__template__1\\" // Template name: \\"__template__1\\"
let h = this.h; let h = this.h;
let 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 c9 = [], p9 = {key:9};
let vn9 = h('div', p9, c9); let vn9 = h('div', p9, c9);
let c10 = [], p10 = {key:10}; c6.push(vn9);
let vn10 = h('div', p10, c10); const slot10 = this.constructor.slots[context.__owl__.slotId + '_' + 'footer'];
c9.push(vn10); if (slot10) {
const slot11 = this.constructor.slots[context.__owl__.slotId + '_' + 'header']; slot10.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c9, parent: extra.parent || context}));
if (slot11) {
slot11.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c10, parent: extra.parent || context}));
} }
let c12 = [], p12 = {key:12}; return vn6;
let vn12 = h('div', p12, c12);
c9.push(vn12);
const slot13 = this.constructor.slots[context.__owl__.slotId + '_' + 'footer'];
if (slot13) {
slot13.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c12, parent: extra.parent || context}));
}
return vn9;
}" }"
`; `;
@@ -159,11 +159,11 @@ exports[`t-slot directive can define and call slots using old t-set keyword 3`]
) { ) {
// Template name: \\"slot_header_template\\" // Template name: \\"slot_header_template\\"
let h = this.h; let h = this.h;
let c4 = extra.parentNode; let c1 = extra.parentNode;
let c5 = [], p5 = {key:5}; let c4 = [], p4 = {key:4};
let vn5 = h('span', p5, c5); let vn4 = h('span', p4, c4);
c4.push(vn5); c1.push(vn4);
c5.push({text: \`header\`}); c4.push({text: \`header\`});
}" }"
`; `;
@@ -172,11 +172,11 @@ exports[`t-slot directive can define and call slots using old t-set keyword 4`]
) { ) {
// Template name: \\"slot_footer_template\\" // Template name: \\"slot_footer_template\\"
let h = this.h; let h = this.h;
let c6 = extra.parentNode; let c1 = extra.parentNode;
let c7 = [], p7 = {key:7}; let c5 = [], p5 = {key:5};
let vn7 = h('span', p7, c7); let vn5 = h('span', p5, c5);
c6.push(vn7); c1.push(vn5);
c7.push({text: \`footer\`}); c5.push({text: \`footer\`});
}" }"
`; `;
@@ -185,11 +185,11 @@ exports[`t-slot directive content is the default slot 1`] = `
) { ) {
// Template name: \\"slot_default_template\\" // Template name: \\"slot_default_template\\"
let h = this.h; let h = this.h;
let c4 = extra.parentNode; let c1 = extra.parentNode;
let c5 = [], p5 = {key:5}; let c4 = [], p4 = {key:4};
let vn5 = h('span', p5, c5); let vn4 = h('span', p4, c4);
c4.push(vn5); c1.push(vn4);
c5.push({text: \`sts rocks\`}); c4.push({text: \`sts rocks\`});
}" }"
`; `;
@@ -215,27 +215,8 @@ exports[`t-slot directive default slot work with text nodes 1`] = `
) { ) {
// Template name: \\"slot_default_template\\" // Template name: \\"slot_default_template\\"
let h = this.h; let h = this.h;
let c4 = extra.parentNode; let c1 = extra.parentNode;
c4.push({text: \`sts rocks\`}); c1.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 c10 = [], p10 = {key:10,on:{}};
let vn10 = h('button', p10, c10);
extra.handlers['click__11__'] = extra.handlers['click__11__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['toggle'](e);};
p10.on['click'] = extra.handlers['click__11__'];
const slot12 = this.constructor.slots[context.__owl__.slotId + '_' + (scope['current'].slot)];
if (slot12) {
slot12.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c10, parent: extra.parent || context}));
}
return vn10;
}" }"
`; `;
@@ -244,15 +225,15 @@ exports[`t-slot directive multiple roots are allowed in a default slot 1`] = `
) { ) {
// Template name: \\"slot_default_template\\" // Template name: \\"slot_default_template\\"
let h = this.h; 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 c5 = [], p5 = {key:5};
let vn5 = h('span', p5, c5); let vn5 = h('span', p5, c5);
c4.push(vn5); c1.push(vn5);
c5.push({text: \`sts\`}); c5.push({text: \`rocks\`});
let c6 = [], p6 = {key:6};
let vn6 = h('span', p6, c6);
c4.push(vn6);
c6.push({text: \`rocks\`});
}" }"
`; `;
@@ -261,15 +242,15 @@ exports[`t-slot directive multiple roots are allowed in a named slot 1`] = `
) { ) {
// Template name: \\"slot_content_template\\" // Template name: \\"slot_content_template\\"
let h = this.h; 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 c5 = [], p5 = {key:5};
let vn5 = h('span', p5, c5); let vn5 = h('span', p5, c5);
c4.push(vn5); c1.push(vn5);
c5.push({text: \`sts\`}); c5.push({text: \`rocks\`});
let c6 = [], p6 = {key:6};
let vn6 = h('span', p6, c6);
c4.push(vn6);
c6.push({text: \`rocks\`});
}" }"
`; `;
@@ -297,22 +278,22 @@ exports[`t-slot directive refs are properly bound in slots 1`] = `
let utils = this.constructor.utils; let utils = this.constructor.utils;
context.__owl__.refs = context.__owl__.refs || {}; context.__owl__.refs = context.__owl__.refs || {};
let h = this.h; let h = this.h;
let c8 = extra.parentNode; let c1 = extra.parentNode;
let c9 = [], p9 = {key:9,on:{}}; let c8 = [], p8 = {key:8,on:{}};
let vn9 = h('button', p9, c9); let vn8 = h('button', p8, c8);
c8.push(vn9); c1.push(vn8);
extra.handlers['click__10__'] = extra.handlers['click__10__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['doSomething'](e);}; extra.handlers['click__9__'] = extra.handlers['click__9__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['doSomething'](e);};
p9.on['click'] = extra.handlers['click__10__']; p8.on['click'] = extra.handlers['click__9__'];
const ref11 = \`myButton\`; const ref10 = \`myButton\`;
p9.hook = { p8.hook = {
create: (_, n) => { create: (_, n) => {
context.__owl__.refs[ref11] = n.elm; context.__owl__.refs[ref10] = n.elm;
}, },
destroy: () => { destroy: () => {
delete context.__owl__.refs[ref11]; delete context.__owl__.refs[ref10];
}, },
}; };
c9.push({text: \`do something\`}); c8.push({text: \`do something\`});
}" }"
`; `;
@@ -322,13 +303,13 @@ exports[`t-slot directive slots are rendered with proper context 1`] = `
// Template name: \\"slot_footer_template\\" // Template name: \\"slot_footer_template\\"
let utils = this.constructor.utils; let utils = this.constructor.utils;
let h = this.h; let h = this.h;
let c8 = extra.parentNode; let c1 = extra.parentNode;
let c9 = [], p9 = {key:9,on:{}}; let c8 = [], p8 = {key:8,on:{}};
let vn9 = h('button', p9, c9); let vn8 = h('button', p8, c8);
c8.push(vn9); c1.push(vn8);
extra.handlers['click__10__'] = extra.handlers['click__10__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['doSomething'](e);}; extra.handlers['click__9__'] = extra.handlers['click__9__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['doSomething'](e);};
p9.on['click'] = extra.handlers['click__10__']; p8.on['click'] = extra.handlers['click__9__'];
c9.push({text: \`do something\`}); c8.push({text: \`do something\`});
}" }"
`; `;
@@ -338,14 +319,14 @@ exports[`t-slot directive slots are rendered with proper context, part 2 1`] = `
// Template name: \\"Link\\" // Template name: \\"Link\\"
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let _12 = scope['props'].to; let _11 = scope['props'].to;
let c13 = [], p13 = {key:13,attrs:{href: _12}}; let c12 = [], p12 = {key:12,attrs:{href: _11}};
let vn13 = h('a', p13, c13); let vn12 = h('a', p12, c12);
const slot14 = this.constructor.slots[context.__owl__.slotId + '_' + 'default']; const slot13 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot14) { if (slot13) {
slot14.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c13, parent: extra.parent || context})); slot13.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c12, parent: extra.parent || context}));
} }
return vn13; return vn12;
}" }"
`; `;
@@ -420,11 +401,11 @@ exports[`t-slot directive slots are rendered with proper context, part 2 3`] = `
// Template name: \\"slot_default_template\\" // Template name: \\"slot_default_template\\"
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let c10 = extra.parentNode; let c7 = extra.parentNode;
c10.push({text: \`User \`}); c7.push({text: \`User \`});
let _11 = scope['user'].name; let _10 = scope['user'].name;
if (_11 != null) { if (_10 != null) {
c10.push({text: _11}); c7.push({text: _10});
} }
}" }"
`; `;
@@ -435,14 +416,14 @@ exports[`t-slot directive slots are rendered with proper context, part 3 1`] = `
// Template name: \\"Link\\" // Template name: \\"Link\\"
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let _12 = scope['props'].to; let _10 = scope['props'].to;
let c13 = [], p13 = {key:13,attrs:{href: _12}}; let c11 = [], p11 = {key:11,attrs:{href: _10}};
let vn13 = h('a', p13, c13); let vn11 = h('a', p11, c11);
const slot14 = this.constructor.slots[context.__owl__.slotId + '_' + 'default']; const slot12 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot14) { if (slot12) {
slot14.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c13, parent: extra.parent || context})); slot12.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c11, parent: extra.parent || context}));
} }
return vn13; return vn11;
}" }"
`; `;
@@ -518,10 +499,9 @@ exports[`t-slot directive slots are rendered with proper context, part 3 3`] = `
// Template name: \\"slot_default_template\\" // Template name: \\"slot_default_template\\"
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let c10 = extra.parentNode; let c7 = extra.parentNode;
let _11 = scope['userdescr']; if (scope.userdescr != null) {
if (_11 != null) { c7.push({text: scope.userdescr});
c10.push({text: _11});
} }
}" }"
`; `;
@@ -572,10 +552,9 @@ exports[`t-slot directive slots are rendered with proper context, part 4 2`] = `
// Template name: \\"slot_default_template\\" // Template name: \\"slot_default_template\\"
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let c4 = extra.parentNode; let c1 = extra.parentNode;
let _5 = scope['userdescr']; if (scope.userdescr != null) {
if (_5 != null) { c1.push({text: scope.userdescr});
c4.push({text: _5});
} }
}" }"
`; `;
@@ -585,13 +564,13 @@ exports[`t-slot directive t-set t-value in a slot 1`] = `
) { ) {
// Template name: \\"__template__1\\" // Template name: \\"__template__1\\"
let h = this.h; let h = this.h;
let c5 = [], p5 = {key:5}; let c4 = [], p4 = {key:4};
let vn5 = h('span', p5, c5); let vn4 = h('span', p4, c4);
const slot6 = this.constructor.slots[context.__owl__.slotId + '_' + 'default']; const slot5 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot6) { if (slot5) {
slot6.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c5, parent: extra.parent || context})); slot5.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c4, parent: extra.parent || context}));
} }
return vn5; return vn4;
}" }"
`; `;
@@ -602,12 +581,12 @@ exports[`t-slot directive template can just return a slot 1`] = `
let utils = this.constructor.utils; let utils = this.constructor.utils;
let result; let result;
let h = this.h; let h = this.h;
const slot7 = this.constructor.slots[context.__owl__.slotId + '_' + 'default']; const slot6 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot7) { if (slot6) {
let children8= [] let children7= []
result = {} result = {}
slot7.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: children8, parent: extra.parent || context})); slot6.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: children7, parent: extra.parent || context}));
utils.defineProxy(result, children8[0]); utils.defineProxy(result, children7[0]);
} }
return result; return result;
}" }"
+20 -19
View File
@@ -1,4 +1,4 @@
import { Component, Env, mount } from "../../src/component/component"; import { Component, Env } from "../../src/component/component";
import { EventBus } from "../../src/core/event_bus"; import { EventBus } from "../../src/core/event_bus";
import { useRef, useState } from "../../src/hooks"; import { useRef, useState } from "../../src/hooks";
import { QWeb } from "../../src/qweb/qweb"; import { QWeb } from "../../src/qweb/qweb";
@@ -69,23 +69,18 @@ describe("basic widget properties", () => {
class SomeWidget extends Component { class SomeWidget extends Component {
static template = xml`<div>content</div>`; 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>"); 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 () => { test("can be mounted on a documentFragment", async () => {
class SomeWidget extends Component { class SomeWidget extends Component {
static template = xml`<div>content</div>`; 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(""); expect(fixture.innerHTML).toBe("");
await widget.mount(fixture); await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div>content</div>"); expect(fixture.innerHTML).toBe("<div>content</div>");
@@ -95,9 +90,10 @@ describe("basic widget properties", () => {
class SomeWidget extends Component { class SomeWidget extends Component {
static template = xml`<div>content</div>`; static template = xml`<div>content</div>`;
} }
const widget = new SomeWidget();
let error; let error;
try { try {
await mount(SomeWidget, { target: null as any }); await widget.mount(null as any);
} catch (e) { } catch (e) {
error = e; error = e;
} }
@@ -111,9 +107,10 @@ describe("basic widget properties", () => {
class SomeWidget extends Component { class SomeWidget extends Component {
static template = xml`<t/>`; static template = xml`<t/>`;
} }
const widget = new SomeWidget();
let error; let error;
try { try {
await mount(SomeWidget, { target: fixture }); await widget.mount(fixture);
} catch (e) { } catch (e) {
error = 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(); await nextTick();
expect(fixture.innerHTML).toBe("<div>0<button>Inc</button></div>"); expect(fixture.innerHTML).toBe("<div>0<button>Inc</button></div>");
const button = (<HTMLElement>counter.el).getElementsByTagName("button")[0]; const button = (<HTMLElement>counter.el).getElementsByTagName("button")[0];
@@ -158,7 +156,8 @@ describe("basic widget properties", () => {
static components = { Child }; 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(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
expect(fixture.innerHTML).toBe("<div><span></span></div>"); expect(fixture.innerHTML).toBe("<div><span></span></div>");
}); });
@@ -172,8 +171,9 @@ describe("basic widget properties", () => {
}); });
} }
const counter = new Counter();
const target = document.createElement("div"); 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>"); expect(target.innerHTML).toBe("<div>0<button>Inc</button></div>");
const button = (<HTMLElement>counter.el).getElementsByTagName("button")[0]; const button = (<HTMLElement>counter.el).getElementsByTagName("button")[0];
button.click(); button.click();
@@ -188,7 +188,8 @@ describe("basic widget properties", () => {
<div style="font-weight:bold;" class="some-class">world</div> <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>`); expect(fixture.innerHTML).toBe(`<div style="font-weight:bold;" class="some-class">world</div>`);
}); });
@@ -211,8 +212,8 @@ describe("basic widget properties", () => {
steps.push("patched"); steps.push("patched");
} }
} }
await mount(TestW, { target: fixture }); const widget = new TestW();
await widget.mount(fixture);
expect(steps).toEqual(["__render", "mounted"]); expect(steps).toEqual(["__render", "mounted"]);
}); });
+1 -67
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 { QWeb } from "../../src/qweb/qweb";
import { xml } from "../../src/tags"; import { xml } from "../../src/tags";
import { useState, useRef } from "../../src/hooks"; import { useState, useRef } from "../../src/hooks";
@@ -1052,70 +1052,4 @@ describe("t-slot directive", () => {
"<div><child><p>Ablip</p>default2<child>default1<p>Bblip</p></child></child></div>" "<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();
});
}); });
+17 -56
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 { useState } from "../../src/hooks";
import { xml } from "../../src/tags"; import { xml } from "../../src/tags";
import { makeDeferred, makeTestEnv, makeTestFixture, nextTick, nextMicroTick } from "../helpers"; import { makeDeferred, makeTestEnv, makeTestFixture, nextTick, nextMicroTick } from "../helpers";
@@ -37,7 +37,8 @@ describe("mount targets", () => {
div.innerHTML = `<p>pre-existing</p>`; div.innerHTML = `<p>pre-existing</p>`;
fixture.appendChild(div); 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( expect(fixture.innerHTML).toBe(
`<div class="arbitrary custom"><p>pre-existing</p>app<p>another tag</p></div>` `<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"); const div = document.createElement("div");
fixture.appendChild(div); fixture.appendChild(div);
const app = new App();
let error; let error;
try { try {
await mount(App, { target: div, position: "self" }); await app.mount(div, { position: "self" });
} catch (e) { } catch (e) {
error = e; error = e;
} }
@@ -82,7 +84,8 @@ describe("mount targets", () => {
const span = document.createElement("span"); const span = document.createElement("span");
fixture.appendChild(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>"); expect(fixture.innerHTML).toBe("<div>app</div><span></span>");
}); });
@@ -93,7 +96,8 @@ describe("mount targets", () => {
const span = document.createElement("span"); const span = document.createElement("span");
fixture.appendChild(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>"); expect(fixture.innerHTML).toBe("<span></span><div>app</div>");
}); });
@@ -104,7 +108,8 @@ describe("mount targets", () => {
const span = document.createElement("span"); const span = document.createElement("span");
fixture.appendChild(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>"); 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(fixture.innerHTML).toBe("<div>Hey</div>");
expect(steps).toEqual(["willstart", "mounted"]); 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); await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div>Hey</div>"); expect(fixture.innerHTML).toBe("<div>Hey</div>");
expect(steps).toEqual(["willstart", "mounted"]); expect(steps).toEqual(["willstart", "mounted"]);
@@ -196,7 +203,8 @@ describe("unmounting and remounting", () => {
state = useState({ val: 1, flag: true }); 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(steps).toEqual(["render"]);
expect(fixture.innerHTML).toBe("<div><span>12</span></div>"); expect(fixture.innerHTML).toBe("<div><span>12</span></div>");
widget.state.flag = false; widget.state.flag = false;
@@ -600,51 +608,4 @@ describe("unmounting and remounting", () => {
await parent.render(); await parent.render();
expect(fixture.textContent).toBe("fixedsome text"); expect(fixture.textContent).toBe("fixedsome text");
}); });
test("remounting component tree where a component implement shouldupdate", async () => {
let state: any;
const steps = [];
class Child extends Component {
static template = xml`<div><t t-esc="state.word"/><t t-esc="props.name"/></div>`;
state = useState({ word: "hello" });
constructor(parent, props) {
super(parent, props);
state = this.state;
}
patched() {
steps.push("patched");
}
mounted() {
steps.push("mounted");
}
willUnmount() {
steps.push("willUnmount");
}
shouldUpdate() {
return false;
}
}
class Parent extends Component {
static template = xml`<div><Child name="state.name"/></div>`;
static components = { Child };
state = useState({ name: "World" });
}
const parent = await mount(Parent, { target: fixture });
expect(fixture.innerHTML).toBe("<div><div>helloWorld</div></div>");
parent.unmount();
expect(fixture.innerHTML).toBe("");
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>helloWorld</div></div>");
state.word = "test";
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>testWorld</div></div>");
expect(steps).toEqual(["mounted", "willUnmount", "mounted", "patched"]);
});
}); });
+1 -20
View File
@@ -289,26 +289,7 @@ describe("Context", () => {
expect(testContext.subscriptions.update.length).toBe(0); expect(testContext.subscriptions.update.length).toBe(0);
}); });
test.skip("concurrent renderings", async () => { test("concurrent renderings", async () => {
/**
* Note: this test is interesting, but sadly just an incomplete attempt at
* protecting users against themselves. With the context API, it is not
* possible for the framework to protect completely against crashes. Maybe
* like in this case, when a component is in a simple hierarchy where all
* renderings come from the context changes, but in a real case, where some
* code can trigger a rendering independently, it is insufficient.
*
* The main problem is that the sub component depends on some external state,
* which may be modified, and then incompatible with the component actual
* state (for example, if the sub component has an id key related to some
* object that has been removed from the context).
*
* For now, sadly, the only solution is that components that depends on external
* state should guarantee their own integrity themselves. Then maybe this
* could be solved at the level of a state management solution that has a
* more advanced API, to let components determine if they should be updated
* or not (so, something slightly more advanced that the useStore hook).
*/
const testContext = new Context({ x: { n: 1 }, key: "x" }); const testContext = new Context({ x: { n: 1 }, key: "x" });
const def = makeDeferred(); const def = makeDeferred();
let stateC; let stateC;
+3 -8
View File
@@ -37,13 +37,8 @@ export function nextMicroTick(): Promise<void> {
} }
export async function nextTick(): 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) => 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() { export function makeTestFixture() {
@@ -132,7 +127,7 @@ export function renderToString(
// is useful for animations tests, as we hook before repaints to trigger // is useful for animations tests, as we hook before repaints to trigger
// animations (thanks to requestAnimationFrame). Patching nextFrame allows to // animations (thanks to requestAnimationFrame). Patching nextFrame allows to
// simulate calls to this hook. One must not forget to unpatch afterwards. // 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) { export function patchNextFrame(f: Function) {
QWeb.utils.nextFrame = (cb: () => void) => { QWeb.utils.nextFrame = (cb: () => void) => {
setTimeout(() => f(cb)); setTimeout(() => f(cb));
@@ -140,7 +135,7 @@ export function patchNextFrame(f: Function) {
} }
export function unpatchNextFrame() { export function unpatchNextFrame() {
QWeb.utils.nextFrame = _nextFrame; QWeb.utils.nextFrame = nextFrame;
} }
export async function editInput(input: HTMLInputElement | HTMLTextAreaElement, value: string) { export async function editInput(input: HTMLInputElement | HTMLTextAreaElement, value: string) {
-28
View File
@@ -9,10 +9,8 @@ import {
onWillPatch, onWillPatch,
onWillStart, onWillStart,
onWillUpdateProps, onWillUpdateProps,
useEnv,
useSubEnv, useSubEnv,
useExternalListener, useExternalListener,
useComponent,
} from "../src/hooks"; } from "../src/hooks";
import { xml } from "../src/tags"; import { xml } from "../src/tags";
@@ -522,19 +520,6 @@ describe("hooks", () => {
}); });
}); });
test("can use useEnv", async () => {
expect.assertions(1);
class TestComponent extends Component {
static template = xml`<div><t t-esc="env.val"/></div>`;
constructor() {
super();
expect(useEnv()).toBe(env);
}
}
const component = new TestComponent();
await component.mount(fixture);
});
test("can use sub env", async () => { test("can use sub env", async () => {
class TestComponent extends Component { class TestComponent extends Component {
static template = xml`<div><t t-esc="env.val"/></div>`; static template = xml`<div><t t-esc="env.val"/></div>`;
@@ -550,19 +535,6 @@ describe("hooks", () => {
expect(component.env).toHaveProperty("val"); expect(component.env).toHaveProperty("val");
}); });
test("can use useComponent", async () => {
expect.assertions(1);
class TestComponent extends Component {
static template = xml`<div></div>`;
constructor() {
super();
expect(useComponent()).toBe(this);
}
}
const component = new TestComponent();
await component.mount(fixture);
});
test("parent and child env", async () => { test("parent and child env", async () => {
class Child extends Component { class Child extends Component {
static template = xml`<div><t t-esc="env.val"/></div>`; static template = xml`<div><t t-esc="env.val"/></div>`;
File diff suppressed because it is too large Load Diff
-38
View File
@@ -238,13 +238,6 @@ describe("t-raw", () => {
"<span><span>hello</span><ok>world</ok></span>" "<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", () => { describe("t-set", () => {
@@ -1095,26 +1088,6 @@ describe("t-call (template calling", () => {
const expected = "<div><p>yip yip</p></div>"; const expected = "<div><p>yip yip</p></div>";
expect(renderToString(qweb, "main")).toBe(expected); 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", () => { describe("foreach", () => {
@@ -2154,17 +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>' '<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("translation is done on the trimmed text, with extra spaces readded after", () => {
const translations = {
word: "mot",
};
const translateFn = jest.fn((expr) => translations[expr] || expr);
const qweb = new QWeb({ translateFn });
qweb.addTemplate("test", "<div> word </div>");
expect(renderToString(qweb, "test")).toBe("<div> mot </div>");
expect(translateFn).toHaveBeenCalledWith("word");
});
}); });
describe("t-key tests", () => { describe("t-key tests", () => {
+10 -10
View File
@@ -7,16 +7,16 @@ exports[`Link component can render simple cases 1`] = `
let utils = this.constructor.utils; let utils = this.constructor.utils;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let _5 = utils.toObj({'router-link-active':scope['isActive']}); let _4 = utils.toObj({'router-link-active':scope['isActive']});
let _6 = scope['href']; let _5 = scope['href'];
let c7 = [], p7 = {key:7,attrs:{href: _6},class:_5,on:{}}; let c6 = [], p6 = {key:6,attrs:{href: _5},class:_4,on:{}};
let vn7 = h('a', p7, c7); let vn6 = h('a', p6, c6);
extra.handlers['click__8__'] = extra.handlers['click__8__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['navigate'](e);}; extra.handlers['click__7__'] = extra.handlers['click__7__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['navigate'](e);};
p7.on['click'] = extra.handlers['click__8__']; p6.on['click'] = extra.handlers['click__7__'];
const slot9 = this.constructor.slots[context.__owl__.slotId + '_' + 'default']; const slot8 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot9) { if (slot8) {
slot9.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c7, parent: extra.parent || context})); slot8.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c6, parent: extra.parent || context}));
} }
return vn7; return vn6;
}" }"
`; `;
+1 -105
View File
@@ -1,4 +1,4 @@
import { Component, Env, mount } from "../src/component/component"; import { Component, Env } from "../src/component/component";
import { Store, useStore, useDispatch, useGetters, EnvWithStore } from "../src/store"; import { Store, useStore, useDispatch, useGetters, EnvWithStore } from "../src/store";
import { useState } from "../src/hooks"; import { useState } from "../src/hooks";
import { xml } from "../src/tags"; import { xml } from "../src/tags";
@@ -1241,108 +1241,4 @@ describe("various scenarios", () => {
await nextTick(); await nextTick();
expect(fixture.innerHTML).toMatchSnapshot(); expect(fixture.innerHTML).toMatchSnapshot();
}); });
test("component with store, useState and shouldUpdate=false", async () => {
let state: any;
const store = new Store({ state: { rev: 0 } });
class Child extends Component {
static template = xml`<div><t t-esc="state.word"/><t t-esc="props.name"/></div>`;
state = useState({ word: "hello" });
constructor(parent, props) {
super(parent, props);
state = this.state;
useStore((props) => {
return 1;
});
}
shouldUpdate() {
return false;
}
}
class Parent extends Component {
static template = xml`<div><Child name="state.name"/></div>`;
static components = { Child };
state = useState({ name: "World" });
constructor(parent, props) {
super(parent, props);
useStore((props) => store.state.rev);
}
}
(env as any).store = store;
await mount(Parent, { target: fixture, env });
expect(fixture.innerHTML).toBe("<div><div>helloWorld</div></div>");
store.state.rev++;
// this is the key to the bug, it makes Parent be in "render" state but not
// yet rendered while the change of state happens
await Promise.resolve();
state.word = "test";
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>testWorld</div></div>");
});
test("component with store, useState, shouldUpdate=false and child with shouldupdate false", async () => {
let state: any;
const store = new Store({ state: { rev: 0 } });
class ChildChild extends Component {
static template = xml`<div><t t-esc="props.value"/></div>`;
shouldUpdate() {
return false;
}
}
class Child extends Component {
static template = xml`<div><t t-esc="state.word"/><t t-esc="props.name"/><ChildChild value="state.value"/></div>`;
static components = { ChildChild };
state = useState({ word: "hello", value: 3 });
constructor(parent, props) {
super(parent, props);
state = this.state;
useStore((props) => {
return 1;
});
}
shouldUpdate() {
return false;
}
}
class Parent extends Component {
static template = xml`<div><Child name="state.name"/></div>`;
static components = { Child };
state = useState({ name: "World" });
constructor(parent, props) {
super(parent, props);
useStore((props) => store.state.rev);
}
}
(env as any).store = store;
await mount(Parent, { target: fixture, env });
expect(fixture.innerHTML).toBe("<div><div>helloWorld<div>3</div></div></div>");
store.state.rev++;
// this is the key to the bug, it makes Parent be in "render" state but not
// yet rendered while the change of state happens
await Promise.resolve();
state.word = "test";
state.value = 44;
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>testWorld<div>3</div></div></div>");
});
}); });
+256 -285
View File
@@ -1,24 +1,23 @@
import { SAMPLES } from "./samples.js"; import { SAMPLES as samples } from "./samples.js" ;
const { mount, hooks } = owl; const { useState, useRef, onMounted, onWillUnmount } = owl.hooks;
const { useState, useRef, onMounted, onWillUnmount } = hooks;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Constants, helpers, utils // Constants, helpers, utils
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
let owlJS; let owlJS;
async function owlSourceCode() { async function owlSourceCode() {
if (owlJS) { if (owlJS) {
return owlJS;
}
const result = await fetch("../owl.js");
owlJS = await result.text();
return owlJS; return owlJS;
}
const result = await fetch("../owl.js");
owlJS = await result.text();
return owlJS;
} }
const MODES = { const MODES = {
js: "ace/mode/javascript", js: "ace/mode/javascript",
css: "ace/mode/css", css: "ace/mode/css",
xml: "ace/mode/xml" xml: "ace/mode/xml"
}; };
const DEFAULT_XML = `<templates> const DEFAULT_XML = `<templates>
@@ -72,22 +71,23 @@ if __name__ == "__main__":
* Make an iframe, with all the js, css and xml properly injected. * Make an iframe, with all the js, css and xml properly injected.
*/ */
function makeCodeIframe(js, css, xml) { function makeCodeIframe(js, css, xml) {
const sanitizedXML = xml.replace(/<!--[\s\S]*?-->/g, ""); const sanitizedXML = xml.replace(/<!--[\s\S]*?-->/g, "");
// create iframe // create iframe
const iframe = document.createElement("iframe"); const iframe = document.createElement("iframe");
iframe.className += "h-full w-full";
iframe.onload = () => { iframe.onload = () => {
const doc = iframe.contentDocument; const doc = iframe.contentDocument;
// inject js // inject js
const owlScript = doc.createElement("script"); const owlScript = doc.createElement("script");
owlScript.type = "text/javascript"; owlScript.type = "text/javascript";
owlScript.src = "../owl.js"; owlScript.src = "../owl.js";
owlScript.addEventListener("load", () => { owlScript.addEventListener("load", () => {
const script = doc.createElement("script"); const script = doc.createElement("script");
script.type = "text/javascript"; script.type = "text/javascript";
const content = ` const content = `
{ {
owl.config.mode = 'dev'; owl.config.mode = 'dev';
let templates = \`${sanitizedXML}\`; let templates = \`${sanitizedXML}\`;
@@ -95,32 +95,32 @@ function makeCodeIframe(js, css, xml) {
owl.Component.env = { qweb }; owl.Component.env = { qweb };
} }
${js}`; ${js}`;
script.innerHTML = content; script.innerHTML = content;
doc.body.appendChild(script); doc.body.appendChild(script);
}); });
doc.head.appendChild(owlScript); doc.head.appendChild(owlScript);
// inject css // inject css
const style = document.createElement("style"); const style = document.createElement("style");
style.innerHTML = css; style.innerHTML = css;
doc.head.appendChild(style); doc.head.appendChild(style);
}; };
return iframe; return iframe;
} }
/** /**
* Make a zip file containing a functioning application * Make a zip file containing a functioning application
*/ */
async function makeApp(js, css, xml) { 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 zip = new JSZip();
const processedJS = js const processedJS = js
.split("\n") .split("\n")
.map(l => (l === "" ? "" : " " + l)) .map(l => (l === "" ? "" : " " + l))
.join("\n"); .join("\n");
const JS = ` const JS = `
/** /**
* This is the javascript code defined in the playground. * This is the javascript code defined in the playground.
* In a larger application, this code should probably be moved in different * In a larger application, this code should probably be moved in different
@@ -151,277 +151,248 @@ async function start() {
start(); start();
`; `;
zip.file("app.js", JS); zip.file("app.js", JS);
zip.file("app.css", css); zip.file("app.css", css);
zip.file("app.py", APP_PY); zip.file("app.py", APP_PY);
zip.file("app.xml", xml); zip.file("app.xml", xml);
zip.file("index.html", DEFAULT_HTML); zip.file("index.html", DEFAULT_HTML);
zip.file("owl.js", owlSourceCode()); zip.file("owl.js", owlSourceCode());
return zip.generateAsync({ type: "blob" }); return zip.generateAsync({ type: "blob" });
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// SAMPLES // SAMPLES
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
function loadSamples() { function loadSamples() {
let result = SAMPLES.slice(); let result = samples.slice();
const localSample = localStorage.getItem("owl-playground-local-sample"); const localSample = localStorage.getItem("owl-playground-local-sample");
if (localSample) { if (localSample) {
const { js, css, xml } = JSON.parse(localSample); const { js, css, xml } = JSON.parse(localSample);
result.unshift({ result.unshift({
description: "Local Storage Code", description: "Local Storage Code",
code: js, code: js,
xml, xml,
css css
}); });
} }
return result; return result;
} }
function saveLocalSample(js, css, xml) { function saveLocalSample(js, css, xml) {
const str = JSON.stringify({ js, css, xml }); const str = JSON.stringify({ js, css, xml });
localStorage.setItem("owl-playground-local-sample", str); localStorage.setItem("owl-playground-local-sample", str);
} }
function deleteLocalSample() { function deleteLocalSample() {
localStorage.removeItem("owl-playground-local-sample"); localStorage.removeItem("owl-playground-local-sample");
} }
function useSamples() { function useSamples() {
const samples = loadSamples(); return 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 // 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({ class Tab extends owl.Component {
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 };
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 // Application initialization
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
async function start() { async function start() {
document.title = `${document.title} (v${owl.__info__.version})`; document.title = `${document.title} (v${owl.__info__.version})`;
const commit = `https://github.com/odoo/owl/commit/${owl.__info__.hash}`; const commit = `https://github.com/odoo/owl/commit/${owl.__info__.hash}`;
console.info(`This application is using Owl built with the following commit:`, commit); console.info(`This application is using Owl built with the following commit:`, commit);
const [templates] = await Promise.all([ const [templates] = await Promise.all([
owl.utils.loadFile("templates.xml"), owl.utils.loadFile("templates.xml"),
owl.utils.whenReady() owl.utils.whenReady()
]); ]);
const qweb = new owl.QWeb({ templates }); const qweb = new owl.QWeb({ templates });
const env = { qweb }; owl.Component.env = { qweb };
await mount(App, {target: document.body, env}); const app = new App();
app.mount(document.body);
} }
start(); start();
+21 -15
View File
@@ -1,20 +1,26 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html>
<head> <head>
<meta charset="utf-8"> <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<title>OWL Playground</title> <link
<link rel="icon" href="data:,"> rel="stylesheet"
data-name="vs/editor/editor.main"
<script src="libs/ace.js" type="text/javascript" charset="utf-8"></script> href="../../node_modules/monaco-editor/min/vs/editor/editor.main.css"
/>
<script src="../owl.js"></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> <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> <script>
<body> var require = { paths: { vs: '../../node_modules/monaco-editor/min/vs' } };
</body> </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> </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>
+38 -27
View File
@@ -1,5 +1,5 @@
const COMPONENTS = `// In this example, we show how components can be defined and created. const COMPONENTS = `// In this example, we show how components can be defined and created.
const { Component, useState, mount } = owl; const { Component, useState } = owl;
class Greeter extends Component { class Greeter extends Component {
constructor() { constructor() {
@@ -22,7 +22,8 @@ class App extends Component {
App.components = { Greeter }; App.components = { Greeter };
// Application setup // Application setup
mount(App, { target: document.body }); const app = new App();
app.mount(document.body);
`; `;
const COMPONENTS_XML = `<templates> const COMPONENTS_XML = `<templates>
@@ -49,7 +50,7 @@ const COMPONENTS_CSS = `.greeter {
const ANIMATION = `// The goal of this component is to see how the t-transition directive can be const ANIMATION = `// The goal of this component is to see how the t-transition directive can be
// used to generate simple transition effects. // used to generate simple transition effects.
const { Component, useState, mount } = owl; const { Component, useState } = owl;
class Counter extends Component { class Counter extends Component {
constructor() { constructor() {
@@ -79,7 +80,8 @@ class App extends Component {
} }
App.components = { Counter }; App.components = { Counter };
mount(App, { target: document.body }); const app = new App();
app.mount(document.body);
`; `;
const ANIMATION_XML = `<templates> const ANIMATION_XML = `<templates>
@@ -191,7 +193,7 @@ 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 // methods in the console. Try modifying its state by clicking on it, or by
// clicking on the two main buttons, and look into the console to see what // clicking on the two main buttons, and look into the console to see what
// happens. // happens.
const { Component, useState, mount } = owl; const { Component, useState } = owl;
class DemoComponent extends Component { class DemoComponent extends Component {
constructor() { constructor() {
@@ -238,7 +240,8 @@ class App extends Component {
} }
App.components = { DemoComponent }; App.components = { DemoComponent };
mount(App, { target: document.body }); const app = new App();
app.mount(document.body);
`; `;
const LIFECYCLE_DEMO_XML = `<templates> const LIFECYCLE_DEMO_XML = `<templates>
@@ -270,8 +273,7 @@ const LIFECYCLE_CSS = `button {
}`; }`;
const HOOKS_DEMO = `// In this example, we show how hooks can be used or defined. const HOOKS_DEMO = `// In this example, we show how hooks can be used or defined.
const { hooks, mount } = owl; const {useState, onMounted, onWillUnmount} = owl.hooks;
const {useState, onMounted, onWillUnmount} = hooks;
// We define here a custom behaviour: this hook tracks the state of the mouse // We define here a custom behaviour: this hook tracks the state of the mouse
// position // position
@@ -310,7 +312,8 @@ class App extends owl.Component {
} }
// Application setup // Application setup
mount(App, { target: document.body }); const app = new App();
app.mount(document.body);
`; `;
const HOOKS_DEMO_XML = `<templates> const HOOKS_DEMO_XML = `<templates>
@@ -329,7 +332,7 @@ const HOOKS_CSS = `button {
const CONTEXT_JS = `// In this example, we show how components can use the Context and 'useContext' const CONTEXT_JS = `// In this example, we show how components can use the Context and 'useContext'
// hook to share information between them. // hook to share information between them.
const { Component, Context, mount } = owl; const { Component, Context } = owl;
const { useContext } = owl.hooks; const { useContext } = owl.hooks;
class ToolbarButton extends Component { class ToolbarButton extends Component {
@@ -364,7 +367,8 @@ const themeContext = new Context({
}); });
// Add the themeContext the environment to make it available to all components // Add the themeContext the environment to make it available to all components
App.env.themeContext = themeContext; App.env.themeContext = themeContext;
mount(App, { target: document.body }); const app = new App();
app.mount(document.body);
`; `;
const CONTEXT_XML = `<templates> const CONTEXT_XML = `<templates>
@@ -391,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 // In this implementation, we use the owl Store class to manage the state. It
// is very similar to the VueX store. // is very similar to the VueX store.
const { Component, useState, mount } = owl; const { Component, useState } = owl;
const { useRef, useStore, useDispatch, onPatched, onMounted } = owl.hooks; const { useRef, useStore, useDispatch, onPatched, onMounted } = owl.hooks;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -564,7 +568,8 @@ function makeStore() {
} }
TodoApp.env.store = makeStore(); TodoApp.env.store = makeStore();
mount(TodoApp, { target: document.body }); const app = new TodoApp();
app.mount(document.body);
`; `;
const TODO_APP_STORE_XML = `<templates> const TODO_APP_STORE_XML = `<templates>
@@ -1055,7 +1060,8 @@ function setupResponsivePlugin(env) {
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
setupResponsivePlugin(App.env); setupResponsivePlugin(App.env);
owl.mount(App, { target: document.body }); const app = new App();
app.mount(document.body);
`; `;
const RESPONSIVE_XML = `<templates> const RESPONSIVE_XML = `<templates>
@@ -1167,7 +1173,7 @@ 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 // Note that the t-on-click event, defined in the App template, is executed in
// the context of the App component, even though it is inside the Card component // the context of the App component, even though it is inside the Card component
const { Component, useState, mount } = owl; const { Component, useState } = owl;
class Card extends Component { class Card extends Component {
constructor() { constructor() {
@@ -1205,8 +1211,8 @@ class App extends Component {
App.components = {Card, Counter}; App.components = {Card, Counter};
// Application setup // Application setup
mount(App, { target: document.body }); const app = new App();
`; app.mount(document.body);`;
const SLOTS_XML = `<templates> const SLOTS_XML = `<templates>
<div t-name="Card" class="card" t-att-class="state.showContent ? 'full' : 'small'"> <div t-name="Card" class="card" t-att-class="state.showContent ? 'full' : 'small'">
@@ -1292,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 // 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 // because of the slow component. We use the AsyncRoot component for this
// purpose. Try removing it to see the difference. // purpose. Try removing it to see the difference.
const { Component, useState, mount } = owl; const { Component, useState } = owl;
const { AsyncRoot } = owl.misc; const { AsyncRoot } = owl.misc;
class SlowComponent extends Component { class SlowComponent extends Component {
@@ -1323,7 +1329,8 @@ class App extends Component {
} }
App.components = {SlowComponent, NotificationList, AsyncRoot}; App.components = {SlowComponent, NotificationList, AsyncRoot};
mount(App, { target: document.body }); const app = new App();
app.mount(document.body);
`; `;
const ASYNC_COMPONENTS_XML = `<templates> const ASYNC_COMPONENTS_XML = `<templates>
@@ -1378,7 +1385,7 @@ 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. // 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 // Note that there are two controls with t-model="color": they are totally
// synchronized. // synchronized.
const { Component, useState, mount } = owl; const { Component, useState } = owl;
class Form extends Component { class Form extends Component {
constructor() { constructor() {
@@ -1394,7 +1401,8 @@ class Form extends Component {
} }
// Application setup // Application setup
mount(Form, { target: document.body }); const form = new Form();
form.mount(document.body);
`; `;
const FORM_XML = `<templates> const FORM_XML = `<templates>
@@ -1440,7 +1448,7 @@ const PORTAL_COMPONENTS = `
// This shows the expected use case of Portal // This shows the expected use case of Portal
// which is to implement something similar // which is to implement something similar
// to bootstrap modal // to bootstrap modal
const { Component, useState, mount } = owl; const { Component, useState } = owl;
const { Portal } = owl.misc; const { Portal } = owl.misc;
class Modal extends Component {} class Modal extends Component {}
@@ -1462,7 +1470,8 @@ class App extends Component {
App.components = { Dialog , Interstellar }; App.components = { Dialog , Interstellar };
// Application setup // Application setup
mount(App, { target: document.body }); const app = new App();
app.mount(document.body);
`; `;
const PORTAL_XML = ` const PORTAL_XML = `
@@ -1550,7 +1559,7 @@ const WMS = `// This example is slightly more complex than usual. We demonstrate
// - minimal width/height // - minimal width/height
// - better heuristic for initial window position // - better heuristic for initial window position
// - ... // - ...
const { Component, useState, mount } = owl; const { Component, useState } = owl;
const { useRef } = owl.hooks; const { useRef } = owl.hooks;
class HelloWorld extends Component {} class HelloWorld extends Component {}
@@ -1690,7 +1699,8 @@ const windows = [
]; ];
App.env.windows = windows; App.env.windows = windows;
mount(App, { target: document.body }); const app = new App();
app.mount(document.body);
`; `;
const WMS_XML = `<templates> const WMS_XML = `<templates>
@@ -1808,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 // Note that this example has no external xml or css file, everything is
// contained in a single js file. // contained in a single js file.
const { Component, useState, tags, mount } = owl; const { Component, useState, tags } = owl;
const { xml, css } = tags; const { xml, css } = tags;
// Counter component // Counter component
@@ -1840,7 +1850,8 @@ App.template = APP_TEMPLATE;
App.components = { Counter }; App.components = { Counter };
// Application setup // Application setup
mount(App, { target: document.body }); const app = new App();
app.mount(document.body);
`; `;
export const SAMPLES = [ export const SAMPLES = [
+74 -52
View File
@@ -1,57 +1,79 @@
<templates> <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 t-name="App" t-on-run-project="_run" class="h-full flex">
<div class="left-bar" t-att-class="{split: state.splitLayout}"
t-att-style="leftPaneStyle" <Editor js="state.js" xml="state.xml" css="state.css" />
t-on-updateCode="updateCode">
<div class="menubar">
<a class="btn run-code flash" t-on-click="runCode" title="Execute this Code">▶ Run</a> <div class="flex flex-col w-1/2 h-full">
<select t-on-change="setSample">
<option t-foreach="SAMPLES" t-as="sample" t-key="sample_index"> <div class="bg-purple-300 mx-2 my-1 p-2 rounded-sm" style="background-color: #865A7B">
<t t-esc="sample.description"/> <div class="flex items-center">
</option> <div class="relative">
</select> <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">
<a class="btn flash" t-on-click="downloadCode" title="Download a Zip with this Code"><i class="fas fa-download"></i></a> <t t-foreach="samples" t-as="sample" t-key="sample_index">
<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> <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> </div>
<TabbedEditor
js="state.js"
css="!state.splitLayout and state.css" </div>
xml="!state.splitLayout and state.xml"
t-att-style="topEditorStyle"/> <t t-name="Editor" t-on-open-file="_openFile">
<t t-if="state.splitLayout"> <div class="resize-x w-1/2">
<div class="separator horizontal"/>
<TabbedEditor <div class="flex justify-between items-center" style="background-color: #865A7B">
js="false" <ul class="flex justify-start">
css="state.css" <li>
xml="state.xml" <Tab t-on-click="_openXML">
resizeable="true" xml
t-on-updatePanelHeight="updatePanelHeight"/> </Tab>
</t> </li>
</div> <li>
<div class="separator vertical" t-on-mousedown="onMouseDown"/> <Tab t-on-click="_openJS">
<div class="right-pane" t-att-style="rightPaneStyle"> js
<div class="welcome" t-if="state.displayWelcome"> </Tab>
<div>🦉 Odoo Web Library 🦉</div> </li>
<div>v<t t-esc="version"/></div> <li>
<div class="url"><a href="https://github.com/odoo/owl">https://github.com/odoo/owl</a></div> <Tab t-on-click="_openCSS">
<div class="note"> css
<p>Note: these examples are using recent features of Javascript, and require a recent browser to work without a transpilation step! </Tab>
For example, it makes use of class fields and class static fields. These examples should work in a recent Chrome version. </li>
</p> </ul>
</div> <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>
<div class="content" t-ref="content"/> </t>
</div>
</div> <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> </templates>
+58 -23
View File
@@ -3,9 +3,10 @@ const readline = require("readline");
const fs = require("fs"); const fs = require("fs");
const exec = require("child_process").exec; const exec = require("child_process").exec;
const chalk = require("chalk"); const chalk = require("chalk");
const GitHub = require("github-api");
const REL_NOTES_FILE = `release-notes.md`; const REL_NOTES_FILE = `release-notes.md`;
const STEPS = 8; const STEPS = 10;
const rl = readline.createInterface({ const rl = readline.createInterface({
input: process.stdin, input: process.stdin,
@@ -27,8 +28,7 @@ async function startRelease() {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
log(`Step 1/${STEPS}: collecting info...`); log(`Step 1/${STEPS}: collecting info...`);
const current = package.version; const current = package.version;
let next = await ask("Next version: "); const next = await ask("Next version: ");
if (next[0] === 'v') next = next.substring(1);
let file = await ask(`Release notes (${REL_NOTES_FILE}): `); let file = await ask(`Release notes (${REL_NOTES_FILE}): `);
file = file || REL_NOTES_FILE; file = file || REL_NOTES_FILE;
let content; let content;
@@ -39,12 +39,11 @@ async function startRelease() {
log("Cannot find release notes... Aborting"); log("Cannot find release notes... Aborting");
return; return;
} }
let shouldBeDraft = await ask(`Should be a draft [y/n] ? (n)`);
let draft = "" // Todo: add playground update feature
if (shouldBeDraft.toLowerCase() === 'y') // let shouldUpdateStr = await ask("Update Playground? (y/n)");
{ // const shouldUpdatePlayground = shouldUpdateStr === "y";
draft = "--draft"; const token = await ask("Github token: ");
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
log(`Step 2/${STEPS}: running tests...`); log(`Step 2/${STEPS}: running tests...`);
@@ -53,12 +52,14 @@ async function startRelease() {
log("Test suite does not pass. Aborting."); log("Test suite does not pass. Aborting.");
return; return;
} }
await ask("Ready for next step...");
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
log(`Step 3/${STEPS}: updating package.json, readme.md and roadmap.md...`); log(`Step 3/${STEPS}: updating package.json, readme.md and roadmap.md...`);
await replaceInFile("./package.json", current, next); await replaceInFile("./package.json", current, next);
await replaceInFile("./README.md", current, next); await replaceInFile("./README.md", current, next);
await replaceInFile("./roadmap.md", current, next); await replaceInFile("./roadmap.md", current, next);
await ask("Ready for next step...");
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
log(`Step 4/${STEPS}: creating git commit...`); log(`Step 4/${STEPS}: creating git commit...`);
@@ -67,40 +68,61 @@ async function startRelease() {
log("Git commit failed. Aborting."); log("Git commit failed. Aborting.");
return; return;
} }
await ask("Ready for next step...");
// ---------------------------------------------------------------------------- // -------------------j--------------------------------------------------------
log(`Step 5/${STEPS}: building owl...`); log(`Step 5/${STEPS}: building owl (iife version)...`);
await execCommand("npm run prettier");
const buildResult = await execCommand("npm run build"); const buildResult = await execCommand("npm run build");
if (buildResult !== 0) { if (buildResult !== 0) {
log("Build failed. Aborting."); log("Build failed. Aborting.");
return; return;
} }
await ask("Ready for next step...");
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
log(`Step 6/${STEPS}: pushing on github...`); 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"); const pushResult = await execCommand("git push");
if (pushResult !== 0) { if (pushResult !== 0) {
log("git push failed. Aborting."); log("git push failed. Aborting.");
return; 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`); log(`Step 9/${STEPS}: adding assets to release...`);
if (relaseResult !== 0) { await ask("Please add owl.js and owl.min.js to draft release, then confirm");
log("github release failed. Aborting."); // todo: do this with curl
return; // 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"); await execCommand("npm run publish");
log("Owl Release process completed! Thank you for your patience"); log("Owl Release process completed! Thank you for your patience");
await execCommand(`gh release view`);
await execCommand(`gh release view -w`);
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@@ -177,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);
}
});
});
}
+1 -1
View File
@@ -16,7 +16,7 @@ URL = 'http://{0}:{1}/tools'.format(HOST, PORT)
class OWLHandler(SimpleHTTPRequestHandler): class OWLHandler(SimpleHTTPRequestHandler):
def do_GET(self): def do_GET(self):
if self.path == '/tools/owl.js': if self.path == '/tools/owl.js':
self.path = '/dist/owl.iife.js' self.path = '/dist/owl.js'
return SimpleHTTPRequestHandler.do_GET(self) return SimpleHTTPRequestHandler.do_GET(self)
def end_headers(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": { "compilerOptions": {
                                                            // Main options "module": "commonjs",
"target": "esnext",                                         // Specify ECMAScript target version: 'es3' (default), 'es5', 'es2015', 'es2016', 'es2017','es2018' or 'esnext'. "preserveConstEnums": true,
"module": "esnext",                                         // Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. "noImplicitThis": true,
// "lib": ["esnext", "dom"],                 // Specify library files to be included in the compilation. "removeComments": false,
// "allowJs": false,                 // Allow javascript files to be compiled. "declaration": true,
// "checkJs": false,                 // Report errors in .js files. "target": "esnext",
// "outFile": "./",                 // Concatenate and emit output to single file. "outDir": "dist",
"outDir": "dist",                                           // Redirect output structure to the directory. "alwaysStrict": true,
// "rootDir": "./",                 // Specify the root directory of input files. Use to control the output directory structure with `--outDir`. "noUnusedLocals": true,
// "project": "",                 // Compile a project given a valid configuration file. "noUnusedParameters": false,
                                                            // Compilation options "noImplicitReturns": true,
// "composite": true,                 // Enable project compilation "noFallthroughCasesInSwitch": true,
// "diagnostics": false,                 // Show diagnostic information. "strictPropertyInitialization": true,
// "incremental": true,                 // Enable incremental compilation by reading/writing information from prior compilations to a file on disk. "strictNullChecks": true,
// "isolatedModules": false,                 // Transpile each file as a separate module (similar to 'ts.transpileModule'). "declarationDir": "dist/types"
// "listEmittedFiles": false,                 // Print names of generated files part of the compilation. },
// "listFiles": true,                 // Print names of files part of the compilation. "include": ["src/**/*.ts","src/*.ts"]
// "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.
}
} }