Compare commits

..

50 Commits

Author SHA1 Message Date
Géry Debongnie b859fcb8f0 [REL] bump to v0.22.0 2019-10-01 21:05:04 +02:00
Géry Debongnie 3005756d89 [LEGAL] add copyright file 2019-10-01 20:44:39 +02:00
Géry Debongnie f6820bb8ac [REF] hooks: better implementation of onMounted/onWillUnmount 2019-09-28 21:14:40 +02:00
Géry Debongnie 090880f478 [DOC] readme: add quick overview section 2019-09-27 17:34:27 +02:00
Géry Debongnie 5cdaa7b473 [IMP] hooks: implement useRef hook
closes #194
2019-09-27 11:20:08 +02:00
Géry Debongnie 91b9e78182 [DOC] update component, tooling doc 2019-09-27 11:20:08 +02:00
Géry Debongnie 5335fb8fba [IMP] components: add hooks mechanism
part of #194
2019-09-27 11:20:08 +02:00
Géry Debongnie 4513e3f31c [FIX] qweb: improved svg support for components
If we want to generate dynamically svg components, it is useful to be
able to define components with a <g> tag as root, and then getting the
correct namespace.

closes #302
2019-09-26 15:46:42 +02:00
Géry Debongnie ecf12f6eee [FIX] qweb: properly supports svg
Snabbdom supports svg, but it adds the namespace at the creation of the
virtual node.  However, owl works slightly differently: it adds children
after creating the parent virtual node.

So, we need to actually call the addNS method after the children nodes
have been created.

As a bonus, this is slightly faster than snabbdom: we only check at
template compilation time once if a node is a svg.
2019-09-24 21:40:46 +02:00
Géry Debongnie 82a6961b3a [FIX] component: properly handle wrapper component and slots
closes #297
2019-09-23 07:13:51 +02:00
Géry Debongnie 78a4550ebb [IMP] component tags: allow injecting values in xml tag string
With this commit, we can now do this:

      const SUBTEMPLATE = xml`<span><t t-esc="state.n"/></span>`
      class Parent extends Widget {
          static template = xml`<div><t t-call="${SUBTEMPLATE}"/></div>`
          state = {n: 42};
      }
2019-09-21 23:05:44 +02:00
Géry Debongnie cc82b3ffcd [IMP] component: support dynamic t-component 2019-09-21 21:58:19 +02:00
Géry Debongnie bbbaf95c8e Merge pull request #296 from odoo/issue-293-owl-fibers
[MERGE] component: introduce owl fiber
2019-09-20 13:54:59 +02:00
Géry Debongnie caa63628dc [REF] component: refactor fiber creation methods 2019-09-20 11:14:50 +02:00
Géry Debongnie 7a49b9d94e [REF] component: move render props and promise to fiber
part of #293
2019-09-20 10:48:10 +02:00
Géry Debongnie 01eb338e69 [REF] component: move patchqueue management to fiber
part of #293
2019-09-20 10:47:58 +02:00
Géry Debongnie 3048c6f961 [REF] component: introduce fiber
part of #293
2019-09-20 10:47:55 +02:00
Géry Debongnie 8edc637033 [FIX] qweb: remove comments in xml strings before parsing
It was done in loadTemplates, but this does not work if we want to add
templates from another source.
2019-09-19 15:18:59 +02:00
Géry Debongnie c9910077a4 [FIX] qweb: do not capture current qweb instance in closure
it is not needed, and has a bad effect on globally registered templates.
2019-09-12 23:23:55 +02:00
Géry Debongnie 6bf70f016c [DOC] doc: update readme.md 2019-09-12 23:23:39 +02:00
Géry Debongnie 905619f80f [ADD] tools: add benchmark for 0.21.0 2019-09-12 14:50:56 +02:00
Géry Debongnie 14ceb382ef [REL] bump to v0.21.0 2019-09-12 14:21:36 +02:00
Géry Debongnie 9846b2e997 [IMP] tags: introduce xml tag
Big change! This commit introduces an xml function tag to easily define
inline templates.

This is a pretty big change toward single file owl components

Part of #284
2019-09-12 14:17:21 +02:00
Géry Debongnie cfc2fb2ba2 [REF] qweb: rename register method to registerComponent 2019-09-12 14:17:21 +02:00
Géry Debongnie d57017b6ed [IMP] qweb: add registerTemplate method 2019-09-12 14:17:21 +02:00
Géry Debongnie 5bf7a5c671 [IMP] component: make template key static
closes #288
2019-09-12 14:17:21 +02:00
Géry Debongnie 9b589af75a [REF] qweb: improve generated code in some cases 2019-09-12 09:43:09 +02:00
Géry Debongnie 471fd49063 [IMP] router: replace t-routecomponent directive by RouteComponent
closes #287
2019-09-11 14:58:36 +02:00
Géry Debongnie 4fc3423682 [IMP] component: support dynamic t-props
It is useful in some rare situations.

closes #144
2019-09-11 14:12:52 +02:00
Géry Debongnie ffb3263c79 [IMP] component: allow dynamic root nodes
closes #283
2019-09-11 13:12:57 +02:00
Géry Debongnie e1acf66143 [IMP] component: components key is now static
This is a breaking change!

closes #279
2019-09-11 11:18:49 +02:00
Géry Debongnie 2a40907e35 [FIX] qweb: do not crash in some rare t-call situation
Before this commit, QWeb crashed when it had to deal with a t-call with
multiple sub nodes on the same line, and one text node:

<t t-call="SomeTemplate">
  <span>hey</span> <span>hey</span>
</t>

This was because we need to compile the content to be able to extract
all possible t-set t-value statements. But doing so means that we had a
multiple root templates, which caused a crash in this case.

Solving this issue is simple: the sub template is compiled with the flag
allowMultipleRoots set to true.
2019-09-09 11:51:33 +02:00
Géry Debongnie 1c0dd79955 [MISC] update package.json 2019-09-05 21:44:29 +02:00
Géry Debongnie 9fad164c79 [REL] bump to v0.20.0 2019-09-05 14:41:12 +02:00
Géry Debongnie 53a8841914 [TEST] component: snapshots slot functions 2019-09-05 14:33:43 +02:00
Géry Debongnie 19f0863aa3 [IMP] qweb: allow recursive templates t-calls
closes #267
2019-09-05 14:33:43 +02:00
Géry Debongnie 28f7b44f9e [IMP] component: make t-model works with sub state keys
closes #273
2019-09-05 08:47:37 +02:00
Géry Debongnie fe46a6ccee [CLEANUP] run prettier on some code 2019-09-04 22:48:37 +02:00
Géry Debongnie 3c23a7b246 [FIX] component: t-model properly works with select
The initial value was not set on the select tag.

closes #271
2019-09-04 22:45:21 +02:00
Géry Debongnie c52799de2c [FIX] ConnectedComponent: keep props uptodate
This is a tricky bug. The problem is that a ConnectedComponent can
trigger a rendering before it is aware that there is a state change in
the store, and just before the store update event comes in.

What we do in this commit is to update the storeProps whenever a
rendering is scheduled.  However, we need to keep the rendering
information as a promise to give it in some case to the __checkUpdate
method (see the note in the render method)

closes #268
2019-09-04 15:36:10 +02:00
Géry Debongnie 4edb90f44b [FIX] router: properly handle popstate
It was not correct, the url was changed multiple times and back
navigation was broken.

Also, note that there are no tests because the history api is not
available in jsdom.
2019-09-04 13:59:54 +02:00
Géry Debongnie 0c1a7aabae [FIX] component: better templateId for multiple subcomponents
Of course, the templateId computation is kind of tricky.  Here, an issue
occurred because the templateId did not take the componentId into
account when we were in a loop, but with no keyed parent.

This meant that multiple sub components shared the same templateId,
confusing the vdom algorithm.
2019-09-03 16:38:24 +02:00
Géry Debongnie cfdce29ce7 [FIX] component: avoid key collisions in sub components
This is a difficult part of owl: we need to be able to reconcile the
vdom generated (this is done with our virtual dom algorithm), but also
to be able to reconcile the proper components.

The issue here is that when we are in a list, with a t-key attribute on all
list nodes, and those list nodes contains sub component, then the
component system used the index of the list, and did not take into
account the key from the parent.  The fix is to keep track of the
current key in the context, and uses that as a part of the templateId.
2019-09-03 12:13:57 +02:00
Géry Debongnie b344d73e07 [FIX] component: prevent collision between template ids and keys 2019-09-02 15:27:37 +02:00
Géry Debongnie 4cd08d25c8 [FIX] component: solve tricky concurrency issue
In some specific situation, a component could crash, because it was
destroyed before being mounted, but reused anyway for another rendering.
2019-09-01 21:24:54 +02:00
Géry Debongnie f9861040c8 [FIX] router: properly react on hashchange
Before this commit, hashchanges were not taken into account by the
router, if used in history mode.

Also, it's stupid, but i ran prettier on the codebase
2019-09-01 11:45:13 +02:00
Géry Debongnie 40d8090232 [FIX] router: update app if parameterized route changes 2019-08-31 15:03:17 +02:00
Géry Debongnie 55be6437f1 [FIX] router: better support for hash mode 2019-08-31 09:55:45 +02:00
Géry Debongnie 6ff4dd74bf [DOC] readme: improve main example 2019-08-28 12:22:32 +02:00
Géry Debongnie 3628b49c37 [DOC] update readme (swap some sections around) 2019-08-28 12:05:53 +02:00
65 changed files with 8781 additions and 1975 deletions
+14
View File
@@ -0,0 +1,14 @@
Most of the files are
Copyright (c) 2004-2015 Odoo S.A.
Many files also contain contributions from third
parties. In this case the original copyright of
the contributions can be traced through the
history of the source version control system.
When that is not the case, the files contain a prominent
notice stating the original copyright and applicable
license, or come with their own dedicated COPYRIGHT
and/or LICENSE file.
+216 -59
View File
@@ -1,20 +1,68 @@
<h1 align="center">🦉 <a href="https://odoo.github.io/owl/">Odoo Web Library</a> 🦉</h1> <h1 align="center">🦉 <a href="https://odoo.github.io/owl/">Odoo Web Library</a> 🦉</h1>
_A web framework for structured, dynamic and maintainable applications_ _A no nonsense web framework for structured, dynamic and maintainable applications_
## Project Overview ## Project Overview
The Odoo Web Library (OWL) is a small (~16kb gzipped) UI framework intended to be the basis for The Odoo Web Library (OWL) is a smallish (~17kb gzipped) UI framework intended to
the [Odoo](https://www.odoo.com/) Web Client, and hopefully many other Odoo be the basis for the [Odoo](https://www.odoo.com/) Web Client. Owl is a modern
related projects. OWL's main features are: framework, written in Typescript, taking the best ideas from React and Vue in a
simple and consistent way. Owl's main features are:
- a _declarative component system_, (template based, with asynchronous rendering and a virtual dom) - a declarative component system,
- a store implementation (for state management) - a reactivity system based on hooks,
- a store implementation (for state management),
- a small frontend router - a small frontend router
**Try it online!** An online playground is available at [https://odoo.github.io/owl/playground](https://odoo.github.io/owl/playground) to let you experiment with the OWL framework. Owl components are defined with ES6 classes, they use QWeb templates, an underlying
virtual dom, integrates beautifully with hooks, and the rendering is asynchronous.
## OWL's Design Principles **Try it online!** An online playground is available at [https://odoo.github.io/owl/playground](https://odoo.github.io/owl/playground) to let you experiment with the Owl framework. There
are some code examples to showcase some interesting features.
## Example
Here is a short example to illustrate interactive components:
```javascript
import { Component, QWeb, useState } from "owl";
import { xml } from "owl/tags";
class Counter extends Component {
static template = xml`
<button t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>`;
state = useState({ value: 0 });
increment() {
this.state.value++;
}
}
class App extends Component {
static template = xml`
<div>
<span>Hello Owl</span>
<Counter />
</div>`;
static components = { Counter };
}
const app = new App({ qweb: new QWeb() });
app.mount(document.body);
```
Note that the counter component is made reactive with the [`useState`](doc/hooks.md#usestate)
hook. Also, all examples here uses the `xml` helper to define inline templates.
But this is not mandatory, many applications will load templates separately.
More interesting examples can be found on the
[playground](https://odoo.github.io/owl/playground) application.
## Design Principles
OWL is designed to be used in highly dynamic applications where changing OWL is designed to be used in highly dynamic applications where changing
requirements are common and code needs to be maintained by large teams. requirements are common and code needs to be maintained by large teams.
@@ -37,66 +85,175 @@ one way to define components (with classes).
If you are interested in a comparison with React or Vue, you will If you are interested in a comparison with React or Vue, you will
find some more information [here](doc/comparison.md). find some more information [here](doc/comparison.md).
## Example
Here is a short example to illustrate interactive components:
```xml
<templates>
<button t-name="Counter" t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>
</templates>
```
```javascript
class Counter extends owl.Component {
state = { value: 0 };
increment() {
this.state.value++;
}
}
const qweb = new owl.QWeb(TEMPLATES);
const counter = new Counter({ qweb });
counter.mount(document.body);
```
Note that we assume here that the xml templates are available in the `TEMPLATES`
string. More interesting examples can be found on the
[playground](https://odoo.github.io/owl/playground) application.
## Installing/Building
If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-0.19.0.js](https://github.com/odoo/owl/releases/download/v0.19.0/owl.js)
- [owl-0.19.0.min.js](https://github.com/odoo/owl/releases/download/v0.19.0/owl.min.js)
Some npm scripts are available:
| Command | Description |
| --------------------- | ---------------------------------------------------------------------------- |
| `npm install` | install every dependency required for this project |
| `npm run build` | build a bundle of _owl_ in the _/dist/_ folder |
| `npm run minify` | minify the prebuilt owl.js file |
| `npm run test` | run all tests |
| `npm run test:watch` | run all tests, and keep a watcher |
| `npm run tools` | build tools applications, start a static server (see [here](doc/tooling.md)) |
| `npm run tools:watch` | same as `tools`, but with a watcher to rebuild owl |
## Documentation ## Documentation
The complete documentation can be found [here](doc/readme.md). The most important sections are: The complete documentation can be found [here](doc/readme.md). The most important sections are:
- [Quick Start](doc/quick_start.md) - [Quick Start](doc/quick_start.md)
- [Component](doc/component.md) - [Component](doc/component.md)
- [QWeb](doc/qweb.md) - [Hooks](doc/hooks.md)
Found an issue in the documentation? A broken link? Some outdated information? Found an issue in the documentation? A broken link? Some outdated information?
Submit a PR! Submit a PR!
## Installing/Building
If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-0.22.0.js](https://github.com/odoo/owl/releases/download/v0.22.0/owl.js)
- [owl-0.22.0.min.js](https://github.com/odoo/owl/releases/download/v0.22.0/owl.min.js)
Some npm scripts are available:
| Command | Description |
| ---------------- | -------------------------------------------------- |
| `npm install` | install every dependency required for this project |
| `npm run build` | build a bundle of _owl_ in the _/dist/_ folder |
| `npm run minify` | minify the prebuilt owl.js file |
| `npm run test` | run all (owl) tests |
## Quick Overview
Owl components in an application are used to define a (dynamic) tree of components.
```
Root
/ \
A B
/ \
C D
```
**Environment:** the root component is special: it is created with an environment,
which should contain a `QWeb` instance. The environment is then automatically
propagated to each sub components (and accessible in the `this.env` property).
```js
const env = { qweb: new QWeb() };
const app = new App(env);
app.mount(document.body);
```
The environment is mostly static. Each application is free to add anything to
the environment, which is very useful, since this can be accessed by each sub
component. Some good use case for that is some configuration keys, session
information or generic services (such as doing rpcs, or accessing local storage).
Doing it this way means that components are easily testable: we can simply
create a test environment with mock services.
**State:** each component can manage its own local state. It is a simple ES6
class, there are no special rules:
```js
class Counter extends Component {
static template = xml`
<button t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>`;
state = { value: 0 };
increment() {
this.state.value++;
this.render();
}
}
```
The example above shows a component with a local state. Note that since there
is nothing magical to the `state` object, we need to manually call the `render`
function whenever we update it. This can quickly become annoying (and not
efficient if we do it too much). There is a better way: using the `useState`
hook, which transforms an object into a reactive version of itself:
```js
const { useState } = owl.hooks;
class Counter extends Component {
static template = xml`
<button t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>`;
state = useState({ value: 0 });
increment() {
this.state.value++;
}
}
```
**Props:** sub components often needs some information from their parents. This
is done by adding the required information to the template. This will then be
accessible by the sub component in the `props` object. Note that there is an
important rule here: the information contained in the `props` object is not
owned by the sub component, and should never be modified.
```js
class Child extends Component {
static template = xml`<div>Hello <t t-esc="props.name"/></div>`;
}
class Parent extends Component {
static template = xml`
<div>
<Child name="'Owl'" />
<Child name="'Framework'" />
</div>`;
static components = { Child };
}
```
**Communication:** there are multiple ways to communicate information between
components. However, the two most important ways are the following:
- from parent to children: by using `props`,
- from a children to one of its parent: by triggering events.
The following example illustrate both mechanisms:
```js
class OrderLine extends Component {
static template = xml`
<div t-on-click="add">
<div><t t-esc="props.line.name"/></div>
<div>Quantity: <t t-esc="props.line.quantity"/></div>
</div>`;
add() {
this.trigger("add-to-order", { line: props.line });
}
}
class Parent extends Component {
static template = xml`
<div t-on-add-to-order="addToOrder">
<OrderLine
t-foreach="orders"
t-as="line"
line="line" />
</div>`;
static components = { OrderLine };
orders = useState([{ id: 1, name: "Coffee", quantity: 0 }, { id: 2, name: "Tea", quantity: 0 }]);
addToOrder(event) {
const line = event.detail.line;
line.quantity++;
}
}
```
In this example, the `OrderLine` component trigger a `add-to-order` event. This
will generate a DOM event which will bubble along the DOM tree. It will then be
intercepted by the parent component, which will then get the line (from the
`detail` key) and then increment its quantity. See the section on [event handling](doc/component.md#event-handling)
for more details on how events work.
Note that this example would have also worked if the `OrderLine` component
directly modifies the `line` object. However, this is not a good practice: this
only works because the `props` object received by the child component is reactive,
so the child component is then coupled to the parents implementation.
## License ## License
OWL is [GPL licensed](./LICENSE). OWL is [LGPL licensed](./LICENSE).
+181 -77
View File
@@ -5,6 +5,7 @@
- [Overview](#overview) - [Overview](#overview)
- [Example](#example) - [Example](#example)
- [Reference](#reference) - [Reference](#reference)
- [Reactive System](#reactive-system)
- [Properties](#properties) - [Properties](#properties)
- [Static Properties](#static-properties) - [Static Properties](#static-properties)
- [Methods](#methods) - [Methods](#methods)
@@ -22,6 +23,7 @@
- [Slots](#slots) - [Slots](#slots)
- [Asynchronous Rendering](#asynchronous-rendering) - [Asynchronous Rendering](#asynchronous-rendering)
- [Error Handling](#error-handling) - [Error Handling](#error-handling)
- [Functional Components](#functional-components)
## Overview ## Overview
@@ -45,16 +47,15 @@ exclusively done by a [QWeb](qweb.md) template (which needs to be preloaded in Q
Rendering a component generates a virtual dom representation Rendering a component generates a virtual dom representation
of the component, which is then patched to the DOM, in order to apply the changes in an efficient way. of the component, which is then patched to the DOM, in order to apply the changes in an efficient way.
OWL components observe their states, and rerender themselves whenever it is
changed. This is done by an [observer](observer.md).
## Example ## Example
Let us have a look at a simple component: Let us have a look at a simple component:
```javascript ```javascript
const { useState } = owl.hooks;
class ClickCounter extends owl.Component { class ClickCounter extends owl.Component {
state = { value: 0 }; state = useState({ value: 0 });
increment() { increment() {
this.state.value++; this.state.value++;
@@ -72,11 +73,10 @@ Note that this code is written in ESNext style, so it will only run on the
latest browsers without a transpilation step. latest browsers without a transpilation step.
This example show how a component should be defined: it simply subclasses the This example show how a component should be defined: it simply subclasses the
Component class. If no `template` key is defined, then Component class. If no static `template` key is defined, then
Owl will use the component's name as template name. Here, Owl will use the component's name as template name. Here,
a state object is defined. It is not mandatory to use the state object, but it a state object is defined, by using the `useState` hook. It is not mandatory to use the state object, but it is certainly encouraged. The result of the `useState` call is
is certainly encouraged. The state object is [observed](observer.md), and any [observed](observer.md), and any change to it will cause a rerendering.
change to it will cause a rerendering.
## Reference ## Reference
@@ -89,6 +89,25 @@ Be aware that the name of the component may be significant: if a component does
not define a `template` key, then Owl will lookup in QWeb to not define a `template` key, then Owl will lookup in QWeb to
find a template with the component name (or one of its ancestor). find a template with the component name (or one of its ancestor).
### Reactive system
OWL components can be made reactive by observing some part of their state. See the [hooks](hooks.md) section for more details.
The main idea is that the `useState` hook generate a proxy version of an object
(this is done by an [observer](observer.md)), which allows the component to
react to any change.
```javascript
const { useState } = owl.hooks;
class SomeComponent extends owl.Component {
state = useState({ a: 0, b: 1 });
}
```
Note that there is an important limitation: hooks need to be called in the
constructor.
### Properties ### Properties
- **`el`** (HTMLElement | null): reference to the DOM root node of the element. It is `null` when the - **`el`** (HTMLElement | null): reference to the DOM root node of the element. It is `null` when the
@@ -96,24 +115,27 @@ find a template with the component name (or one of its ancestor).
- **`env`** (Object): the component environment, which contains a QWeb instance. - **`env`** (Object): the component environment, which contains a QWeb instance.
- **`template`** (string, optional): if given, this is the name of the QWeb template that will render
the component.
- **`state`** (Object): this is the location of the component's state, if there is
any. After the willStart method, the `state` property is observed, and each
change will cause the component to rerender itself.
- **`props`** (Object): this is an object given (in the constructor) by the parent - **`props`** (Object): this is an object given (in the constructor) by the parent
to configure the component. It can be dynamically changed later by the parent, to configure the component. It can be dynamically changed later by the parent,
in some case. Note that `props` are owned by the parent, not by the component. in some case. Note that `props` are owned by the parent, not by the component.
As such, it should not ever be modified by the component!! As such, it should not ever be modified by the component!!
- **`refs`** (Object): the `refs` object contains all references to sub DOM nodes
or sub components defined by a `t-ref` directive in the component's template.
### Static Properties ### Static Properties
- **`props`** (Object, optional): if given, this is an object that describes the - **`template`** (string, optional): if given, this is the name of the QWeb template that will render the component. Note that there is a helper `xml` to
make it easy to define an inline template.
* **`components`** (Object, optional): if given, this is an object that contains
the classes of any sub components needed by the template. This is the main way
used by Owl to be able to create sub components.
```js
class ParentComponent extends owl.Component {
static components = { SubComponent };
}
```
* **`props`** (Object, optional): if given, this is an object that describes the
type and shape of the (actual) props given to the component. If Owl mode is type and shape of the (actual) props given to the component. If Owl mode is
`dev`, this will be used to validate the props each time the component is `dev`, this will be used to validate the props each time the component is
created/updated. See [Props Validation](#props-validation) for more information. created/updated. See [Props Validation](#props-validation) for more information.
@@ -127,7 +149,7 @@ find a template with the component name (or one of its ancestor).
} }
``` ```
* **`defaultProps`** (Object, optional): if given, this object define default - **`defaultProps`** (Object, optional): if given, this object define default
values for (top-level) props. Whenever `props` are given to the object, they values for (top-level) props. Whenever `props` are given to the object, they
will be altered to add default value (if missing). Note that it does not will be altered to add default value (if missing). Note that it does not
change the initial object, a new object will be created instead. change the initial object, a new object will be created instead.
@@ -226,7 +248,7 @@ component.
```javascript ```javascript
constructor(parent, props) { constructor(parent, props) {
super(parent, props); super(parent, props);
this.state = {someValue: true}; this.state = useState({someValue: true});
this.template = 'mytemplate'; this.template = 'mytemplate';
} }
``` ```
@@ -236,7 +258,7 @@ implemented in most cases:
```javascript ```javascript
class ClickCounter extends owl.Component { class ClickCounter extends owl.Component {
state = { value: 0 }; state = useState({ value: 0 });
... ...
} }
@@ -261,9 +283,6 @@ At this point, the component is not yet rendered. Note that a slow `willStart` m
interface. Therefore, some care should be made to make this method as interface. Therefore, some care should be made to make this method as
fast as possible. fast as possible.
After the `willStart` method is completed, the state will be observed with a
new `Observer`. Then, the component will be rendered by `QWeb`.
#### `mounted()` #### `mounted()`
`mounted` is called each time a component is attached to the `mounted` is called each time a component is attached to the
@@ -277,10 +296,9 @@ always be unmounted at some point in the future.
The mounted method will be called recursively on each of its children. First, The mounted method will be called recursively on each of its children. First,
the parent, then all its children. the parent, then all its children.
Note that the state is now observed. It is however allowed (but not encouraged) It is allowed (but not encouraged) to modify the state in the `mounted` hook.
to modify the state in the `mounted` hook. Doing so will cause a rerender, Doing so will cause a rerender, which will not be perceptible by the user, but
which will not be perceptible by the user, but will slightly slow down the will slightly slow down the component.
component.
#### `willUpdateProps(nextProps)` #### `willUpdateProps(nextProps)`
@@ -305,7 +323,7 @@ It is not called on the initial render. This is useful to read some
information from the DOM. For example, the current position of the information from the DOM. For example, the current position of the
scrollbar. scrollbar.
Note that modifying the state object is not allowed here. This method is called just Note that modifying the state is not allowed here. This method is called just
before an actual DOM patch, and is only intended to be used to save some local before an actual DOM patch, and is only intended to be used to save some local
DOM state. Also, it will not be called if the component is not in the DOM (this can DOM state. Also, it will not be called if the component is not in the DOM (this can
happen with components with `t-keepalive`). happen with components with `t-keepalive`).
@@ -387,9 +405,9 @@ easy to test a component.
Updating the environment is not as simple as changing a component's state: its Updating the environment is not as simple as changing a component's state: its
content is not observed, so updates will not be reflected immediately in the content is not observed, so updates will not be reflected immediately in the
user interface. There is however a mechanism to force root widgets to rerender user interface. There is however a mechanism to force root widgets to rerender
themselves whenever the environment is modified: one only needs to trigger the themselves whenever the environment is modified: one only needs to call the
`update` event on the QWeb instance. For example, a responsive environment `forceUpdate` method on the QWeb instance. For example, a responsive environment
could be programmed like this: could be done like this:
```js ```js
function setupResponsivePlugin(env) { function setupResponsivePlugin(env) {
@@ -398,7 +416,7 @@ function setupResponsivePlugin(env) {
const updateEnv = owl.utils.debounce(() => { const updateEnv = owl.utils.debounce(() => {
if (env.isMobile !== isMobile()) { if (env.isMobile !== isMobile()) {
env.isMobile = !env.isMobile; env.isMobile = !env.isMobile;
env.qweb.trigger("update"); env.qweb.forceUpdate();
} }
}, 15); }, 15);
window.addEventListener("resize", updateEnv); window.addEventListener("resize", updateEnv);
@@ -420,7 +438,7 @@ to be capitalized.
```js ```js
class ParentComponent extends owl.Component { class ParentComponent extends owl.Component {
components = { MyComponent: MyComponent}; static components = { MyComponent: MyComponent};
... ...
} }
``` ```
@@ -432,7 +450,8 @@ dynamic. If it is necessary to give a string, this can be done by quoting it:
`someString="'somevalue'"`. `someString="'somevalue'"`.
Note that the rendering context for the template is the component itself. This means Note that the rendering context for the template is the component itself. This means
that the template can access `state`, `props`, `env`, or any methods defined in the component. that the template can access `state` (if it exists), `props`, `env`, or any
methods defined in the component.
```xml ```xml
<div t-name="ParentComponent"> <div t-name="ParentComponent">
@@ -442,16 +461,16 @@ that the template can access `state`, `props`, `env`, or any methods defined in
```js ```js
class ParentComponent { class ParentComponent {
components = { ChildComponent }; static components = { ChildComponent };
state = { val: 4 }; state = useState({ val: 4 });
} }
``` ```
Whenever the template is rendered, it will automatically create the subcomponent Whenever the template is rendered, it will automatically create the subcomponent
`ChildComponent` at the correct place. It needs to find the reference to the `ChildComponent` at the correct place. It needs to find the reference to the
actual component class in the special `components` key, or the class registered in actual component class in the special static `components` key, or the class registered in
QWeb's global registry (see `register` function of QWeb). It first looks inside QWeb's global registry (see `register` function of QWeb). It first looks inside
the local `components` key, then fallbacks on the global registry. the static `components` key, then fallbacks on the global registry.
_Props_: In this example, the child component will receive the object `{count: 4}` in its _Props_: In this example, the child component will receive the object `{count: 4}` in its
constructor. This will be assigned to the `props` variable, which can be accessed constructor. This will be assigned to the `props` variable, which can be accessed
@@ -461,7 +480,8 @@ the sub component will also be updated automatically.
Note that there are some restrictions on prop names: `class`, `style` and any Note that there are some restrictions on prop names: `class`, `style` and any
string which starts with `t-` are not allowed. string which starts with `t-` are not allowed.
The `t-component` directive can also be used to accept dynamic values with string interpolation (like the [`t-attf-`](qweb.md#dynamic-attributes) directive): It is not common, but sometimes we need a dynamic component name and/or dynamic props. In this case,
the `t-component` directive can also be used to accept dynamic values with string interpolation (like the [`t-attf-`](qweb.md#dynamic-attributes) directive):
```xml ```xml
<div t-name="ParentComponent"> <div t-name="ParentComponent">
@@ -471,11 +491,51 @@ The `t-component` directive can also be used to accept dynamic values with strin
```js ```js
class ParentComponent { class ParentComponent {
components = { ChildComponent1, ChildComponent2 }; static components = { ChildComponent1, ChildComponent2 };
state = { id: 1 }; state = { id: 1 };
} }
``` ```
And the `t-props` directive can be used to specify totally dynamic props:
```xml
<div t-name="ParentComponent">
<Child t-props="some.obj"/>
</div>
```
```js
class ParentComponent {
static components = { Child };
some = { obj: { a: 1, b: 2 } };
}
```
There is an even more dynamic way to use `t-component`: its value can be an
expression evaluating to an actual component class. In that case, this is the
class that will be used to create the component:
```js
class A extends Component<any, any, any> {
static template = xml`<span>child a</span>`;
}
class B extends Component<any, any, any> {
static template = xml`<span>child b</span>`;
}
class App extends Component<any, any, any> {
static template = xml`<t t-component="myComponent" t-key="state.child"/>`;
state = { child: "a" };
get myComponent() {
return this.state.child === "a" ? A : B;
}
}
```
In this example, the component `App` selects dynamically the concrete sub
component class.
**CSS and style:** there is some specific support to allow the parent to declare **CSS and style:** there is some specific support to allow the parent to declare
additional css classes or style for the sub component: css declared in `class`, `style`, `t-att-class` or `t-att-style` will be added to the additional css classes or style for the sub component: css declared in `class`, `style`, `t-att-class` or `t-att-style` will be added to the
root component element. root component element.
@@ -591,7 +651,7 @@ form!). A possible way to do this is to do it by hand:
```js ```js
class Form extends owl.Component { class Form extends owl.Component {
state = { text: "" }; state = useState({ text: "" });
_updateInputValue(event) { _updateInputValue(event) {
this.state.text = event.target.value; this.state.text = event.target.value;
@@ -611,8 +671,9 @@ plumbing code is slightly different if you need to interact with a checkbox,
or with radio buttons, or with select tags. or with radio buttons, or with select tags.
To help with this situation, Owl has a builtin directive `t-model`: its value To help with this situation, Owl has a builtin directive `t-model`: its value
is the (top-level) name in the state object. With the `t-model` directive, we should be an observed value in the component (usually `state.someValue`). With
can write a shorter code, equivalent to the previous example: the `t-model` directive, we can write a shorter code, equivalent to the previous
example:
```js ```js
class Form extends owl.Component { class Form extends owl.Component {
@@ -622,7 +683,7 @@ class Form extends owl.Component {
```xml ```xml
<div> <div>
<input t-model="text" /> <input t-model="state.text" />
<span t-esc="state.text" /> <span t-esc="state.text" />
</div> </div>
``` ```
@@ -632,11 +693,11 @@ The `t-model` directive works with `<input>`, `<input type="checkbox">`,
```xml ```xml
<div> <div>
<div>Text in an input: <input t-model="someVal"/></div> <div>Text in an input: <input t-model="state.someVal"/></div>
<div>Textarea: <textarea t-model="otherVal"/></div> <div>Textarea: <textarea t-model="state.otherVal"/></div>
<div>Boolean value: <input type="checkbox" t-model="someFlag"/></div> <div>Boolean value: <input type="checkbox" t-model="state.someFlag"/></div>
<div>Selection: <div>Selection:
<select t-model="color"> <select t-model="state.color">
<option value="">Select a color</option> <option value="">Select a color</option>
<option value="red">Red</option> <option value="red">Red</option>
<option value="blue">Blue</option> <option value="blue">Blue</option>
@@ -645,11 +706,11 @@ The `t-model` directive works with `<input>`, `<input type="checkbox">`,
<div> <div>
Selection with radio buttons: Selection with radio buttons:
<span> <span>
<input type="radio" name="color" id="red" value="red" t-model="color"/> <input type="radio" name="color" id="red" value="red" t-model="state.color"/>
<label for="red">Red</label> <label for="red">Red</label>
</span> </span>
<span> <span>
<input type="radio" name="color" id="blue" value="blue" t-model="color" /> <input type="radio" name="color" id="blue" value="blue" t-model="state.color" />
<label for="blue">Blue</label> <label for="blue">Blue</label>
</span> </span>
</div> </div>
@@ -667,7 +728,7 @@ Like event handling, the `t-model` directive accepts some modifiers:
For example: For example:
```xml ```xml
<input t-model.lazy="someVal" /> <input t-model.lazy="state.someVal" />
``` ```
These modifiers can be combined. For instance, `t-model.lazy.number` will only These modifiers can be combined. For instance, `t-model.lazy.number` will only
@@ -901,44 +962,60 @@ Examples:
### References ### References
The `t-ref` directive helps a component keep reference to some inside part of it. The `useRef` hook is useful when we need a way to interact with some inside part
Like the `t-on` directive, it can work either on a DOM node, or on a component: of a component, rendered by Owl. It can work either on a DOM node, or on a component,
tagged by the `t-ref` directive. See the [hooks section](hooks.md#useref) for
more detail.
As a short example, here is how we could set the focus on a given input:
```xml ```xml
<div> <div>
<div t-ref="someDiv"/> <input t-ref="input"/>
<SubComponent t-ref="someComponent"/> <button t-on-click="focusInput">Click</button>
</div> </div>
``` ```
In this example, the component will be able to access the `div` and the component
inside the special `refs` variable:
```js ```js
this.refs.someDiv; import { useRef } from "owl/hooks";
this.refs.someComponent;
class SomeComponent extends Component {
inputRef = useRef("input");
focusInput() {
this.inputRef.el.focus();
}
}
``` ```
This is useful for various usecases: for example, integrating with an external The `useRef` hook can also be used to get a reference to an instance of a sub
library that needs to render itself inside an actual DOM node. Or for calling component rendered by Owl. In that case, we need to access it with the `comp`
some method on a sub component. property instead of `el`:
Note: if used on a component, the reference will be set in the `refs`
variable between `willPatch` and `patched`.
The `t-ref` directive also accepts dynamic values with string interpolation
(like the [`t-attf-`](qweb.md#dynamic-attributes) and
`t-component` directives). For example, if we have
`id` set to 44 in the rendering context,
```xml ```xml
<div t-ref="component_{{id}}"/> <div>
<SubComponent t-ref="sub"/>
<button t-on-click="doSomething">Click</button>
</div>
``` ```
```js ```js
this.refs.component_44; import { useRef } from "owl/hooks";
class SomeComponent extends Component {
static components = { SubComponent };
subRef = useRef("sub");
doSomething() {
this.subRef.comp.doSomeThingElse();
}
}
``` ```
Note that these two examples uses the suffix `ref` to name the reference. This
is not mandatory, but it is a useful convention, so we do not forget to access
it with the `el` or `comp` suffix.
### Slots ### Slots
To make generic components, it is useful to be able for a parent component to _inject_ To make generic components, it is useful to be able for a parent component to _inject_
@@ -1066,8 +1143,8 @@ For example, here is how we could implement an `ErrorBoundary` component:
``` ```
```js ```js
class ErrorBoundary extends Widget { class ErrorBoundary extends Component {
state = { error: false }; state = useState({ error: false });
catchError() { catchError() {
this.state.error = true; this.state.error = true;
@@ -1094,3 +1171,30 @@ env.qweb.on("error", null, function(error) {
// react to the error // react to the error
}); });
``` ```
### Functional Components
Owl does not exactly have functional components. However, there is an extremely
close alternative: calling sub templates.
A stateless functional component in react is usually some kind of function that
maps props to a virtual dom (often with `jsx`). So, basically, almost like a
template rendered with `props`. In Owl, this can be done by
simply defining a template, that will access the `props` object:
```js
const Welcome = xml`<h1>Hello, {props.name}</h1>`;
class MyComponent extends Component {
static template = xml`
<div>
<t t-call=${Welcome}/>
<div>something</div>
</div>
`;
}
```
The way this works is that sub templates are inlined, and have access to the
ambient context. They can therefore access `props`, and any other part of the
caller component.
+189
View File
@@ -0,0 +1,189 @@
# 🦉 Hooks 🦉
## Content
- [Overview](#overview)
- [Example](#example)
- [Reference](#reference)
- [One Rule](#one-rule)
- [`useState`](#usestate)
- [`onMounted`](#onmounted)
- [`onWillUnmount`](#onwillunmount)
- [`useRef`](#useref)
## Overview
Hooks were popularised by React as a way to solve the following issues:
- help reusing stateful logic between components
- help organizing code by feature in complex components
- use state in functional components, without writing a class.
Owl hooks serve the same purpose, except that they work for class components
(note: React hooks do not work on class components, and maybe because of that,
there seems to be the misconception that hooks are in opposition to class. This
is clearly not true, as shown by Owl hooks).
Hooks works beautifully with Owl components: they solve the problems mentioned
above, and in particular, they are the perfect way to make your component
reactive.
## Example
Here is the classical example of a non trivial hook to track the mouse position.
```js
const { useState, onMounted, onWillUnmount } = owl.hooks;
// We define here a custom behaviour: this hook tracks the state of the mouse
// position
function useMouse() {
const position = useState({ x: 0, y: 0 });
function update(e) {
position.x = e.clientX;
position.y = e.clientY;
}
onMounted(() => {
window.addEventListener("mousemove", update);
});
onWillUnmount(() => {
window.removeEventListener("mousemove", update);
});
return position;
}
// Main root component
class App extends owl.Component {
static template = xml`
<div t-name="App">
<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>
</div>`;
// this hooks is bound to the 'mouse' property.
mouse = useMouse();
}
```
Note that we use the prefix `use` for hooks, just like in React. This is just
a convention.
## Reference
### One rule
There is only one rule: every hook for a component have to be called in the
constructor (or in class fields):
```js
// ok
class SomeComponent extends Component {
state = useState({value: 0});
}
// also ok
class SomeComponent extends Component {
constructor(...args) {
super(...args);
this.state = useState({value: 0});
}
}
// not ok: this is executed after the constructor is called
class SomeComponent extends Component {
async willStart() {
this.state = useState({value: 0});
}
}
```
### `useState`
The `useState` hook is certainly the most important hooks for Owl components:
this is what enables component to be reactive, to react to state change.
The `useState` hook has to be given an object or an array, and will return
an observed version of it (using a `Proxy`).
```javascript
const { useState } = owl.hooks;
class Counter extends owl.Component {
static template = xml`
<button t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>`;
state = useState({ value: 0 });
increment() {
this.state.value++;
}
}
```
### `onMounted`
`onMounted` is not an hook, but is a building block designed to help make useful
hooks. `onMounted` register a callback, which will be called when the component
is mounted (see example on top of this page).
### `onWillUnmount`
`onWillUnmount` is not an hook, but is a building block designed to help make useful
hooks. `onWillUnmount` register a callback, which will be called when the component
is unmounted (see example on top of this page).
### `useRef`
The `useRef` hook is useful when we need a way to interact with some inside part
of a component, rendered by Owl. It can work either on a DOM node, or on a component,
tagged by the `t-ref` directive:
```xml
<div>
<div t-ref="someDiv"/>
<SubComponent t-ref="someComponent"/>
</div>
```
In this example, the component will be able to access the `div` and the component
`SubComponent` using the `useRef` hook:
```js
class Parent extends Component {
subRef = useRef("someComponent");
divRef = useRef("someDiv");
someMethod() {
// here, if component is mounted, refs are active:
// - this.divRef.el is the div HTMLElement
// - this.subRef.comp is the instance of the sub component
}
}
```
As shown by the example above, html elements are accessed by using the `el`
key, and components references are accessed with `comp`.
Note: if used on a component, the reference will be set in the `refs`
variable between `willPatch` and `patched`.
The `t-ref` directive also accepts dynamic values with string interpolation
(like the [`t-attf-`](qweb.md#dynamic-attributes) and
`t-component` directives). For example,
```xml
<div t-ref="component_{{someCondition ? '1' : '2'}}"/>
```
Here, the references needs to be set like this:
```js
this.ref1 = useRef("component_1");
this.ref2 = useRef("component_2");
```
References are only guaranteed to be active while the parent component is mounted.
If this is not the case, accessing `el` or `comp` on it will return `null`.
+3 -33
View File
@@ -11,40 +11,10 @@ For example, this code will display `update` in the console:
```javascript ```javascript
const observer = new owl.Observer(); const observer = new owl.Observer();
observer.notifyCB = () => console.log("update"); observer.notifyCB = () => console.log("update");
observer.observe(obj); const obj = observer.observe({ a: { b: 1 } });
const obj = { a: { b: 1 } };
obj.a.b = 2; obj.a.b = 2;
``` ```
## Technical Limitations The observer is implemented with the native `Proxy` object. Note that this
means that it will not work on older browsers.
Since the observer uses getters and setters, it is actually unable to react to
changes in three situations:
- adding a key to an object
- deleting a key from an object
- modifying an array by setting a new value at a given index
In those situations, we need a way to tell the observer that something happened.
This can be done by using the `set` and `delete` (only for objects) static
methods of the `Observer`.
```javascript
const observer = new owl.Observer();
const obj = { a: 1 };
observer.observe(obj);
obj.b = 2; // won't notify the change
owl.Observer.set(obj, "b", 2); // will notify the change
delete obj.b; // won't notify the change
owl.Observer.delete(obj, "b"); // will notify the change
```
```javascript
const observer = new owl.Observer();
const arr = ["a"];
observer.observe(arr);
arr[0] = "b"; // won't notify the change
owl.Observer.set(arr, 0, "b"); // will notify the change
```
+4 -2
View File
@@ -72,11 +72,13 @@ Here are a few steps that we may take to get started:
Let us now add the javascript to make it work, in `app.js`: Let us now add the javascript to make it work, in `app.js`:
```javascript ```javascript
const useState = owl.hooks.useState;
class ClickCounter extends owl.Component { class ClickCounter extends owl.Component {
static template = "clickcounter";
constructor() { constructor() {
super(...arguments); super(...arguments);
this.template = "clickcounter"; this.state = useState({ value: 0 });
this.state = { value: 0 };
} }
increment() { increment() {
+13 -4
View File
@@ -66,8 +66,8 @@ The component system in Owl requires additional directives, to express various
needs. Here is a list of all Owl specific directives: needs. Here is a list of all Owl specific directives:
| Name | Description | | Name | Description |
| ------------------------------------------- | ----------------------------------------------------------------------------------- | | ------------------------------------------------------ | ----------------------------------------------------------------------------------- |
| `t-component`, `t-keepalive`, `t-asyncroot` | [Defining a sub component](component.md#composition) | | `t-component`, `t-props`, `t-keepalive`, `t-asyncroot` | [Defining a sub component](component.md#composition) |
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#references) | | `t-ref` | [Setting a reference to a dom node or a sub component](component.md#references) |
| `t-key` | [Defining a key (to help virtual dom reconciliation)](component.md#t-key-directive) | | `t-key` | [Defining a key (to help virtual dom reconciliation)](component.md#t-key-directive) |
| `t-on-*` | [Event handling](component.md#event-handling) | | `t-on-*` | [Event handling](component.md#event-handling) |
@@ -129,14 +129,23 @@ It's API is quite simple:
const str = qweb.renderToString("someTemplate", somecontext); const str = qweb.renderToString("someTemplate", somecontext);
``` ```
- **`register(name, Component)`**: static function to register an OWL Component - **`registerTemplate(name, template)`**: static function to register an global
QWeb template. This is useful for commonly used components accross the
application, and for making a template available to an application without
having a reference to the actual QWeb instance.
```js
QWeb.registerTemplate("mytemplate", `<div>some template`);
```
- **`registerComponent(name, Component)`**: static function to register an OWL Component
to QWeb's global registry. Globally registered Components can be used in to QWeb's global registry. Globally registered Components can be used in
templates (see the `t-component` directive). This is useful for commonly used templates (see the `t-component` directive). This is useful for commonly used
components accross the application. components accross the application.
```js ```js
class Dialog extends owl.Component { ... } class Dialog extends owl.Component { ... }
QWeb.register("Dialog", Dialog); QWeb.registerComponent("Dialog", Dialog);
... ...
+13
View File
@@ -9,15 +9,24 @@ build applications. Here is a complete representation of its content:
owl owl
Component Component
QWeb QWeb
useState
core core
EventBus EventBus
Observer Observer
hooks
onMounted
onWillUnmount
useState
useRef
router router
Link Link
RouteComponent
Router Router
store store
Store Store
ConnectedComponent ConnectedComponent
tags
xml
utils utils
debounce debounce
escape escape
@@ -26,15 +35,19 @@ owl
whenReady whenReady
``` ```
Note that for convenience, the `useState` hook is also exported at the root of the `owl` object.
## Reference ## Reference
- [Animations](animations.md) - [Animations](animations.md)
- [Component](component.md) - [Component](component.md)
- [Event Bus](event_bus.md) - [Event Bus](event_bus.md)
- [Hooks](hooks.md)
- [Observer](observer.md) - [Observer](observer.md)
- [QWeb](qweb.md) - [QWeb](qweb.md)
- [Router](router.md) - [Router](router.md)
- [Store](store.md) - [Store](store.md)
- [Tags](tags.md)
- [Utils](utils.md) - [Utils](utils.md)
- [Virtual DOM](vdom.md) - [Virtual DOM](vdom.md)
+7 -8
View File
@@ -8,7 +8,7 @@
- [Route Definition](#route-definition) - [Route Definition](#route-definition)
- [Router](#router) - [Router](#router)
- [Navigation Guards](#navigation-guards) - [Navigation Guards](#navigation-guards)
- [`t-routecomponent`](#t-routecomponent) - [RouteComponent](#routecomponent)
- [Link](#link) - [Link](#link)
## Overview ## Overview
@@ -26,8 +26,8 @@ The Owl router support the following features:
- route redirection - route redirection
- navigation guards - navigation guards
- parameterized routes - parameterized routes
- a `<Link>` component - a `<Link/>` component
- a `t-routecomponent` directive - a `<RouteComponent/>` component
Note that it is still in early stage of developments, and there are probably Note that it is still in early stage of developments, and there are probably
still some issues. still some issues.
@@ -125,7 +125,7 @@ The router also has a `navigate` method, useful to programmatically change the
application to another state (and the url): application to another state (and the url):
```js ```js
router.navigate({to: 'USER', params: {id: 51}}); router.navigate({ to: "USER", params: { id: 51 } });
``` ```
### Navigation Guards ### Navigation Guards
@@ -148,19 +148,18 @@ async function protectRoute({ env, to }) {
A navigation guard is a function that returns a promise, which either resolves A navigation guard is a function that returns a promise, which either resolves
to `true` (the navigation is accepted), or to another destination object. to `true` (the navigation is accepted), or to another destination object.
### `t-routecomponent` ### `RouteComponent`
The `t-routecomponent` directive directs Owl to render the component associated The `RouteComponent` component directs Owl to render the component associated
to the currently active route (if any): to the currently active route (if any):
```xml ```xml
<div t-name="App"> <div t-name="App">
<NavBar /> <NavBar />
<t t-routecomponent="1"/> <RouteComponent />
</div> </div>
``` ```
### `Link` ### `Link`
The `Link` component is a Owl component which render as a `<a>` tag with any The `Link` component is a Owl component which render as a `<a>` tag with any
+1 -1
View File
@@ -103,7 +103,7 @@ const actions = {
try { try {
const loginInfo = await doSomeRPC("/login/", info); const loginInfo = await doSomeRPC("/login/", info);
state.loginState = loginInfo; state.loginState = loginInfo;
} catch { } catch (e) {
state.loginState = "error"; state.loginState = "error";
} }
} }
+47
View File
@@ -0,0 +1,47 @@
# 🦉 Tags 🦉
Tags are very small helper to make it easy to write inline templates. There is
only one currently available tag: `xml`, but we plan to add other tags later,
such as a `css` tag, which will be used to write single file components.
## XML tag
Without tags, creating a standalone component would look like this:
```js
import { Component } from 'owl'
const name = 'some-unique-name';
const template = `
<div>
<span t-if="somecondition">text</span>
<button t-on-click="someMethod">Click</button>
</div>
`;
QWeb.registerTemplate(name, template);
class MyComponent extends Component {
static template = name;
...
}
```
With tags, this process is slightly simplified. The name is uniquely generated,
and the template is automatically registered:
```js
import { Component } from 'owl'
import { xml } from 'owl/tags'
class MyComponent extends Component {
static template = xml`
<div>
<span t-if="somecondition">text</span>
<button t-on-click="someMethod">Click</button>
</div>
`;
...
}
```
+28
View File
@@ -56,3 +56,31 @@ Note: This is more an internal tool, useful for people working on Owl.
The benchmarks application is a very small application, implemented in different The benchmarks application is a very small application, implemented in different
frameworks, and in different versions of Owl. This is a simple internal tool, frameworks, and in different versions of Owl. This is a simple internal tool,
useful to compare various performance metrics on some tasks. useful to compare various performance metrics on some tasks.
## Single File Component
If you want to have `xml` syntax highlighting while using the `xml` helper which
helps you define inline templates, there is a VS Code addon `Comment tagged template`
which, if installed, does exactly that. To enable it, you need to add a comment,
like this:
```js
// -----------------------------------------------------------------------------
// TEMPLATE
// -----------------------------------------------------------------------------
const TEMPLATE = xml/* xml */ `
<div class="main two-columns">
<Sidebar/>
<Content />
</div>`;
// -----------------------------------------------------------------------------
// CODE
// -----------------------------------------------------------------------------
class MyComponent extends Component {
static template = TEMPLATE;
static components = { Sidebar, Content };
// rest of component...
}
```
+1 -3
View File
@@ -18,9 +18,7 @@ not ready yet, resolved directly otherwise). If called with a callback as
argument, it executes it as soon as the DOM ready (or directly). argument, it executes it as soon as the DOM ready (or directly).
```js ```js
Promise.all([loadTemplates(), owl.utils.whenReady()]).then(function([ Promise.all([loadTemplates(), owl.utils.whenReady()]).then(function([templates]) {
templates
]) {
const qweb = new owl.QWeb(templates); const qweb = new owl.QWeb(templates);
const app = new App({ qweb }); const app = new App({ qweb });
app.mount(document.body); app.mount(document.body);
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "owl", "name": "owl-framework",
"version": "0.19.0", "version": "0.22.0",
"description": "Odoo Web Library (OWL)", "description": "Odoo Web Library (OWL)",
"main": "src/index.ts", "main": "src/index.ts",
"scripts": { "scripts": {
@@ -21,11 +21,12 @@
"url": "git+https://github.com/odoo/owl.git" "url": "git+https://github.com/odoo/owl.git"
}, },
"author": "Odoo", "author": "Odoo",
"license": "GPL-3.0-or-later", "license": "LGPL",
"bugs": { "bugs": {
"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.12", "@types/jest": "^23.3.12",
"cpx": "^1.5.0", "cpx": "^1.5.0",
@@ -42,7 +43,6 @@
"typescript": "^3.2.2", "typescript": "^3.2.2",
"uglify-es": "^3.3.9" "uglify-es": "^3.3.9"
}, },
"dependencies": {},
"jest": { "jest": {
"roots": [ "roots": [
"<rootDir>/src", "<rootDir>/src",
+128 -113
View File
@@ -11,7 +11,8 @@ import "./props_validation";
* contains: * contains:
* *
* - the Env interface (generic type for the environment) * - the Env interface (generic type for the environment)
* - the Meta interface (the owl specific metadata attached to a component) * - the Fiber interface (owl metadata attached to a rendering)
* - the Internal interface (the owl specific metadata attached to a component)
* - the Component class * - the Component class
*/ */
@@ -33,6 +34,28 @@ export interface Env {
[key: string]: any; [key: string]: any;
} }
/**
* Fibers are small abstractions designed to contain all the internal state
* associated to a "rendering work unit", relative to a specific component.
*
* A rendering will cause the creation of a fiber for each impacted components.
*/
export interface Fiber<Props> {
force: boolean;
rootFiber: Fiber<any> | null;
isCancelled: boolean;
scope: any;
vars: any;
patchQueue: Fiber<any>[];
component: Component<any, any>;
vnode: VNode | null;
willPatchResult: any;
props: Props;
promise: Promise<VNode> | null;
// handlers?: any;
// mountedHandlers?: any;
}
/** /**
* This is mostly an internal detail of implementation. The Meta interface is * This is mostly an internal detail of implementation. The Meta interface is
* useful to typecheck and describe the internal keys used by Owl to manage the * useful to typecheck and describe the internal keys used by Owl to manage the
@@ -48,42 +71,37 @@ interface Internal<T extends Env, Props> {
// parent and children keys are obviously useful to setup the parent-children // parent and children keys are obviously useful to setup the parent-children
// relationship. // relationship.
parent: Component<T, any, any> | null; parent: Component<T, any> | null;
children: { [key: number]: Component<T, any, any> }; children: { [key: number]: Component<T, any> };
// children mapping: from templateID to componentID. templateID identifies a // children mapping: from templateID to componentID. templateID identifies a
// place in a template. The t-component directive needs it to be able to get // place in a template. The t-component directive needs it to be able to get
// the component instance back whenever the template is rerendered. // the component instance back whenever the template is rerendered.
cmap: { [key: number]: number }; cmap: { [key: number]: number };
renderId: number; currentFiber: Fiber<Props> | null;
// the renderProps and renderPromise keys are only useful for the "prepare"
// step of the lifecycle of a component. Once a component has been rendered
// and patched, it is no longer useful.
renderProps: Props | null;
renderPromise: Promise<VNode> | null;
boundHandlers: { [key: number]: any }; boundHandlers: { [key: number]: any };
observer: Observer | null; observer: Observer | null;
render: CompiledTemplate | null; render: CompiledTemplate | null;
mountedHandlers: { [key: number]: Function }; mountedHandlers: { [key: number]: Function };
willUnmountCB: Function | null;
classObj: { [key: string]: boolean } | null; classObj: { [key: string]: boolean } | null;
refs: { [key: string]: Component<T, any> | HTMLElement | undefined } | null;
} }
// If a component does not define explicitely a template
// key, it needs to find a template with its name (or a parent's). This is
// qweb dependant, so we need a place to store this information indexed by
// qweb instances.
const TEMPLATE_MAP: { [key: number]: { [name: string]: string } } = {};
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Component // Component
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
let nextId = 1; let nextId = 1;
export class Component<T extends Env, Props extends {}, State extends {}> { export class Component<T extends Env, Props extends {}> {
readonly __owl__: Internal<Env, Props>; readonly __owl__: Internal<Env, Props>;
template?: string; static template?: string | null = null;
static _template?: string | null = null;
static _current?: any | null = null;
static components = {};
static props?: any;
static defaultProps?: any;
/** /**
* The `el` is the root element of the component. Note that it could be null: * The `el` is the root element of the component. Note that it could be null:
@@ -94,17 +112,8 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
} }
env: T; env: T;
state?: State;
props: Props; props: Props;
// type of props is not easily representable in typescript...
static props?: any;
static defaultProps?: any;
refs: {
[key: string]: Component<T, any, any> | HTMLElement | undefined;
} = {};
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
// Lifecycle // Lifecycle
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
@@ -128,8 +137,9 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
* hand. Other components should be created automatically by the framework (with * hand. Other components should be created automatically by the framework (with
* the t-component directive in a template) * the t-component directive in a template)
*/ */
constructor(parent: Component<T, any, any> | T, props?: Props) { constructor(parent: Component<T, any> | T, props?: Props) {
const defaultProps = (<any>this.constructor).defaultProps; const defaultProps = (<any>this.constructor).defaultProps;
Component._current = this;
if (defaultProps) { if (defaultProps) {
props = this.__applyDefaultProps(props, defaultProps); props = this.__applyDefaultProps(props, defaultProps);
} }
@@ -139,7 +149,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
// Pro: but creating component (by a template) is always unsafe anyway // Pro: but creating component (by a template) is always unsafe anyway
this.props = <Props>props || <Props>{}; this.props = <Props>props || <Props>{};
let id: number = nextId++; let id: number = nextId++;
let p: Component<T, any, any> | null = null; let p: Component<T, any> | null = null;
if (parent instanceof Component) { if (parent instanceof Component) {
p = parent; p = parent;
this.env = parent.env; this.env = parent.env;
@@ -173,14 +183,14 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
parent: p, parent: p,
children: {}, children: {},
cmap: {}, cmap: {},
renderId: 1, currentFiber: null,
renderPromise: null,
renderProps: props || null,
boundHandlers: {}, boundHandlers: {},
mountedHandlers: {}, mountedHandlers: {},
willUnmountCB: null,
observer: null, observer: null,
render: null, render: null,
classObj: null classObj: null,
refs: null
}; };
} }
@@ -283,17 +293,20 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
if (__owl__.isMounted) { if (__owl__.isMounted) {
return; return;
} }
const fiber = this.__createFiber(false, undefined, undefined, undefined);
if (!__owl__.vnode) { if (!__owl__.vnode) {
const vnode = await this.__prepare(); fiber.promise = this.__prepareAndRender(fiber);
const vnode = await fiber.promise;
if (__owl__.isDestroyed) { if (__owl__.isDestroyed) {
// component was destroyed before we get here... // component was destroyed before we get here...
return; return;
} }
this.__patch(vnode); this.__patch(vnode);
} else if (renderBeforeRemount) { } else if (renderBeforeRemount) {
const patchQueue = []; fiber.patchQueue.push(fiber);
await this.__render(false, patchQueue, undefined, undefined); fiber.promise = this.__render(fiber);
this.__applyPatchQueue(<any[]>patchQueue); await fiber.promise;
this.__applyPatchQueue(fiber);
} }
target.appendChild(this.el!); target.appendChild(this.el!);
@@ -327,18 +340,37 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
if (!__owl__.isMounted) { if (!__owl__.isMounted) {
return; return;
} }
const patchQueue = []; const fiber = this.__createFiber(force, undefined, undefined, undefined);
fiber.patchQueue.push(fiber);
fiber.promise = this.__render(fiber);
await fiber.promise;
const renderId = ++__owl__.renderId; if (__owl__.isMounted && fiber === __owl__.currentFiber) {
await this.__render(force, patchQueue, undefined, undefined);
if (__owl__.isMounted && renderId === __owl__.renderId) {
// we only update the vnode and the actual DOM if no other rendering // we only update the vnode and the actual DOM if no other rendering
// occurred between now and when the render method was initially called. // occurred between now and when the render method was initially called.
this.__applyPatchQueue(<any[]>patchQueue); this.__applyPatchQueue(fiber);
} }
} }
__createFiber(force, scope, vars, parent?: Fiber<any>): Fiber<Props> {
const fiber: Fiber<Props> = {
force,
scope,
vars,
rootFiber: null,
isCancelled: false,
component: this,
vnode: null,
patchQueue: parent ? parent.patchQueue : [],
willPatchResult: null,
props: this.props,
promise: null
};
fiber.rootFiber = parent ? parent.rootFiber : fiber;
this.__owl__.currentFiber = fiber;
return fiber;
}
/** /**
* Destroy the component. This operation is quite complex: * Destroy the component. This operation is quite complex:
* - it recursively destroy all children * - it recursively destroy all children
@@ -420,7 +452,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
* Note that it does not call the __callWillUnmount method to avoid visiting * Note that it does not call the __callWillUnmount method to avoid visiting
* all children many times. * all children many times.
*/ */
__destroy(parent: Component<any, any, any> | null) { __destroy(parent: Component<any, any> | null) {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
const isMounted = __owl__.isMounted; const isMounted = __owl__.isMounted;
if (isMounted) { if (isMounted) {
@@ -451,19 +483,22 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
} }
__owl__.isMounted = true; __owl__.isMounted = true;
const handlers = __owl__.mountedHandlers; const handlers = __owl__.mountedHandlers;
try {
this.mounted();
for (let key in handlers) { for (let key in handlers) {
handlers[key](); handlers[key]();
} }
try {
this.mounted();
} catch (e) { } catch (e) {
errorHandler(e, this); errorHandler(e, this);
} }
} }
__callWillUnmount() { __callWillUnmount() {
this.willUnmount();
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
if (__owl__.willUnmountCB) {
__owl__.willUnmountCB();
}
this.willUnmount();
__owl__.isMounted = false; __owl__.isMounted = false;
const children = __owl__.children; const children = __owl__.children;
for (let id in children) { for (let id in children) {
@@ -480,12 +515,11 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
*/ */
async __updateProps( async __updateProps(
nextProps: Props, nextProps: Props,
forceUpdate: boolean = false, parentFiber: Fiber<any>,
patchQueue?: any[],
scope?: any, scope?: any,
vars?: any vars?: any
): Promise<void> { ): Promise<void> {
const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps); const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps);
if (shouldUpdate) { if (shouldUpdate) {
const defaultProps = (<any>this.constructor).defaultProps; const defaultProps = (<any>this.constructor).defaultProps;
if (defaultProps) { if (defaultProps) {
@@ -493,7 +527,10 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
} }
await this.willUpdateProps(nextProps); await this.willUpdateProps(nextProps);
this.props = nextProps; this.props = nextProps;
await this.__render(forceUpdate, patchQueue, scope, vars); const fiber = this.__createFiber(parentFiber.force, scope, vars, parentFiber);
fiber.patchQueue.push(fiber);
await this.__render(fiber);
} }
} }
@@ -507,14 +544,18 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
__owl__.vnode = patch(target, vnode); __owl__.vnode = patch(target, vnode);
} }
__prepare(scope?: Object, vars?: any): Promise<VNode> { /**
const __owl__ = this.__owl__; * The __prepare method is only called by the t-component directive, when a
__owl__.renderProps = this.props; * subcomponent is created. It gets its scope and vars, if any, from the
__owl__.renderPromise = this.__prepareAndRender(scope, vars); * parent template.
return __owl__.renderPromise; */
__prepare(parentFiber: Fiber<any>, scope: any, vars: any): Promise<VNode> {
const fiber = this.__createFiber(parentFiber.force, scope, vars, parentFiber);
fiber.promise = this.__prepareAndRender(fiber);
return fiber.promise;
} }
async __prepareAndRender(scope?: Object, vars?: any): Promise<VNode> { async __prepareAndRender(fiber: Fiber<Props>): Promise<VNode> {
try { try {
await this.willStart(); await this.willStart();
} catch (e) { } catch (e) {
@@ -526,45 +567,34 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
return Promise.resolve(h("div")); return Promise.resolve(h("div"));
} }
const qweb = this.env.qweb; const qweb = this.env.qweb;
if (!this.template) {
let tmap = TEMPLATE_MAP[qweb.id];
if (!tmap) {
tmap = {};
TEMPLATE_MAP[qweb.id] = tmap;
}
let p = (<any>this).constructor; let p = (<any>this).constructor;
let name: string = p.name; // console.warn(p, p.template, p._template, 'template' in p, p.hasOwnProperty('template'))
let template = tmap[name]; if (!p.hasOwnProperty("_template")) {
if (template) { if (p.template) {
this.template = template; p._template = p.template;
} else { } else {
// here, the component and none of its superclasses defines a static `template`
// key. So we fall back on looking for a template matching its name (or
// one of its subclass).
let template: string;
while ((template = p.name) && !(template in qweb.templates) && p !== Component) { while ((template = p.name) && !(template in qweb.templates) && p !== Component) {
p = p.__proto__; p = p.__proto__;
} }
if (p === Component) { if (p === Component) {
throw new Error(`Could not find template for component "${this.constructor.name}"`); throw new Error(`Could not find template for component "${this.constructor.name}"`);
} else { } else {
tmap[name] = template; p._template = template;
this.template = template;
} }
} }
} }
__owl__.render = qweb.render.bind(qweb, this.template); __owl__.render = qweb.render.bind(qweb, p._template);
this.__observeState(); return this.__render(fiber);
return this.__render(false, [], scope, vars);
} }
__render( __render(fiber: Fiber<Props>): Promise<VNode> {
force: boolean = false,
patchQueue: any[] = [],
scope?: Object,
vars?: any
): Promise<VNode> {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
const promises: Promise<void>[] = []; const promises: Promise<void>[] = [];
const patch: any[] = [this];
patchQueue.push(patch);
if (__owl__.observer) { if (__owl__.observer) {
__owl__.observer.allowMutations = false; __owl__.observer.allowMutations = false;
} }
@@ -574,16 +604,13 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
promises, promises,
handlers: __owl__.boundHandlers, handlers: __owl__.boundHandlers,
mountedHandlers: __owl__.mountedHandlers, mountedHandlers: __owl__.mountedHandlers,
forceUpdate: force, fiber: fiber
patchQueue,
scope,
vars
}); });
} catch (e) { } catch (e) {
vnode = __owl__.vnode || h("div"); vnode = __owl__.vnode || h("div");
errorHandler(e, this); errorHandler(e, this);
} }
patch.push(vnode); fiber.vnode = vnode;
if (__owl__.observer) { if (__owl__.observer) {
__owl__.observer.allowMutations = true; __owl__.observer.allowMutations = true;
} }
@@ -631,19 +658,6 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
} }
} }
/**
* Enable the observe feature on the state. We only create an observer if
* there is some state to be observed.
*/
__observeState() {
if (this.state) {
const __owl__ = this.__owl__;
__owl__.observer = new Observer();
this.state = __owl__.observer.observe(this.state);
__owl__.observer.notifyCB = this.render.bind(this);
}
}
/** /**
* Apply default props (only top level). * Apply default props (only top level).
* *
@@ -661,29 +675,30 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
} }
/** /**
* Apply the given patch queue. A patch is a pair [c, vn], where c is a * Apply the given patch queue from a fiber.
* Component instance and vn a VNode.
* 1) Call 'willPatch' on the component of each patch * 1) Call 'willPatch' on the component of each patch
* 2) Call '__patch' on the component of each patch * 2) Call '__patch' on the component of each patch
* 3) Call 'patched' on the component of each patch, in inverse order * 3) Call 'patched' on the component of each patch, in reverse order
*/ */
__applyPatchQueue(patchQueue: any[]) { __applyPatchQueue(fiber: Fiber<Props>) {
let component = this; const patchQueue = fiber.patchQueue;
let component: Component<any, any> = this;
try { try {
const patchLen = patchQueue.length; const patchLen = patchQueue.length;
for (let i = 0; i < patchLen; i++) { for (let i = 0; i < patchLen; i++) {
const patch = patchQueue[i]; const fiber = patchQueue[i];
component = patch[0]; component = fiber.component;
patch.push(patch[0].willPatch()); fiber.willPatchResult = component.willPatch();
} }
for (let i = 0; i < patchLen; i++) { for (let i = 0; i < patchLen; i++) {
const patch = patchQueue[i]; const fiber = patchQueue[i];
patch[0].__patch(patch[1]); component = fiber.component;
component.__patch(fiber.vnode);
} }
for (let i = patchLen - 1; i >= 0; i--) { for (let i = patchLen - 1; i >= 0; i--) {
const patch = patchQueue[i]; const fiber = patchQueue[i];
component = patch[0]; component = fiber.component;
patch[0].patched(patch[2]); component.patched(fiber.willPatchResult);
} }
} catch (e) { } catch (e) {
errorHandler(e, component); errorHandler(e, component);
@@ -703,7 +718,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
* If there are no such component, we destroy everything. This is better than * If there are no such component, we destroy everything. This is better than
* being in a corrupted state. * being in a corrupted state.
*/ */
function errorHandler(error: Error, component: Component<any, any, any>) { function errorHandler(error: Error, component: Component<any, any>) {
let canCatch = false; let canCatch = false;
let qweb = component.env.qweb; let qweb = component.env.qweb;
let root = component; let root = component;
+51 -30
View File
@@ -1,4 +1,5 @@
import { QWeb } from "../qweb/index"; import { QWeb } from "../qweb/index";
import { INTERP_REGEXP } from "../qweb/context";
import { MODS_CODE } from "../qweb/extensions"; import { MODS_CODE } from "../qweb/extensions";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -194,6 +195,7 @@ QWeb.addDirective({
ctx.rootContext.shouldDefineParent = true; ctx.rootContext.shouldDefineParent = true;
ctx.rootContext.shouldDefineUtils = true; ctx.rootContext.shouldDefineUtils = true;
let keepAlive = node.getAttribute("t-keepalive") ? true : false; let keepAlive = node.getAttribute("t-keepalive") ? true : false;
let hasDynamicProps = node.getAttribute("t-props") ? true : false;
let async = node.getAttribute("t-asyncroot") ? true : false; let async = node.getAttribute("t-asyncroot") ? true : false;
// t-on- events and t-transition // t-on- events and t-transition
@@ -238,32 +240,33 @@ QWeb.addDirective({
if (key) { if (key) {
// we bind a variable to the key (could be a complex expression, so we // we bind a variable to the key (could be a complex expression, so we
// want to evaluate it only once) // want to evaluate it only once)
ctx.addLine(`let key${keyID} = ${key};`); ctx.addLine(`let key${keyID} = 'key' + ${key};`);
} }
ctx.addLine(`let def${defID};`); ctx.addLine(`let def${defID};`);
let templateID = key let templateID = key
? `key${keyID}` ? `key${keyID}`
: ctx.inLoop : ctx.inLoop
? `String(-${componentID} - i)` ? ctx.currentKey
? `String(${ctx.currentKey} + '_k_' + i + '_c_' + ${componentID} )`
: `String(-${componentID} - i)`
: String(componentID); : String(componentID);
if (ctx.allowMultipleRoots) { if (ctx.allowMultipleRoots) {
// necessary to prevent collisions
if (!key && ctx.inLoop) {
let id = ctx.generateID();
ctx.addLine(`let template${id} = "_slot_" + String(-${componentID} - i)`);
templateID = `template${id}`;
} else {
templateID = `"_slot_${templateID}"`; templateID = `"_slot_${templateID}"`;
} }
if (key || ctx.inLoop) {
let id = ctx.generateID();
ctx.addLine(`let templateId${id} = ${templateID};`);
templateID = `templateId${id}`;
} }
let ref = node.getAttribute("t-ref"); let ref = node.getAttribute("t-ref");
let refExpr = ""; let refExpr = "";
let refKey: string = ""; let refKey: string = "";
if (ref) { if (ref) {
ctx.rootContext.shouldDefineRefs = true;
refKey = `ref${ctx.generateID()}`; refKey = `ref${ctx.generateID()}`;
ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`); ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`);
refExpr = `context.refs[${refKey}] = w${componentID};`; refExpr = `context.__owl__.refs[${refKey}] = w${componentID};`;
} }
let transitionsInsertCode = ""; let transitionsInsertCode = "";
if (transition) { if (transition) {
@@ -271,7 +274,7 @@ QWeb.addDirective({
} }
let finalizeComponentCode = `w${componentID}.${keepAlive ? "unmount" : "destroy"}();`; let finalizeComponentCode = `w${componentID}.${keepAlive ? "unmount" : "destroy"}();`;
if (ref && !keepAlive) { if (ref && !keepAlive) {
finalizeComponentCode += `delete context.refs[${refKey}];`; finalizeComponentCode += `delete context.__owl__.refs[${refKey}];`;
} }
if (transition) { if (transition) {
finalizeComponentCode = `let finalize = () => { finalizeComponentCode = `let finalize = () => {
@@ -355,8 +358,12 @@ QWeb.addDirective({
ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`); ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`);
} }
let shouldProxy = false; let shouldProxy = false;
if (async || keepAlive) {
ctx.addLine(
`const fiber${componentID} = Object.assign(Object.create(extra.fiber), {patchQueue: []});`
);
}
if (async) { if (async) {
ctx.addLine(`const patchQueue${componentID} = [];`);
ctx.addLine( ctx.addLine(
`c${ctx.parentNode}.push(w${componentID} && w${componentID}.__owl__.pvnode || null);` `c${ctx.parentNode}.push(w${componentID} && w${componentID}.__owl__.pvnode || null);`
); );
@@ -372,12 +379,19 @@ QWeb.addDirective({
ctx.addLine(`result = vn${id};`); ctx.addLine(`result = vn${id};`);
} }
} }
if (hasDynamicProps) {
const dynamicProp = ctx.formatExpression(node.getAttribute("t-props")!);
ctx.addLine(`let props${componentID} = Object.assign({${propStr}}, ${dynamicProp});`);
} else {
ctx.addLine(`let props${componentID} = {${propStr}};`); ctx.addLine(`let props${componentID} = {${propStr}};`);
}
ctx.addIf( ctx.addIf(
`w${componentID} && w${componentID}.__owl__.renderPromise && !w${componentID}.__owl__.vnode` `w${componentID} && w${componentID}.__owl__.currentFiber && !w${componentID}.__owl__.vnode`
); );
ctx.addIf(`utils.shallowEqual(props${componentID}, w${componentID}.__owl__.renderProps)`); ctx.addIf(
ctx.addLine(`def${defID} = w${componentID}.__owl__.renderPromise;`); `utils.shallowEqual(props${componentID}, w${componentID}.__owl__.currentFiber.props)`
);
ctx.addLine(`def${defID} = w${componentID}.__owl__.currentFiber.promise;`);
ctx.addElse(); ctx.addElse();
ctx.addLine(`w${componentID}.destroy();`); ctx.addLine(`w${componentID}.destroy();`);
ctx.addLine(`w${componentID} = false;`); ctx.addLine(`w${componentID} = false;`);
@@ -386,10 +400,16 @@ QWeb.addDirective({
ctx.addIf(`!w${componentID}`); ctx.addIf(`!w${componentID}`);
// new component // new component
ctx.addLine(`let componentKey${componentID} = ${ctx.interpolate(value)};`); let dynamicFallback = "";
if (!value.match(INTERP_REGEXP)) {
dynamicFallback = `|| ${ctx.formatExpression(value)}`;
}
const interpValue = ctx.interpolate(value);
ctx.addLine(`let componentKey${componentID} = ${interpValue};`);
ctx.addLine( ctx.addLine(
`let W${componentID} = context.components && context.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}];` `let W${componentID} = context.constructor.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}]${dynamicFallback};`
); );
// maybe only do this in dev mode... // maybe only do this in dev mode...
ctx.addLine( ctx.addLine(
`if (!W${componentID}) {throw new Error('Cannot find the definition of component "' + componentKey${componentID} + '"')}` `if (!W${componentID}) {throw new Error('Cannot find the definition of component "' + componentKey${componentID} + '"')}`
@@ -413,7 +433,7 @@ QWeb.addDirective({
const clone = <Element>node.cloneNode(true); const clone = <Element>node.cloneNode(true);
const slotNodes = clone.querySelectorAll("[t-set]"); const slotNodes = clone.querySelectorAll("[t-set]");
const slotId = qweb.nextSlotId++; const slotId = QWeb.nextSlotId++;
ctx.addLine(`w${componentID}.__owl__.slotId = ${slotId};`); ctx.addLine(`w${componentID}.__owl__.slotId = ${slotId};`);
if (slotNodes.length) { if (slotNodes.length) {
for (let i = 0, length = slotNodes.length; i < length; i++) { for (let i = 0, length = slotNodes.length; i < length; i++) {
@@ -422,7 +442,7 @@ QWeb.addDirective({
const key = slotNode.getAttribute("t-set")!; const key = slotNode.getAttribute("t-set")!;
slotNode.removeAttribute("t-set"); slotNode.removeAttribute("t-set");
const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx); const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx);
qweb.slots[`${slotId}_${key}`] = slotFn.bind(qweb); QWeb.slots[`${slotId}_${key}`] = slotFn;
} }
} }
if (clone.childNodes.length) { if (clone.childNodes.length) {
@@ -431,42 +451,43 @@ QWeb.addDirective({
t.appendChild(child); t.appendChild(child);
} }
const slotFn = qweb._compile(`slot_default_template`, t, ctx); const slotFn = qweb._compile(`slot_default_template`, t, ctx);
qweb.slots[`${slotId}_default`] = slotFn.bind(qweb); QWeb.slots[`${slotId}_default`] = slotFn;
} }
} }
let scopeVars = ""; let scopeVars;
if (hasSlots) { if (hasSlots) {
scopeVars += ctx.scopeVars.length ? `Object.assign({}, scope)` : varDefs.length ? `{}` : ""; let scope = ctx.scopeVars.length ? `Object.assign({}, scope)` : `{}`;
if (varDefs.length) { let vars = varDefs.length ? `{${varDefs.join(",")}}` : "undefined";
scopeVars += `, {${varDefs.join(",")}}`; scopeVars = `${scope}, ${vars}`;
} else {
scopeVars = "undefined, undefined";
} }
} ctx.addLine(`def${defID} = w${componentID}.__prepare(extra.fiber, ${scopeVars});`);
ctx.addLine(`def${defID} = w${componentID}.__prepare(${scopeVars});`);
// hack: specify empty remove hook to prevent the node from being removed from the DOM // hack: specify empty remove hook to prevent the node from being removed from the DOM
let registerCode = `c${ctx.parentNode}[_${dummyID}_index]=pvnode;`; let registerCode = `c${ctx.parentNode}[_${dummyID}_index]=pvnode;`;
if (shouldProxy) { if (shouldProxy) {
registerCode = `utils.defineProxy(vn${ctx.rootNode}, pvnode);`; registerCode = `utils.defineProxy(vn${ctx.rootNode}, pvnode);`;
} }
ctx.addLine( ctx.addLine(
`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${componentID}.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeComponentCode}}}});${registerCode}w${componentID}.__owl__.pvnode = pvnode;});` `def${defID} = def${defID}.then(vnode=>{if (w${componentID}.__owl__.isDestroyed){return}${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${componentID}.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeComponentCode}}}});${registerCode}w${componentID}.__owl__.pvnode = pvnode;});`
); );
ctx.addElse(); ctx.addElse();
// need to update component // need to update component
let patchQueueCode = async ? `patchQueue${componentID}` : "extra.patchQueue"; let patchQueueCode = async || keepAlive ? `fiber${componentID}` : "extra.fiber";
if (keepAlive) { if (keepAlive) {
// if we have t-keepalive="1", the component could be unmounted, but then // if we have t-keepalive="1", the component could be unmounted, but then
// we __updateProps is called. This is ok, but we do not want to call // we __updateProps is called. This is ok, but we do not want to call
// the willPatch/patched hooks of the component in this case, so we // the willPatch/patched hooks of the component in this case, so we
// disable the patch queue // disable the patch queue
patchQueueCode = `w${componentID}.__owl__.isMounted ? ${patchQueueCode} : []`; patchQueueCode = `w${componentID}.__owl__.isMounted ? extra.fiber : fiber${componentID}`;
} }
if (QWeb.dev) { if (QWeb.dev) {
ctx.addLine(`utils.validateProps(w${componentID}.constructor, props${componentID})`); ctx.addLine(`utils.validateProps(w${componentID}.constructor, props${componentID})`);
} }
ctx.addLine( ctx.addLine(
`def${defID} = def${defID} || w${componentID}.__updateProps(props${componentID}, extra.forceUpdate, ${patchQueueCode}${scopeVars && `def${defID} = def${defID} || w${componentID}.__updateProps(props${componentID}, ${patchQueueCode}${scopeVars &&
", " + scopeVars});` ", " + scopeVars});`
); );
let keepAliveCode = ""; let keepAliveCode = "";
@@ -486,7 +507,7 @@ QWeb.addDirective({
if (async) { if (async) {
ctx.addLine( ctx.addLine(
`def${defID}.then(w${componentID}.__applyPatchQueue.bind(w${componentID}, patchQueue${componentID}));` `def${defID}.then(w${componentID}.__applyPatchQueue.bind(w${componentID}, fiber${componentID}));`
); );
} else { } else {
ctx.addLine(`extra.promises.push(def${defID});`); ctx.addLine(`extra.promises.push(def${defID});`);
+1 -2
View File
@@ -25,7 +25,6 @@ export class Observer {
notifyCB() {} notifyCB() {}
async notifyChange() { async notifyChange() {
this.dirty = true; this.dirty = true;
await Promise.resolve(); await Promise.resolve();
if (this.dirty) { if (this.dirty) {
@@ -34,7 +33,7 @@ export class Observer {
} }
} }
observe(value: any, parent?: any): any { observe<T>(value: T, parent?: any): T {
if (value === null || typeof value !== "object" || value instanceof Date) { if (value === null || typeof value !== "object" || value instanceof Date) {
// fun fact: typeof null === 'object' // fun fact: typeof null === 'object'
return value; return value;
+83
View File
@@ -0,0 +1,83 @@
import { Component } from "./component/component";
import { Observer } from "./core/observer";
/**
* Owl Hook System
*
* This file introduces the concept of hooks, similar to React or Vue hooks.
* We have currently an implementation of:
* - useState (reactive state)
* - onMounted
* - onWillUnmount
* - useRef
*/
/**
* useState hook
*
* This is the main way a component can be made reactive. The useState hook
* will return an observed object (or array). Changes to that value will then
* trigger a rerendering of the current component.
*/
export function useState<T>(state: T): T {
const component: Component<any, any> = Component._current;
const __owl__ = component.__owl__;
if (!__owl__.observer) {
__owl__.observer = new Observer();
__owl__.observer.notifyCB = component.render.bind(component);
}
return __owl__.observer.observe(state);
}
/**
* Mounted hook. The callback will be called when the current component is
* mounted. Note that the component mounted method is called first.
*/
let nextID = 1;
export function onMounted(cb) {
const component: Component<any, any> = Component._current;
component.__owl__.mountedHandlers[`h${nextID++}`] = cb;
}
/**
* willUnmount hook. The callback will be called when the current component is
* willUnmounted. Note that the component mounted method is called last.
*/
export function onWillUnmount(cb) {
const component: Component<any, any> = Component._current;
if (component.__owl__.willUnmountCB) {
const current = component.__owl__.willUnmountCB;
component.__owl__.willUnmountCB = function() {
cb.call(component);
current.call(component);
};
} else {
component.__owl__.willUnmountCB = cb;
}
}
/**
* useRef hook
*
* The purpose of this hook is to allow components to get a reference to a sub
* html node or component.
*/
interface Ref {
el: HTMLElement | null;
comp: Component<any, any> | null;
}
export function useRef(name: string): Ref {
const __owl__ = Component._current.__owl__;
return {
get el(): HTMLElement | null {
const val = __owl__.refs && __owl__.refs[name];
return val instanceof HTMLElement ? val : null;
},
get comp(): Component<any, any> | null {
const val = __owl__.refs && __owl__.refs[name];
return val instanceof Component ? val : null;
}
};
}
+8 -2
View File
@@ -10,16 +10,22 @@ import { QWeb } from "./qweb/index";
import { ConnectedComponent } from "./store/connected_component"; import { ConnectedComponent } from "./store/connected_component";
import { Store } from "./store/store"; import { Store } from "./store/store";
import * as _utils from "./utils"; import * as _utils from "./utils";
import * as _tags from "./tags";
import * as _hooks from "./hooks";
import { Link } from "./router/Link"; import { Link } from "./router/Link";
import { RouteComponent } from "./router/RouteComponent";
import { Router } from "./router/Router"; import { Router } from "./router/Router";
export { Component } from "./component/component"; export { Component } from "./component/component";
export { QWeb }; export { QWeb };
export const useState = _hooks.useState;
export const core = { EventBus, Observer }; export const core = { EventBus, Observer };
export const router = { Router, Link }; export const router = { Router, RouteComponent, Link };
export const store = { Store, ConnectedComponent }; export const store = { Store, ConnectedComponent };
export const utils = _utils; export const utils = _utils;
export const tags = _tags;
export const hooks = _hooks;
export const __info__ = {}; export const __info__ = {};
Object.defineProperty(__info__, "mode", { Object.defineProperty(__info__, "mode", {
+47 -7
View File
@@ -55,6 +55,7 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Context) {
ctx.rootContext.rootNode = nodeID; ctx.rootContext.rootNode = nodeID;
ctx.rootContext.parentTextNode = nodeID; ctx.rootContext.parentTextNode = nodeID;
ctx.addLine(`var vn${nodeID} = {text: ${exprID}};`); ctx.addLine(`var vn${nodeID} = {text: ${exprID}};`);
ctx.addLine(`result = vn${nodeID}`);
} }
} else { } else {
let fragID = ctx.generateID(); let fragID = ctx.generateID();
@@ -82,6 +83,7 @@ QWeb.addDirective({
if (node.nodeName !== "t") { if (node.nodeName !== "t") {
let nodeID = qweb._compileGenericNode(node, ctx); let nodeID = qweb._compileGenericNode(node, ctx);
ctx = ctx.withParent(nodeID); ctx = ctx.withParent(nodeID);
ctx = ctx.subContext("currentKey", ctx.lastNodeKey);
} }
let value = ctx.getValue(node.getAttribute("t-esc")!); let value = ctx.getValue(node.getAttribute("t-esc")!);
compileValueNode(value, node, qweb, ctx.subContext("escaping", true)); compileValueNode(value, node, qweb, ctx.subContext("escaping", true));
@@ -96,6 +98,7 @@ QWeb.addDirective({
if (node.nodeName !== "t") { if (node.nodeName !== "t") {
let nodeID = qweb._compileGenericNode(node, ctx); let nodeID = qweb._compileGenericNode(node, ctx);
ctx = ctx.withParent(nodeID); ctx = ctx.withParent(nodeID);
ctx = ctx.subContext("currentKey", ctx.lastNodeKey);
} }
let value = ctx.getValue(node.getAttribute("t-raw")!); let value = ctx.getValue(node.getAttribute("t-raw")!);
compileValueNode(value, node, qweb, ctx); compileValueNode(value, node, qweb, ctx);
@@ -198,12 +201,55 @@ QWeb.addDirective({
// extract variables from nodecopy // extract variables from nodecopy
const tempCtx = new Context(); const tempCtx = new Context();
tempCtx.nextID = ctx.rootContext.nextID; tempCtx.nextID = ctx.rootContext.nextID;
tempCtx.allowMultipleRoots = true;
qweb._compileNode(nodeCopy, tempCtx); qweb._compileNode(nodeCopy, tempCtx);
const vars = Object.assign({}, ctx.variables, tempCtx.variables); const vars = Object.assign({}, ctx.variables, tempCtx.variables);
ctx.rootContext.nextID = tempCtx.nextID; ctx.rootContext.nextID = tempCtx.nextID;
const templateMap = Object.create(ctx.templates);
// open new scope, if necessary // open new scope, if necessary
const hasNewVariables = Object.keys(tempCtx.variables).length > 0; const hasNewVariables = Object.keys(tempCtx.variables).length > 0;
// compile sub template
let subCtx = ctx.subContext("caller", nodeCopy).subContext("variables", Object.create(vars));
subCtx = subCtx.subContext("templates", templateMap);
if (templateMap[subTemplate]) {
// OUCH, IT IS A RECURSIVE TEMPLATE SITUATION...
// This is a tricky situation... We obviously cannot inline the compiled
// template. So, what we need to do is to compile it, and make sure we
// properly transfer everything from the current scope to the sub template.
ctx.rootContext.shouldTrackScope = true;
ctx.rootContext.shouldDefineOwner = true;
let subTemplateName;
if (ctx.hasParentWidget) {
subTemplateName = ctx.templateName;
} else {
subTemplateName = `__${ctx.generateID()}`;
subCtx.variables = {};
let id = 0;
for (let v in vars) {
subCtx.variables[v] = vars[v];
(vars[v] as any).id = `_v${id++}`;
}
const subTemplateFn = qweb._compile(subTemplateName, nodeTemplate.elem, subCtx);
qweb.recursiveFns[subTemplateName] = subTemplateFn;
}
let varCode = `{}`;
if (Object.keys(vars).length) {
let id = 0;
const content = Object.values(vars)
.map((v: any) => `_v${id++}: ${v.expr}`)
.join(",");
varCode = `{${content}}`;
}
ctx.addLine(
`this.recursiveFns['${subTemplateName}'].call(this, context, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, fiber: {vars: ${varCode}, scope}}));`
);
return true;
}
templateMap[subTemplate] = true;
if (hasNewVariables) { if (hasNewVariables) {
ctx.addLine("{"); ctx.addLine("{");
ctx.indent(); ctx.indent();
@@ -216,10 +262,6 @@ QWeb.addDirective({
// todo: handle XML variables... // todo: handle XML variables...
} }
} }
// compile sub template
const subCtx = ctx.subContext("caller", nodeCopy).subContext("variables", Object.create(vars));
qweb._compileNode(nodeTemplate.elem, subCtx); qweb._compileNode(nodeTemplate.elem, subCtx);
// close new scope // close new scope
@@ -283,9 +325,7 @@ QWeb.addDirective({
} }
if (shouldWarn) { if (shouldWarn) {
console.warn( console.warn(
`Directive t-foreach should always be used with a t-key! (in template: '${ `Directive t-foreach should always be used with a t-key! (in template: '${ctx.templateName}')`
ctx.templateName
}')`
); );
} }
nodeCopy.removeAttribute("t-foreach"); nodeCopy.removeAttribute("t-foreach");
+14 -2
View File
@@ -1,5 +1,6 @@
import { compileExpr, QWebVar } from "./expression_parser"; import { compileExpr, QWebVar } from "./expression_parser";
export const INTERP_REGEXP = /\{\{.*?\}\}/g;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Compilation Context // Compilation Context
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -19,7 +20,8 @@ export class Context {
shouldDefineParent: boolean = false; shouldDefineParent: boolean = false;
shouldDefineQWeb: boolean = false; shouldDefineQWeb: boolean = false;
shouldDefineUtils: boolean = false; shouldDefineUtils: boolean = false;
shouldDefineResult: boolean = false; shouldDefineRefs: boolean = false;
shouldDefineResult: boolean = true;
shouldProtectContext: boolean = false; shouldProtectContext: boolean = false;
shouldTrackScope: boolean = false; shouldTrackScope: boolean = false;
inLoop: boolean = false; inLoop: boolean = false;
@@ -28,10 +30,14 @@ export class Context {
allowMultipleRoots: boolean = false; allowMultipleRoots: boolean = false;
hasParentWidget: boolean = false; hasParentWidget: boolean = false;
scopeVars: any[] = []; scopeVars: any[] = [];
currentKey: string = "";
lastNodeKey: string = ""; // temp variable to communicate to previous caller
templates: { [key: string]: boolean } = {};
constructor(name?: string) { constructor(name?: string) {
this.rootContext = this; this.rootContext = this;
this.templateName = name || "noname"; this.templateName = name || "noname";
this.templates[this.templateName] = true;
this.addLine("var h = this.h;"); this.addLine("var h = this.h;");
} }
@@ -57,6 +63,9 @@ export class Context {
if (this.shouldDefineResult) { if (this.shouldDefineResult) {
this.code.unshift(" let result;"); this.code.unshift(" let result;");
} }
if (this.shouldDefineRefs) {
this.code.unshift(" context.__owl__.refs = context.__owl__.refs || {};");
}
if (this.shouldDefineOwner) { if (this.shouldDefineOwner) {
// this is necessary to prevent some directives (t-forach for ex) to // this is necessary to prevent some directives (t-forach for ex) to
// pollute the rendering context by adding some keys in it. // pollute the rendering context by adding some keys in it.
@@ -89,6 +98,9 @@ export class Context {
if (!this.rootContext.rootNode) { if (!this.rootContext.rootNode) {
this.rootContext.rootNode = node; this.rootContext.rootNode = node;
} }
if (!this.parentNode) {
this.addLine(`result = vn${node};`);
}
return this.subContext("parentNode", node); return this.subContext("parentNode", node);
} }
@@ -156,7 +168,7 @@ export class Context {
* '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b') * '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b')
*/ */
interpolate(s: string): string { interpolate(s: string): string {
let matches = s.match(/\{\{.*?\}\}/g); let matches = s.match(INTERP_REGEXP);
if (matches && matches[0].length === s.length) { if (matches && matches[0].length === s.length) {
return `(${this.formatExpression(s.slice(2, -2))})`; return `(${this.formatExpression(s.slice(2, -2))})`;
} }
+17 -14
View File
@@ -79,9 +79,10 @@ QWeb.addDirective({
name: "ref", name: "ref",
priority: 95, priority: 95,
atNodeCreation({ ctx, value, addNodeHook }) { atNodeCreation({ ctx, value, addNodeHook }) {
ctx.rootContext.shouldDefineRefs = true;
const refKey = `ref${ctx.generateID()}`; const refKey = `ref${ctx.generateID()}`;
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`); ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
addNodeHook("create", `context.refs[${refKey}] = n.elm;`); addNodeHook("create", `context.__owl__.refs[${refKey}] = n.elm;`);
} }
}); });
@@ -227,12 +228,12 @@ QWeb.addDirective({
atNodeEncounter({ ctx, value }): boolean { atNodeEncounter({ ctx, value }): boolean {
const slotKey = ctx.generateID(); const slotKey = ctx.generateID();
ctx.rootContext.shouldDefineOwner = true; ctx.rootContext.shouldDefineOwner = true;
ctx.addLine(`const slot${slotKey} = this.slots[context.__owl__.slotId + '_' + '${value}'];`); ctx.addLine(
`const slot${slotKey} = this.constructor.slots[context.__owl__.slotId + '_' + '${value}'];`
);
ctx.addIf(`slot${slotKey}`); ctx.addIf(`slot${slotKey}`);
ctx.addLine( ctx.addLine(
`slot${slotKey}(context.__owl__.parent, Object.assign({}, extra, {parentNode: c${ `slot${slotKey}.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, vars: extra.vars, parent: owner}));`
ctx.parentNode
}, vars: extra.vars, parent: owner}));`
); );
ctx.closeIf(); ctx.closeIf();
return true; return true;
@@ -250,31 +251,33 @@ QWeb.utils.toNumber = function(val: string): number | string {
QWeb.addDirective({ QWeb.addDirective({
name: "model", name: "model",
priority: 42, priority: 42,
atNodeCreation({ ctx, nodeID, value, node, fullName }) { atNodeCreation({ ctx, nodeID, value, node, fullName, addNodeHook }) {
const type = node.getAttribute("type"); const type = node.getAttribute("type");
let handler; let handler;
let event = fullName.includes(".lazy") ? "change" : "input"; let event = fullName.includes(".lazy") ? "change" : "input";
const expr = ctx.formatExpression(value);
if (node.tagName === "select") { if (node.tagName === "select") {
ctx.addLine(`p${nodeID}.props = {value: context.state['${value}']};`); ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
addNodeHook("create", `n.elm.value=${expr};`);
event = "change"; event = "change";
handler = `(ev) => {context.state['${value}'] = ev.target.value}`; handler = `(ev) => {${expr} = ev.target.value}`;
} else if (type === "checkbox") { } else if (type === "checkbox") {
ctx.addLine(`p${nodeID}.props = {checked: context.state['${value}']};`); ctx.addLine(`p${nodeID}.props = {checked: ${expr}};`);
handler = `(ev) => {context.state['${value}'] = ev.target.checked}`; handler = `(ev) => {${expr} = ev.target.checked}`;
} else if (type === "radio") { } else if (type === "radio") {
const nodeValue = node.getAttribute("value")!; const nodeValue = node.getAttribute("value")!;
ctx.addLine(`p${nodeID}.props = {checked:context.state['${value}'] === '${nodeValue}'};`); ctx.addLine(`p${nodeID}.props = {checked:${expr} === '${nodeValue}'};`);
handler = `(ev) => {context.state['${value}'] = ev.target.value}`; handler = `(ev) => {${expr} = ev.target.value}`;
event = "click"; event = "click";
} else { } else {
ctx.addLine(`p${nodeID}.props = {value: context.state['${value}']};`); ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
const trimCode = fullName.includes(".trim") ? ".trim()" : ""; const trimCode = fullName.includes(".trim") ? ".trim()" : "";
let valueCode = `ev.target.value${trimCode}`; let valueCode = `ev.target.value${trimCode}`;
if (fullName.includes(".number")) { if (fullName.includes(".number")) {
ctx.rootContext.shouldDefineUtils = true; ctx.rootContext.shouldDefineUtils = true;
valueCode = `utils.toNumber(${valueCode})`; valueCode = `utils.toNumber(${valueCode})`;
} }
handler = `(ev) => {context.state['${value}'] = ${valueCode}}`; handler = `(ev) => {${expr} = ${valueCode}}`;
} }
ctx.addLine( ctx.addLine(
`extra.handlers['${event}' + ${nodeID}] = extra.handlers['${event}' + ${nodeID}] || (${handler});` `extra.handlers['${event}' + ${nodeID}] = extra.handlers['${event}' + ${nodeID}] || (${handler});`
+65 -27
View File
@@ -1,6 +1,8 @@
import { EventBus } from "../core/event_bus"; import { EventBus } from "../core/event_bus";
import { h, patch, VNode } from "../vdom/index"; import { h, patch, VNode } from "../vdom/index";
import { Context } from "./context"; import { Context } from "./context";
import { shallowEqual } from "../utils";
import { addNS } from "../vdom/vdom";
/** /**
* Owl QWeb Engine * Owl QWeb Engine
@@ -92,18 +94,17 @@ const UTILS: Utils = {
} }
return expr; return expr;
}, },
shallowEqual(p1, p2) { shallowEqual,
for (let k in p1) { addNameSpace(vnode) {
if (p1[k] !== p2[k]) { addNS(vnode.data, vnode.children, vnode.sel);
return false;
}
}
return true;
} }
}; };
function parseXML(xml: string): Document { function parseXML(xml: string): Document {
const parser = new DOMParser(); const parser = new DOMParser();
// we remove comments from the xml string
xml = xml.replace(/<!--[\s\S]*?-->/g, "");
const doc = parser.parseFromString(xml, "text/xml"); const doc = parser.parseFromString(xml, "text/xml");
if (doc.getElementsByTagName("parsererror").length) { if (doc.getElementsByTagName("parsererror").length) {
let msg = "Invalid XML in template."; let msg = "Invalid XML in template.";
@@ -131,13 +132,11 @@ function parseXML(xml: string): Document {
return doc; return doc;
} }
let nextID = 1;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// QWeb rendering engine // QWeb rendering engine
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
export class QWeb extends EventBus { export class QWeb extends EventBus {
templates: { [name: string]: Template } = {}; templates: { [name: string]: Template };
static utils = UTILS; static utils = UTILS;
static components = Object.create(null); static components = Object.create(null);
@@ -149,24 +148,29 @@ export class QWeb extends EventBus {
}; };
static DIRECTIVES: Directive[] = []; static DIRECTIVES: Directive[] = [];
static TEMPLATES: { [name: string]: Template } = {};
static nextId: number = 1;
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;
// the id field is useful to be able to hash qweb instances. The current
// use case is that component's templates are qweb dependant, and need to be
// able to map a qweb instance to a template name.
id = nextID++;
// 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.
slots = {}; static slots = {};
nextSlotId = 1; static nextSlotId = 1;
// recursiveTemplates contains sub templates called with t-call, but which
// ends up in recursive situations. This is very similar to the slot situation,
// as in we need to propagate the scope.
recursiveFns = {};
isUpdating: boolean = false; isUpdating: boolean = false;
constructor(data?: string) { constructor(data?: string) {
super(); super();
this.templates = Object.create(QWeb.TEMPLATES);
if (data) { if (data) {
this.addTemplates(data); this.addTemplates(data);
} }
@@ -184,13 +188,27 @@ export class QWeb extends EventBus {
} }
} }
static register(name: string, Component: any) { static registerComponent(name: string, Component: any) {
if (QWeb.components[name]) { if (QWeb.components[name]) {
throw new Error(`Component '${name}' has already been registered`); throw new Error(`Component '${name}' has already been registered`);
} }
QWeb.components[name] = Component; QWeb.components[name] = Component;
} }
/**
* Register globally a template. All QWeb instances will obtain their
* templates from their own template map, and then, from the global static
* TEMPLATES property.
*/
static registerTemplate(name: string, template: string) {
if (QWeb.TEMPLATES[name]) {
throw new Error(`Template '${name}' has already been registered`);
}
const qweb = new QWeb();
qweb.addTemplate(name, template);
QWeb.TEMPLATES[name] = qweb.templates[name];
}
/** /**
* Add a template to the internal template map. Note that it is not * Add a template to the internal template map. Note that it is not
* immediately compiled. * immediately compiled.
@@ -212,7 +230,7 @@ export class QWeb extends EventBus {
* template, with the name given by the t-name attribute. * template, with the name given by the t-name attribute.
*/ */
addTemplates(xmlstr: string | Document) { addTemplates(xmlstr: string | Document) {
const doc = typeof xmlstr === 'string' ? parseXML(xmlstr) : xmlstr; const doc = typeof xmlstr === "string" ? parseXML(xmlstr) : xmlstr;
const templates = doc.getElementsByTagName("templates")[0]; const templates = doc.getElementsByTagName("templates")[0];
if (!templates) { if (!templates) {
return; return;
@@ -230,7 +248,7 @@ export class QWeb extends EventBus {
this._processTemplate(elem); this._processTemplate(elem);
const template = { const template = {
elem, elem,
fn: (context, extra) => { fn: function(this: QWeb, context, extra) {
const compiledFunction = this._compile(name, elem); const compiledFunction = this._compile(name, elem);
template.fn = compiledFunction; template.fn = compiledFunction;
return compiledFunction.call(this, context, extra); return compiledFunction.call(this, context, extra);
@@ -298,8 +316,8 @@ export class QWeb extends EventBus {
* to render a full component tree, since this is an asynchronous operation. * to render a full component tree, since this is an asynchronous operation.
* This method can only render templates without components. * This method can only render templates without components.
*/ */
renderToString(name: string, context: EvalContext = {}): string { renderToString(name: string, context: EvalContext = {}, extra?: any): string {
const vnode = this.render(name, context); const vnode = this.render(name, context, extra);
if (vnode.sel === undefined) { if (vnode.sel === undefined) {
return vnode.text!; return vnode.text!;
} }
@@ -327,23 +345,28 @@ export class QWeb extends EventBus {
_compile(name: string, elem: Element, parentContext?: Context): CompiledTemplate { _compile(name: string, elem: Element, parentContext?: Context): CompiledTemplate {
const isDebug = elem.attributes.hasOwnProperty("t-debug"); const isDebug = elem.attributes.hasOwnProperty("t-debug");
const ctx = new Context(name); const ctx = new Context(name);
if (elem.tagName !== "t") {
ctx.shouldDefineResult = false;
}
if (parentContext) { if (parentContext) {
ctx.templates = Object.create(parentContext.templates);
ctx.variables = Object.create(parentContext.variables); ctx.variables = Object.create(parentContext.variables);
ctx.nextID = parentContext.parentNode! + 1; ctx.nextID = parentContext.nextID + 1;
ctx.parentNode = parentContext.parentNode!; ctx.parentNode = parentContext.parentNode || ctx.nextID++;
ctx.allowMultipleRoots = true; ctx.allowMultipleRoots = true;
ctx.hasParentWidget = true; ctx.hasParentWidget = true;
ctx.shouldDefineResult = false;
ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`); ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`);
for (let v in parentContext.variables) { for (let v in parentContext.variables) {
let variable = <any>parentContext.variables[v]; let variable = <any>parentContext.variables[v];
if (variable.id) { if (variable.id) {
ctx.addLine(`let ${variable.id} = extra.vars.${variable.id}`); ctx.addLine(`let ${variable.id} = extra.fiber.vars.${variable.id}`);
} }
} }
} }
if (parentContext) { if (parentContext) {
ctx.addLine(" Object.assign(context, extra.scope);"); ctx.addLine(" Object.assign(context, extra.fiber.scope);");
} }
this._compileNode(elem, ctx); this._compileNode(elem, ctx);
@@ -405,6 +428,7 @@ export class QWeb extends EventBus {
// template rendering. // template rendering.
let nodeID = ctx.generateID(); let nodeID = ctx.generateID();
ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`); ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`);
ctx.addLine(`result = vn${nodeID};`);
ctx.rootContext.rootNode = nodeID; ctx.rootContext.rootNode = nodeID;
ctx.rootContext.parentTextNode = nodeID; ctx.rootContext.parentTextNode = nodeID;
} }
@@ -479,6 +503,7 @@ export class QWeb extends EventBus {
if (node.nodeName !== "t") { if (node.nodeName !== "t") {
let nodeID = this._compileGenericNode(node, ctx, withHandlers); let nodeID = this._compileGenericNode(node, ctx, withHandlers);
ctx = ctx.withParent(nodeID); ctx = ctx.withParent(nodeID);
ctx = ctx.subContext("currentKey", ctx.lastNodeKey);
let nodeHooks = {}; let nodeHooks = {};
let addNodeHook = function(hook, handler) { let addNodeHook = function(hook, handler) {
nodeHooks[hook] = nodeHooks[hook] || []; nodeHooks[hook] = nodeHooks[hook] || [];
@@ -516,6 +541,17 @@ export class QWeb extends EventBus {
} }
this._compileChildren(node, ctx); this._compileChildren(node, ctx);
// svg support
// we hadd svg namespace if it is a svg or if it is a g, but only if it is
// the root node. This is the easiest way to support svg sub components:
// they need to have a g tag as root. Otherwise, we would need a complete
// list of allowed svg tags.
const shouldAddNS =
node.nodeName === "svg" || (node.nodeName === "g" && ctx.rootNode === ctx.parentNode);
if (shouldAddNS) {
ctx.rootContext.shouldDefineUtils = true;
ctx.addLine(`utils.addNameSpace(vn${ctx.parentNode});`);
}
for (let { directive, value, fullName } of validDirectives) { for (let { directive, value, fullName } of validDirectives) {
if (directive.finalize) { if (directive.finalize) {
@@ -647,7 +683,9 @@ export class QWeb extends EventBus {
let nodeID = ctx.generateID(); let nodeID = ctx.generateID();
let nodeKey: any = (<Element>node).getAttribute("t-key"); let nodeKey: any = (<Element>node).getAttribute("t-key");
if (nodeKey) { if (nodeKey) {
nodeKey = ctx.formatExpression(nodeKey); ctx.addLine(`const nodeKey${nodeID} = ${ctx.formatExpression(nodeKey)}`);
nodeKey = `nodeKey${nodeID}`;
ctx.lastNodeKey = nodeKey;
} else { } else {
nodeKey = nodeID; nodeKey = nodeID;
} }
+7 -7
View File
@@ -1,18 +1,18 @@
import { Component } from "../component/component"; import { Component } from "../component/component";
import { xml } from "../tags";
import { Destination, RouterEnv } from "./Router"; import { Destination, RouterEnv } from "./Router";
export const LINK_TEMPLATE_NAME = "__owl__-router-link"; type Props = Destination;
export const LINK_TEMPLATE = `
export class Link<Env extends RouterEnv> extends Component<Env, Props> {
static template = xml`
<a t-att-class="{'router-link-active': isActive }" <a t-att-class="{'router-link-active': isActive }"
t-att-href="href" t-att-href="href"
t-on-click="navigate"> t-on-click="navigate">
<t t-slot="default"/> <t t-slot="default"/>
</a>`; </a>
`;
type Props = Destination;
export class Link<Env extends RouterEnv> extends Component<Env, Props, {}> {
template = LINK_TEMPLATE_NAME;
href: string = this.env.router.destToPath(this.props); href: string = this.env.router.destToPath(this.props);
async willUpdateProps(nextProps) { async willUpdateProps(nextProps) {
+18
View File
@@ -0,0 +1,18 @@
import { Component } from "../component/component";
import { xml } from "../tags";
export class RouteComponent extends Component<any, {}> {
static template = xml`
<t>
<t
t-if="routeComponent"
t-component="routeComponent"
t-key="env.router.currentRouteName"
t-props="env.router.currentParams" />
</t>
`;
get routeComponent(): any {
return this.env.router.currentRoute && this.env.router.currentRoute.component;
}
}
+26 -13
View File
@@ -1,7 +1,6 @@
import { Env } from "../component/component"; import { Env } from "../component/component";
import { QWeb } from "../qweb/index"; import { QWeb } from "../qweb/index";
import { makeDirective } from "./directive"; import { shallowEqual } from "../utils";
import { LINK_TEMPLATE, LINK_TEMPLATE_NAME } from "./Link";
type NavigationGuard = (info: { type NavigationGuard = (info: {
env: Env; env: Env;
@@ -74,7 +73,7 @@ export class Router {
partialRoute.name = "__route__" + nextId++; partialRoute.name = "__route__" + nextId++;
} }
if (partialRoute.component) { if (partialRoute.component) {
QWeb.register("__component__" + partialRoute.name, partialRoute.component); QWeb.registerComponent("__component__" + partialRoute.name, partialRoute.component);
} }
if (partialRoute.redirect) { if (partialRoute.redirect) {
this.validateDestination(partialRoute.redirect); this.validateDestination(partialRoute.redirect);
@@ -83,13 +82,6 @@ export class Router {
this.routes[partialRoute.name] = partialRoute as Route; this.routes[partialRoute.name] = partialRoute as Route;
this.routeIds.push(partialRoute.name); this.routeIds.push(partialRoute.name);
} }
(this as any)._listener = () => this.matchAndApplyRules(this.currentPath());
window.addEventListener("popstate", (this as any)._listener);
// setup link and directive
env.qweb.addTemplate(LINK_TEMPLATE_NAME, LINK_TEMPLATE);
QWeb.addDirective(makeDirective(<RouterEnv>env));
} }
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
@@ -97,6 +89,11 @@ export class Router {
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
async start() { async start() {
(this as any)._listener = ev => this._navigate(this.currentPath(), ev);
window.addEventListener("popstate", (this as any)._listener);
if (this.mode === "hash") {
window.addEventListener("hashchange", (this as any)._listener);
}
const result = await this.matchAndApplyRules(this.currentPath()); const result = await this.matchAndApplyRules(this.currentPath());
if (result.type === "match") { if (result.type === "match") {
this.currentRoute = result.route; this.currentRoute = result.route;
@@ -110,18 +107,27 @@ export class Router {
async navigate(to: Destination): Promise<boolean> { async navigate(to: Destination): Promise<boolean> {
const path = this.destToPath(to); const path = this.destToPath(to);
return this._navigate(path);
}
async _navigate(path: string, ev?: any): Promise<boolean> {
const initialName = this.currentRouteName; const initialName = this.currentRouteName;
const initialParams = this.currentParams;
const result = await this.matchAndApplyRules(path); const result = await this.matchAndApplyRules(path);
if (result.type === "match") { if (result.type === "match") {
const finalPath = this.routeToPath(result.route, result.params); const finalPath = this.routeToPath(result.route, result.params);
const isPopStateEvent = ev && ev instanceof PopStateEvent;
if (!isPopStateEvent) {
this.setUrlFromPath(finalPath); this.setUrlFromPath(finalPath);
}
this.currentRoute = result.route; this.currentRoute = result.route;
this.currentParams = result.params; this.currentParams = result.params;
} else if (result.type === "nomatch") { } else if (result.type === "nomatch") {
this.currentRoute = null; this.currentRoute = null;
this.currentParams = null; this.currentParams = null;
} }
if (this.currentRouteName !== initialName) { const didChange =
this.currentRouteName !== initialName || !shallowEqual(this.currentParams, initialParams);
if (didChange) {
this.env.qweb.forceUpdate(); this.env.qweb.forceUpdate();
return true; return true;
} }
@@ -142,9 +148,12 @@ export class Router {
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
private setUrlFromPath(path: string) { private setUrlFromPath(path: string) {
const url = location.origin + path; const separator = this.mode === "hash" ? "/" : "";
const url = location.origin + separator + path;
if (url !== window.location.href) {
window.history.pushState({}, path, url); window.history.pushState({}, path, url);
} }
}
private validateDestination(dest: Destination) { private validateDestination(dest: Destination) {
if ((!dest.path && !dest.to) || (dest.path && dest.to)) { if ((!dest.path && !dest.to) || (dest.path && dest.to)) {
@@ -164,7 +173,8 @@ export class Router {
parts[i] = <string>params[key]; parts[i] = <string>params[key];
} }
} }
return parts.join("/"); const prefix = this.mode === "hash" ? "#" : "";
return prefix + parts.join("/");
} }
private currentPath(): string { private currentPath(): string {
@@ -223,6 +233,9 @@ export class Router {
if (route.path === "*") { if (route.path === "*") {
return {}; return {};
} }
if (path.startsWith("#")) {
path = path.slice(1);
}
const descrParts = route.path.split("/"); const descrParts = route.path.split("/");
const targetParts = path.split("/"); const targetParts = path.split("/");
const l = descrParts.length; const l = descrParts.length;
-28
View File
@@ -1,28 +0,0 @@
import { RouterEnv } from "./Router";
export function makeDirective(env: RouterEnv) {
return {
name: "routecomponent",
priority: 13,
atNodeEncounter({ node }): boolean {
let first = true;
const router = env.router;
for (let name of router.routeIds) {
const route = router.routes[name];
if (route.component) {
// make new t t-component element
const comp = node.ownerDocument.createElement("t");
comp.setAttribute("t-component", "__component__" + route.name);
comp.setAttribute(first ? "t-if" : "t-elif", `env.router.currentRouteName === '${route.name}'`);
first = false;
for (let param of route.params) {
comp.setAttribute(param, `env.router.currentParams.${param}`);
}
node.appendChild(comp);
}
}
node.removeAttribute("t-routecomponent");
return false;
}
};
}
+46 -18
View File
@@ -1,4 +1,5 @@
import { Component, Env } from "../component/component"; import { Component, Env, Fiber } from "../component/component";
import { VNode } from "../vdom/index";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Connect function // Connect function
@@ -6,7 +7,7 @@ import { Component, Env } from "../component/component";
type HashFunction = (a: any, b: any) => number; type HashFunction = (a: any, b: any) => number;
export class ConnectedComponent<T extends Env, P, S> extends Component<T, P, S> { export class ConnectedComponent<T extends Env, P> extends Component<T, P> {
deep: boolean = true; deep: boolean = true;
getStore(env) { getStore(env) {
return env.store; return env.store;
@@ -46,22 +47,20 @@ export class ConnectedComponent<T extends Env, P, S> extends Component<T, P, S>
/** /**
* Need to do this here so 'deep' can be overrided by subcomponent easily * Need to do this here so 'deep' can be overrided by subcomponent easily
*/ */
async __prepareAndRender( async __prepareAndRender(fiber: Fiber<P>): Promise<VNode> {
scope?: Object,
vars?: any
): ReturnType<Component<any, any, any>["__prepareAndRender"]> {
const store = this.getStore(this.env); const store = this.getStore(this.env);
const ownProps = this.props || {}; const ownProps = this.props || {};
this.storeProps = (<any>this.constructor).mapStoreToProps(store.state, ownProps, store.getters); this.storeProps = (<any>this.constructor).mapStoreToProps(store.state, ownProps, store.getters);
const observer = store.observer; const observer = store.observer;
const revFn = this.deep ? observer.deepRevNumber : observer.revNumber; const revFn = this.deep ? observer.deepRevNumber : observer.revNumber;
(this.__owl__ as any).store = store; (this.__owl__ as any).store = store;
(this.__owl__ as any).ownProps = this.props;
(this.__owl__ as any).revFn = revFn.bind(observer); (this.__owl__ as any).revFn = revFn.bind(observer);
(this.__owl__ as any).storeHash = this.hashFunction(this.storeProps, { (this.__owl__ as any).storeHash = this.hashFunction(this.storeProps, {
prevStoreProps: this.storeProps prevStoreProps: this.storeProps
}); });
(this.__owl__ as any).rev = observer.rev; (this.__owl__ as any).rev = observer.rev;
return super.__prepareAndRender(scope, vars); return super.__prepareAndRender(fiber);
} }
/** /**
* We do not use the mounted hook here for a subtle reason: we want the * We do not use the mounted hook here for a subtle reason: we want the
@@ -81,13 +80,44 @@ export class ConnectedComponent<T extends Env, P, S> extends Component<T, P, S>
super.__destroy(parent); super.__destroy(parent);
} }
async __updateProps(nextProps: P, f, p, s, v) { async render(force: boolean = false) {
this.__updateStoreProps(this.props);
// this is quite technical, so this deserves some explanation.
// When we have a connected component, it can be updated for 3 reasons:
// - some internal state changes (this will go through this method)
// - some props changes (if a parent is changed and need to rerender itself)
// - a store update
//
// It is possible (with connected component and parent) to have the following
// situation: the parent component is rendered first (from its state change),
// then immediately after, it is rendered (from store update). Then, if the
// __checkUpdate method is immediately over, the children component will
// be rendered again by the store update, even though it is supposed to be
// destroyed by the first rendering.
//
// So, the solution is to keep the information that there is a current
// rendering occuring with the same store state, the same props, and return
// that in the __checkUpdate method. To do this, we use the renderPromise
// deferred, which is not used by the component system once the
// component is ready, so we can use it for our own purpose.
(this.__owl__ as any).renderPromise = super.render(force);
return (this.__owl__ as any).renderPromise;
}
async __updateProps(nextProps: P, f, s, v) {
this.__updateStoreProps(nextProps); this.__updateStoreProps(nextProps);
return super.__updateProps(nextProps, f, p, s, v); return super.__updateProps(nextProps, f, s, v);
} }
__updateStoreProps(nextProps): boolean { __updateStoreProps(nextProps): boolean {
const store = (this.__owl__ as any).store; const __owl__ = this.__owl__ as any;
const store = __owl__.store;
const observer = store.observer;
if (observer.rev === __owl__.rev && nextProps === __owl__.ownProps) {
return false;
}
const storeProps = (<any>this.constructor).mapStoreToProps( const storeProps = (<any>this.constructor).mapStoreToProps(
store.state, store.state,
nextProps, nextProps,
@@ -97,23 +127,21 @@ export class ConnectedComponent<T extends Env, P, S> extends Component<T, P, S>
const storeHash = this.hashFunction(storeProps, options); const storeHash = this.hashFunction(storeProps, options);
this.storeProps = storeProps; this.storeProps = storeProps;
let didChange = options.didChange; let didChange = options.didChange;
if (storeHash !== (this.__owl__ as any).storeHash) { if (storeHash !== __owl__.storeHash) {
(this.__owl__ as any).storeHash = storeHash; __owl__.storeHash = storeHash;
didChange = true; didChange = true;
} }
(this.__owl__ as any).rev = store.observer.rev; __owl__.rev = store.observer.rev;
__owl__.ownProps = nextProps;
return didChange; return didChange;
} }
async __checkUpdate() { async __checkUpdate() {
const observer = (this.__owl__ as any).store.observer;
if (observer.rev === (this.__owl__ as any).rev) {
// update was already done by updateProps, from parent
return;
}
const didChange = this.__updateStoreProps(this.props); const didChange = this.__updateStoreProps(this.props);
if (didChange) { if (didChange) {
return this.render(); return this.render();
} }
// see note in render method
return (this.__owl__ as any).renderPromise;
} }
} }
View File
+1 -1
View File
@@ -56,7 +56,7 @@ export class Store extends EventBus {
if (config.getters) { if (config.getters) {
const firstArg = { const firstArg = {
state: this.state, state: this.state,
getters: this.getters, getters: this.getters
}; };
for (let g in config.getters) { for (let g in config.getters) {
this.getters[g] = config.getters[g].bind(this, firstArg); this.getters[g] = config.getters[g].bind(this, firstArg);
+27
View File
@@ -0,0 +1,27 @@
import { QWeb } from "./qweb/index";
/**
* Owl Tags
*
* We have here a (very) small collection of tag functions:
*
* - xml
*
* The plan is to add a few other tags such as css, globalcss.
*/
/**
* XML tag helper for defining templates. With this, one can simply define
* an inline template with just the template xml:
* ```js
* class A extends Component {
* static template = xml`<div>some template</div>`;
* }
* ```
*/
export function xml(strings, ...args) {
const name = `__template__${QWeb.nextId++}`;
const value = String.raw(strings, ...args);
QWeb.registerTemplate(name, value);
return name;
}
+10 -3
View File
@@ -48,9 +48,7 @@ export async function loadTemplates(url: string): Promise<string> {
if (!result.ok) { if (!result.ok) {
throw new Error("Error while fetching xml templates"); throw new Error("Error while fetching xml templates");
} }
let templates = await result.text(); return await result.text();
templates = templates.replace(/<!--[\s\S]*?-->/g, "");
return templates;
} }
export function escape(str: string | number | undefined): string { export function escape(str: string | number | undefined): string {
@@ -95,3 +93,12 @@ export function debounce(func: Function, wait: number, immediate?: boolean): Fun
} }
}; };
} }
export function shallowEqual(p1, p2): boolean {
for (let k in p1) {
if (p1[k] !== p2[k]) {
return false;
}
}
return true;
}
+8 -12
View File
@@ -526,7 +526,7 @@ const htmlDomApi = {
parentNode, parentNode,
nextSibling, nextSibling,
tagName, tagName,
setTextContent, setTextContent
} as DOMAPI; } as DOMAPI;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -564,13 +564,17 @@ type VNodeChildElement = VNode | string | number | undefined | null;
type ArrayOrElement<T> = T | T[]; type ArrayOrElement<T> = T | T[];
type VNodeChildren = ArrayOrElement<VNodeChildElement>; type VNodeChildren = ArrayOrElement<VNodeChildElement>;
function addNS(data: any, children: VNodes | undefined, sel: string | undefined): void { export function addNS(data: any, children: VNodes | undefined, sel: string | undefined): void {
data.ns = "http://www.w3.org/2000/svg"; data.ns = "http://www.w3.org/2000/svg";
if (sel !== "foreignObject" && children !== undefined) { if (sel !== "foreignObject" && children !== undefined) {
for (let i = 0, iLen = children.length; i < iLen; ++i) { for (let i = 0, iLen = children.length; i < iLen; ++i) {
let childData = children[i].data; const child = children[i];
if (child === null) {
continue;
}
let childData = child.data;
if (childData !== undefined) { if (childData !== undefined) {
addNS(childData, (children[i] as VNode).children as VNodes, children[i].sel); addNS(childData, (child as VNode).children as VNodes, child.sel);
} }
} }
} }
@@ -612,13 +616,5 @@ export function h(sel: any, b?: any, c?: any): VNode {
children[i] = vnode(undefined, undefined, undefined, children[i], undefined); children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
} }
} }
if (
sel[0] === "s" &&
sel[1] === "v" &&
sel[2] === "g" &&
(sel.length === 3 || sel[3] === "." || sel[3] === "#")
) {
addNS(data, children, sel);
}
return vnode(sel, data, children, text, undefined); return vnode(sel, data, children, text, undefined);
} }
+18 -14
View File
@@ -10,15 +10,16 @@ exports[`animations t-transition combined with component 1`] = `
var h = this.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
result = vn1;
//COMPONENT //COMPONENT
let def3; let def3;
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false; let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
let _2_index = c1.length; let _2_index = c1.length;
c1.push(null); c1.push(null);
let props4 = {}; let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) { if (w4 && w4.__owl__.currentFiber && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) { if (utils.shallowEqual(props4, w4.__owl__.currentFiber.props)) {
def3 = w4.__owl__.renderPromise; def3 = w4.__owl__.currentFiber.promise;
} else { } else {
w4.destroy(); w4.destroy();
w4 = false; w4 = false;
@@ -26,17 +27,17 @@ exports[`animations t-transition combined with component 1`] = `
} }
if (!w4) { if (!w4) {
let componentKey4 = \`Child\`; let componentKey4 = \`Child\`;
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4]; let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4]|| context['Child'];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')} if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(parent, props4); w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id; parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare(); def3 = w4.__prepare(extra.fiber, undefined, undefined);
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => { def3 = def3.then(vnode=>{if (w4.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
w4.destroy(); w4.destroy();
}; };
utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;}); utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else { } else {
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue); def3 = def3 || w4.__updateProps(props4, extra.fiber, undefined, undefined);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;}); def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
} }
extra.promises.push(def3); extra.promises.push(def3);
@@ -54,6 +55,7 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
var h = this.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
result = vn1;
if (context['state'].display) { if (context['state'].display) {
//COMPONENT //COMPONENT
let def3; let def3;
@@ -61,9 +63,9 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
let _2_index = c1.length; let _2_index = c1.length;
c1.push(null); c1.push(null);
let props4 = {}; let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) { if (w4 && w4.__owl__.currentFiber && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) { if (utils.shallowEqual(props4, w4.__owl__.currentFiber.props)) {
def3 = w4.__owl__.renderPromise; def3 = w4.__owl__.currentFiber.promise;
} else { } else {
w4.destroy(); w4.destroy();
w4 = false; w4 = false;
@@ -71,17 +73,17 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
} }
if (!w4) { if (!w4) {
let componentKey4 = \`Child\`; let componentKey4 = \`Child\`;
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4]; let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4]|| context['Child'];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')} if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(parent, props4); w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id; parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare(); def3 = w4.__prepare(extra.fiber, undefined, undefined);
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => { def3 = def3.then(vnode=>{if (w4.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
w4.destroy(); w4.destroy();
}; };
utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;}); utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else { } else {
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue); def3 = def3 || w4.__updateProps(props4, extra.fiber, undefined, undefined);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;}); def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
} }
extra.promises.push(def3); extra.promises.push(def3);
@@ -97,6 +99,7 @@ exports[`animations t-transition with no delay/duration 1`] = `
var h = this.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1); var vn1 = h('span', p1, c1);
result = vn1;
p1.hook = { p1.hook = {
insert: vn => { insert: vn => {
utils.transitionInsert(vn, 'jupiler'); utils.transitionInsert(vn, 'jupiler');
@@ -117,6 +120,7 @@ exports[`animations t-transition, on a simple node (insert) 1`] = `
var h = this.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('span', p1, c1); var vn1 = h('span', p1, c1);
result = vn1;
p1.hook = { p1.hook = {
insert: vn => { insert: vn => {
utils.transitionInsert(vn, 'chimay'); utils.transitionInsert(vn, 'chimay');
+19 -21
View File
@@ -1,5 +1,6 @@
import { Component, Env } from "../src/component/component"; import { Component, Env } from "../src/component/component";
import { QWeb } from "../src/qweb/index"; import { QWeb } from "../src/qweb/index";
import { useState, useRef } from "../src/hooks";
import { import {
makeDeferred, makeDeferred,
makeTestFixture, makeTestFixture,
@@ -36,7 +37,7 @@ afterEach(() => {
fixture.remove(); fixture.remove();
}); });
class Widget extends Component<any, any, any> {} class Widget extends Component<any, any> {}
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Tests // Tests
@@ -106,7 +107,7 @@ describe("animations", () => {
`<div><span t-if="!state.hide" t-transition="chimay">blue</span></div>` `<div><span t-if="!state.hide" t-transition="chimay">blue</span></div>`
); );
class TestWidget extends Widget { class TestWidget extends Widget {
state = { hide: false }; state = useState({ hide: false });
} }
const widget = new TestWidget(env); const widget = new TestWidget(env);
@@ -148,7 +149,8 @@ describe("animations", () => {
`<div><span t-ref="span" t-transition="chimay">blue</span></div>` `<div><span t-ref="span" t-transition="chimay">blue</span></div>`
); );
class TestWidget extends Widget { class TestWidget extends Widget {
state = { hide: false }; state = useState({ hide: false });
span = useRef("span");
} }
const widget = new TestWidget(env); const widget = new TestWidget(env);
@@ -163,7 +165,7 @@ describe("animations", () => {
}); });
await widget.mount(fixture); await widget.mount(fixture);
spanNode = widget.el!.children[0]; spanNode = widget.el!.children[0];
expect(widget.refs.span).toBe(spanNode); expect(widget.span.el).toBe(spanNode);
expect(spanNode.className).toBe("chimay-enter chimay-enter-active"); expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
await def; // wait for the mocked repaint to be done await def; // wait for the mocked repaint to be done
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
@@ -175,10 +177,10 @@ describe("animations", () => {
env.qweb.addTemplate("Parent", `<div><Child t-transition="chimay"/></div>`); env.qweb.addTemplate("Parent", `<div><Child t-transition="chimay"/></div>`);
env.qweb.addTemplate("Child", `<span>blue</span>`); env.qweb.addTemplate("Child", `<span>blue</span>`);
class Parent extends Widget {
components = { Child: Child };
}
class Child extends Widget {} class Child extends Widget {}
class Parent extends Widget {
static components = { Child: Child };
}
const widget = new Parent(env); const widget = new Parent(env);
let def = makeDeferred(); let def = makeDeferred();
@@ -214,11 +216,11 @@ describe("animations", () => {
`<div><t t-if="state.display" t-component="Child" t-transition="chimay"/></div>` `<div><t t-if="state.display" t-component="Child" t-transition="chimay"/></div>`
); );
env.qweb.addTemplate("Child", `<span>blue</span>`); env.qweb.addTemplate("Child", `<span>blue</span>`);
class Parent extends Widget {
components = { Child: Child };
state = { display: true };
}
class Child extends Widget {} class Child extends Widget {}
class Parent extends Widget {
static components = { Child: Child };
state = useState({ display: true });
}
const widget = new Parent(env); const widget = new Parent(env);
let def = makeDeferred(); let def = makeDeferred();
@@ -269,7 +271,7 @@ describe("animations", () => {
`<div><span t-if="state.flag" t-mounted="f" t-transition="chimay">blue</span></div>` `<div><span t-if="state.flag" t-mounted="f" t-transition="chimay">blue</span></div>`
); );
class TestWidget extends Widget { class TestWidget extends Widget {
state = { flag: false }; state = useState({ flag: false });
f() {} f() {}
} }
const widget = new TestWidget(env); const widget = new TestWidget(env);
@@ -297,10 +299,8 @@ describe("animations", () => {
</templates>` </templates>`
); );
class Parent extends Widget { class Parent extends Widget {
constructor(parent) { state = useState({ flag: false });
super(parent);
this.state = { flag: false };
}
toggle() { toggle() {
this.state.flag = !this.state.flag; this.state.flag = !this.state.flag;
} }
@@ -356,11 +356,9 @@ describe("animations", () => {
); );
class Child extends Widget {} class Child extends Widget {}
class Parent extends Widget { class Parent extends Widget {
components = { Child }; static components = { Child };
constructor(parent) { state = useState({ flag: false });
super(parent);
this.state = { flag: false };
}
toggle() { toggle() {
this.state.flag = !this.state.flag; this.state.flag = !this.state.flag;
} }
File diff suppressed because it is too large Load Diff
@@ -14,15 +14,16 @@ exports[`props validation props are validated in dev mode (code snapshot) 1`] =
var h = this.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
result = vn1;
//COMPONENT //COMPONENT
let def3; let def3;
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false; let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
let _2_index = c1.length; let _2_index = c1.length;
c1.push(null); c1.push(null);
let props4 = {message:1}; let props4 = {message:1};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) { if (w4 && w4.__owl__.currentFiber && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) { if (utils.shallowEqual(props4, w4.__owl__.currentFiber.props)) {
def3 = w4.__owl__.renderPromise; def3 = w4.__owl__.currentFiber.promise;
} else { } else {
w4.destroy(); w4.destroy();
w4 = false; w4 = false;
@@ -30,16 +31,16 @@ exports[`props validation props are validated in dev mode (code snapshot) 1`] =
} }
if (!w4) { if (!w4) {
let componentKey4 = \`Child\`; let componentKey4 = \`Child\`;
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4]; let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4]|| context['Child'];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')} if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
utils.validateProps(W4, props4) utils.validateProps(W4, props4)
w4 = new W4(parent, props4); w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id; parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare(); def3 = w4.__prepare(extra.fiber, undefined, undefined);
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;}); def3 = def3.then(vnode=>{if (w4.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else { } else {
utils.validateProps(w4.constructor, props4) utils.validateProps(w4.constructor, props4)
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue); def3 = def3 || w4.__updateProps(props4, extra.fiber, undefined, undefined);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;}); def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
} }
extra.promises.push(def3); extra.promises.push(def3);
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -22,7 +22,7 @@ afterEach(() => {
QWeb.dev = dev; QWeb.dev = dev;
}); });
class Widget extends Component<any, any, any> {} class Widget extends Component<any, any> {}
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Tests // Tests
@@ -235,7 +235,7 @@ describe("props validation", () => {
static props = ["message"]; static props = ["message"];
} }
class App extends Widget { class App extends Widget {
components = { Child }; static components = { Child };
} }
const app = new App(env); const app = new App(env);
await app.mount(fixture); await app.mount(fixture);
@@ -310,7 +310,6 @@ describe("props validation", () => {
QWeb.utils.validateProps(TestWidget, { message: null }); QWeb.utils.validateProps(TestWidget, { message: null });
}).toThrow(); }).toThrow();
}); });
}); });
describe("default props", () => { describe("default props", () => {
@@ -335,7 +334,8 @@ describe("default props", () => {
const w = new TestWidget(env, { p: 1 }); const w = new TestWidget(env, { p: 1 });
await w.mount(fixture); await w.mount(fixture);
expect(fixture.innerHTML).toMatchSnapshot(); expect(fixture.innerHTML).toMatchSnapshot();
await w.__updateProps({}); const fiber = w.__createFiber(false, undefined, undefined, undefined);
await w.__updateProps({}, fiber);
await w.render(); await w.render();
expect(w.props.p).toBe(4); expect(w.props.p).toBe(4);
expect(fixture.innerHTML).toMatchSnapshot(); expect(fixture.innerHTML).toMatchSnapshot();
+1 -1
View File
@@ -65,7 +65,7 @@ describe("observer", () => {
expect(observer.revNumber(obj)).toBe(1); expect(observer.revNumber(obj)).toBe(1);
expect(observer.deepRevNumber(obj)).toBe(1); expect(observer.deepRevNumber(obj)).toBe(1);
expect(observer.rev).toBe(1); expect(observer.rev).toBe(1);
expect(typeof obj.date.getFullYear()).toBe('number'); expect(typeof obj.date.getFullYear()).toBe("number");
expect(obj.date).toBe(date); expect(obj.date).toBe(date);
obj.date = new Date(); obj.date = new Date();
+29 -3
View File
@@ -5,6 +5,27 @@ import "../src/qweb/base_directives";
import "../src/qweb/extensions"; import "../src/qweb/extensions";
import "../src/component/directive"; import "../src/component/directive";
// Some static cleanup
let nextSlotId;
let slots;
let nextId;
let TEMPLATES;
beforeEach(() => {
nextSlotId = QWeb.nextSlotId;
slots = Object.assign({}, QWeb.slots);
nextId = QWeb.nextId;
TEMPLATES = Object.assign({}, QWeb.TEMPLATES);
});
afterEach(() => {
QWeb.nextSlotId = nextSlotId;
QWeb.slots = slots;
QWeb.nextId = nextId;
QWeb.TEMPLATES = TEMPLATES;
});
// helpers
export function nextMicroTick(): Promise<void> { export function nextMicroTick(): Promise<void> {
return Promise.resolve(); return Promise.resolve();
} }
@@ -78,10 +99,15 @@ export function renderToDOM(
* Note that the result of renderToString is guaranteed to be the same as the * Note that the result of renderToString is guaranteed to be the same as the
* one from QWeb. * one from QWeb.
*/ */
export function renderToString(qweb: QWeb, t: string, context: EvalContext = {}): string { export function renderToString(
const node = renderToDOM(qweb, t, context); qweb: QWeb,
t: string,
context: EvalContext = {},
extra?: any
): string {
const node = renderToDOM(qweb, t, context, extra);
const result = node instanceof Text ? node.textContent! : node.outerHTML; const result = node instanceof Text ? node.textContent! : node.outerHTML;
if (result !== qweb.renderToString(t, context)) { if (result !== qweb.renderToString(t, context, extra)) {
throw new Error("HTML string returned by renderToString helper does not match QWeb render"); throw new Error("HTML string returned by renderToString helper does not match QWeb render");
} }
return result; return result;
+152
View File
@@ -0,0 +1,152 @@
import { makeTestEnv, makeTestFixture, nextTick } from "./helpers";
import { Component, Env } from "../src/component/component";
import { useState, onMounted, onWillUnmount, useRef } from "../src/hooks";
import { xml } from "../src/tags";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
// We create before each test:
// - fixture: a div, appended to the DOM, intended to be the target of dom
// manipulations. Note that it is removed after each test.
// - env: a WEnv, necessary to create new components
let fixture: HTMLElement;
let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
});
afterEach(() => {
fixture.remove();
});
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("hooks", () => {
test("can use a state hook", async () => {
class Counter extends Component<any, any> {
static template = xml`<div><t t-esc="counter.value"/></div>`;
counter = useState({ value: 42 });
}
const counter = new Counter(env);
await counter.mount(fixture);
expect(fixture.innerHTML).toBe("<div>42</div>");
counter.counter.value = 3;
await nextTick();
expect(fixture.innerHTML).toBe("<div>3</div>");
});
test("can use onMounted, onWillUnmount", async () => {
const steps: string[] = [];
function useMyHook() {
onMounted(() => {
steps.push("mounted");
});
onWillUnmount(() => {
steps.push("willunmount");
});
}
class MyComponent extends Component<any, any> {
static template = xml`<div>hey</div>`;
constructor(env) {
super(env);
useMyHook();
}
}
const component = new MyComponent(env);
await component.mount(fixture);
expect(component).not.toHaveProperty("mounted");
expect(component).not.toHaveProperty("willUnmount");
expect(fixture.innerHTML).toBe("<div>hey</div>");
expect(steps).toEqual(["mounted"]);
component.unmount();
expect(fixture.innerHTML).toBe("");
expect(steps).toEqual(["mounted", "willunmount"]);
});
test("mounted, willUnmount, onMounted, onWillUnmount order", async () => {
const steps: string[] = [];
function useMyHook() {
onMounted(() => {
steps.push("hook:mounted");
});
onWillUnmount(() => {
steps.push("hook:willunmount");
});
}
class MyComponent extends Component<any, any> {
static template = xml`<div>hey</div>`;
constructor(env) {
super(env);
useMyHook();
}
mounted() {
steps.push("comp:mounted");
}
willUnmount() {
steps.push("comp:willunmount");
}
}
const component = new MyComponent(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>hey</div>");
component.unmount();
expect(fixture.innerHTML).toBe("");
expect(steps).toEqual(["comp:mounted", "hook:mounted", "hook:willunmount", "comp:willunmount"]);
});
test("two different call to mounted/willunmount should work", async () => {
const steps: string[] = [];
function useMyHook(i) {
onMounted(() => {
steps.push("hook:mounted" + i);
});
onWillUnmount(() => {
steps.push("hook:willunmount" + i);
});
}
class MyComponent extends Component<any, any> {
static template = xml`<div>hey</div>`;
constructor(env) {
super(env);
useMyHook(1);
useMyHook(2);
}
}
const component = new MyComponent(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>hey</div>");
component.unmount();
expect(fixture.innerHTML).toBe("");
expect(steps).toEqual([
"hook:mounted1",
"hook:mounted2",
"hook:willunmount2",
"hook:willunmount1"
]);
});
test("useRef hook", async () => {
class Counter extends Component<any, any> {
static template = xml`<div><button t-ref="button"><t t-esc="value"/></button></div>`;
button = useRef("button");
value = 0;
increment() {
this.value++;
(this.button.el as HTMLButtonElement).innerHTML = String(this.value);
}
}
const counter = new Counter(env);
await counter.mount(fixture);
expect(fixture.innerHTML).toBe("<div><button>0</button></div>");
counter.increment();
await nextTick();
expect(fixture.innerHTML).toBe("<div><button>1</button></div>");
});
});
File diff suppressed because it is too large Load Diff
+148 -3
View File
@@ -11,6 +11,7 @@ import { normalize, renderToDOM, renderToString, trim, nextTick } from "../helpe
let qweb: QWeb; let qweb: QWeb;
beforeEach(() => { beforeEach(() => {
QWeb.TEMPLATES = {};
qweb = new QWeb(); qweb = new QWeb();
}); });
@@ -48,6 +49,11 @@ describe("static templates", () => {
qweb.addTemplate("test", "<div><span>word</span></div>"); qweb.addTemplate("test", "<div><span>word</span></div>");
expect(renderToString(qweb, "test")).toBe("<div><span>word</span></div>"); expect(renderToString(qweb, "test")).toBe("<div><span>word</span></div>");
}); });
test("ignore comments", () => {
qweb.addTemplate("test", "<div>hello <!-- comment-->owl</div>");
expect(renderToString(qweb, "test")).toBe("<div>hello owl</div>");
});
}); });
describe("error handling", () => { describe("error handling", () => {
@@ -573,6 +579,103 @@ describe("t-call (template calling", () => {
const expected = "<div>ok</div>"; const expected = "<div>ok</div>";
expect(trim(renderToString(qweb, "caller"))).toBe(expected); expect(trim(renderToString(qweb, "caller"))).toBe(expected);
}); });
test("call with several sub nodes on same line", () => {
qweb.addTemplates(`
<templates>
<div t-name="SubTemplate">
<t t-raw="0"/>
</div>
<div t-name="main">
<t t-call="SubTemplate">
<span>hey</span> <span>yay</span>
</t>
</div>
</templates>
`);
const expected = "<div><div><span>hey</span> <span>yay</span></div></div>";
expect(renderToString(qweb, "main")).toBe(expected);
});
test("recursive template, part 1", () => {
qweb.addTemplates(`
<templates>
<div t-name="recursive">
<span>hey</span>
<t t-if="false">
<t t-call="recursive"/>
</t>
</div>
</templates>
`);
const expected = "<div><span>hey</span></div>";
expect(renderToString(qweb, "recursive")).toBe(expected);
const recursiveFn = Object.values(qweb.recursiveFns)[0];
expect(recursiveFn.toString()).toMatchSnapshot();
});
test("recursive template, part 2", () => {
qweb.addTemplates(`
<templates>
<div t-name="Parent">
<t t-call="nodeTemplate">
<t t-set="node" t-value="root"/>
</t>
</div>
<div t-name="nodeTemplate">
<p><t t-esc="node.val"/></p>
<t t-foreach="node.children or []" t-as="subtree">
<t t-call="nodeTemplate">
<t t-set="node" t-value="subtree"/>
</t>
</t>
</div>
</templates>
`);
const root = { val: "a", children: [{ val: "b" }, { val: "c" }] };
const expected = "<div><div><p>a</p><div><p>b</p></div><div><p>c</p></div></div></div>";
expect(renderToString(qweb, "Parent", { root }, { fiber: { vars: {}, scope: {} } })).toBe(
expected
);
const recursiveFn = Object.values(qweb.recursiveFns)[0];
expect(recursiveFn.toString()).toMatchSnapshot();
});
test("recursive template, part 3", () => {
qweb.addTemplates(`
<templates>
<div t-name="Parent">
<t t-call="nodeTemplate">
<t t-set="node" t-value="root"/>
</t>
</div>
<div t-name="nodeTemplate">
<p><t t-esc="node.val"/></p>
<t t-foreach="node.children or []" t-as="subtree">
<t t-call="nodeTemplate">
<t t-set="node" t-value="subtree"/>
</t>
</t>
</div>
</templates>
`);
const root = { val: "a", children: [{ val: "b", children: [{ val: "d" }] }, { val: "c" }] };
const expected =
"<div><div><p>a</p><div><p>b</p><div><p>d</p></div></div><div><p>c</p></div></div></div>";
expect(renderToString(qweb, "Parent", { root }, { fiber: {} })).toBe(expected);
const recursiveFn = Object.values(qweb.recursiveFns)[0];
expect(recursiveFn.toString()).toMatchSnapshot();
});
test("t-call, global templates", () => {
QWeb.registerTemplate("abcd", '<div><t t-call="john"/></div>');
qweb.addTemplate("john", `<span>desk</span>`);
const expected = "<div><span>desk</span></div>";
expect(trim(renderToString(qweb, "abcd"))).toBe(expected);
});
}); });
describe("foreach", () => { describe("foreach", () => {
@@ -989,14 +1092,14 @@ describe("t-ref", () => {
test("can get a ref on a node", () => { test("can get a ref on a node", () => {
qweb.addTemplate("test", `<div><span t-ref="myspan"/></div>`); qweb.addTemplate("test", `<div><span t-ref="myspan"/></div>`);
let refs: any = {}; let refs: any = {};
renderToDOM(qweb, "test", { refs }); renderToDOM(qweb, "test", { __owl__: { refs } });
expect(refs.myspan.tagName).toBe("SPAN"); expect(refs.myspan.tagName).toBe("SPAN");
}); });
test("can get a dynamic ref on a node", () => { test("can get a dynamic ref on a node", () => {
qweb.addTemplate("test", `<div><span t-ref="myspan{{id}}"/></div>`); qweb.addTemplate("test", `<div><span t-ref="myspan{{id}}"/></div>`);
let refs: any = {}; let refs: any = {};
renderToDOM(qweb, "test", { refs, id: 3 }); renderToDOM(qweb, "test", { id: 3, __owl__: { refs } });
expect(refs.myspan3.tagName).toBe("SPAN"); expect(refs.myspan3.tagName).toBe("SPAN");
}); });
@@ -1011,7 +1114,7 @@ describe("t-ref", () => {
</div>` </div>`
); );
let refs: any = {}; let refs: any = {};
renderToDOM(qweb, "test", { refs, items: [1, 2, 3] }); renderToDOM(qweb, "test", { items: [1, 2, 3], __owl__: { refs } });
expect(Object.keys(refs)).toEqual(["1", "2", "3"]); expect(Object.keys(refs)).toEqual(["1", "2", "3"]);
}); });
}); });
@@ -1190,3 +1293,45 @@ describe("update on event bus", () => {
expect(fn).toBeCalledTimes(1); expect(fn).toBeCalledTimes(1);
}); });
}); });
describe("global template registration", () => {
test("can register template globally", () => {
expect.assertions(5);
let qweb = new QWeb();
try {
qweb.render("mytemplate");
} catch (e) {
expect(e.message).toMatch("Template mytemplate does not exist");
}
expect(qweb.templates.mytemplate).toBeUndefined();
QWeb.registerTemplate("mytemplate", "<div>global</div>");
expect(qweb.templates.mytemplate).toBeDefined();
const vnode = qweb.render("mytemplate");
expect(vnode.sel).toBe("div");
expect((vnode as any).children[0].text).toBe("global");
});
});
describe("properly support svg", () => {
test("add proper namespace to svg", () => {
qweb.addTemplate(
"test",
`<svg width="100px" height="90px"><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/> </svg>`
);
expect(renderToString(qweb, "test")).toBe(
`<svg width=\"100px\" height=\"90px\"><circle cx=\"50\" cy=\"50\" r=\"4\" stroke=\"green\" stroke-width=\"1\" fill=\"yellow\"></circle> </svg>`
);
});
test("add proper namespace to g tags", () => {
// this is necessary if one wants to use components in a svg
qweb.addTemplate(
"test",
`<g><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/> </g>`
);
expect(renderToString(qweb, "test")).toBe(
`<g><circle cx=\"50\" cy=\"50\" r=\"4\" stroke=\"green\" stroke-width=\"1\" fill=\"yellow\"></circle> </g>`
);
});
});
+6 -6
View File
@@ -1,5 +1,5 @@
import { Component } from "../../src/component/component"; import { Component } from "../../src/component/component";
import { Link, LINK_TEMPLATE_NAME } from "../../src/router/Link"; import { Link } from "../../src/router/Link";
import { RouterEnv } from "../../src/router/Router"; import { RouterEnv } from "../../src/router/Router";
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers"; import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
import { TestRouter } from "./TestRouter"; import { TestRouter } from "./TestRouter";
@@ -30,8 +30,8 @@ describe("Link component", () => {
</div> </div>
</templates> </templates>
`); `);
class App extends Component<any, any, any> { class App extends Component<any, any> {
components = { Link: Link }; static components = { Link: Link };
} }
const routes = [{ name: "about", path: "/about" }, { name: "users", path: "/users" }]; const routes = [{ name: "about", path: "/about" }, { name: "users", path: "/users" }];
@@ -50,7 +50,7 @@ describe("Link component", () => {
'<div><a href="/about" class="router-link-active">About</a></div>' '<div><a href="/about" class="router-link-active">About</a></div>'
); );
expect(env.qweb.templates[LINK_TEMPLATE_NAME].fn.toString()).toMatchSnapshot(); expect(env.qweb.templates[Link.template].fn.toString()).toMatchSnapshot();
}); });
test("do not redirect if right clicking", async () => { test("do not redirect if right clicking", async () => {
@@ -61,8 +61,8 @@ describe("Link component", () => {
</div> </div>
</templates> </templates>
`); `);
class App extends Component<any, any, any> { class App extends Component<any, any> {
components = { Link: Link }; static components = { Link: Link };
} }
const routes = [{ name: "about", path: "/about" }, { name: "users", path: "/users" }]; const routes = [{ name: "about", path: "/about" }, { name: "users", path: "/users" }];
@@ -1,9 +1,10 @@
import { Component } from "../../src/component/component"; import { Component } from "../../src/component/component";
import { RouterEnv } from "../../src/router/Router"; import { RouterEnv } from "../../src/router/Router";
import { RouteComponent } from "../../src/router/RouteComponent";
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers"; import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
import { TestRouter } from "./TestRouter"; import { TestRouter } from "./TestRouter";
describe("router directive t-routecomponent", () => { describe("RouteComponent", () => {
let fixture: HTMLElement; let fixture: HTMLElement;
let env: RouterEnv; let env: RouterEnv;
let router: TestRouter | null = null; let router: TestRouter | null = null;
@@ -25,16 +26,16 @@ describe("router directive t-routecomponent", () => {
env.qweb.addTemplates(` env.qweb.addTemplates(`
<templates> <templates>
<div t-name="App"> <div t-name="App">
<t t-routecomponent="1" /> <RouteComponent />
</div> </div>
<span t-name="About">About</span> <span t-name="About">About</span>
<span t-name="Users">Users</span> <span t-name="Users">Users</span>
</templates> </templates>
`); `);
class About extends Component<any, any, any> {} class About extends Component<any, any> {}
class Users extends Component<any, any, any> {} class Users extends Component<any, any> {}
class App extends Component<any, any, any> { class App extends Component<any, any> {
components = { About, Users }; static components = { RouteComponent };
} }
const routes = [ const routes = [
@@ -51,21 +52,21 @@ describe("router directive t-routecomponent", () => {
await router.navigate({ to: "users" }); await router.navigate({ to: "users" });
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe("<div><span>Users</span></div>"); expect(fixture.innerHTML).toBe("<div><span>Users</span></div>");
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot(); expect(env.qweb.templates[RouteComponent.template].fn.toString()).toMatchSnapshot();
}); });
test("can render parameterized route", async () => { test("can render parameterized route", async () => {
env.qweb.addTemplates(` env.qweb.addTemplates(`
<templates> <templates>
<div t-name="App"> <div t-name="App">
<t t-routecomponent="1" /> <RouteComponent />
</div> </div>
<span t-name="Book">Book <t t-esc="props.title"/></span> <span t-name="Book">Book <t t-esc="props.title"/></span>
</templates> </templates>
`); `);
class Book extends Component<any, any, any> {} class Book extends Component<any, any> {}
class App extends Component<any, any, any> { class App extends Component<any, any> {
components = { Book }; static components = { RouteComponent };
} }
const routes = [{ name: "book", path: "/book/{{title}}", component: Book }]; const routes = [{ name: "book", path: "/book/{{title}}", component: Book }];
@@ -74,25 +75,24 @@ describe("router directive t-routecomponent", () => {
const app = new App(env); const app = new App(env);
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>Book 1984</span></div>"); expect(fixture.innerHTML).toBe("<div><span>Book 1984</span></div>");
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
}); });
test("can render parameterized route with suffixes", async () => { test("can render parameterized route with suffixes", async () => {
env.qweb.addTemplates(` env.qweb.addTemplates(`
<templates> <templates>
<div t-name="App"> <div t-name="App">
<t t-routecomponent="1" /> <RouteComponent />
</div> </div>
<span t-name="Book">Book <t t-esc="props.title"/>|<t t-esc="incVal"/></span> <span t-name="Book">Book <t t-esc="props.title"/>|<t t-esc="incVal"/></span>
</templates> </templates>
`); `);
class Book extends Component<any, any, any> { class Book extends Component<any, any> {
get incVal() { get incVal() {
return this.props.val + 1; return this.props.val + 1;
} }
} }
class App extends Component<any, any, any> { class App extends Component<any, any> {
components = { Book }; static components = { RouteComponent };
} }
const routes = [{ name: "book", path: "/book/{{title}}/{{val.number}}", component: Book }]; const routes = [{ name: "book", path: "/book/{{title}}/{{val.number}}", component: Book }];
@@ -101,6 +101,5 @@ describe("router directive t-routecomponent", () => {
const app = new App(env); const app = new App(env);
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>Book 1984|124</span></div>"); expect(fixture.innerHTML).toBe("<div><span>Book 1984|124</span></div>");
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
}); });
}); });
-2
View File
@@ -4,8 +4,6 @@ import { QWeb } from "../../src/qweb/index";
export class TestRouter extends Router { export class TestRouter extends Router {
destroy() { destroy() {
window.removeEventListener("popstate", (this as any)._listener); window.removeEventListener("popstate", (this as any)._listener);
delete QWeb.DIRECTIVE_NAMES.routecomponent;
QWeb.DIRECTIVES = QWeb.DIRECTIVES.filter(d => d.name !== "routecomponent");
// remove component defined inroutes // remove component defined inroutes
for (let key in QWeb.components) { for (let key in QWeb.components) {
+4 -3
View File
@@ -10,14 +10,15 @@ exports[`Link component can render simple cases 1`] = `
var _2 = context['href']; var _2 = context['href'];
let c3 = [], p3 = {key:3,attrs:{href: _2},class:_1,on:{}}; let c3 = [], p3 = {key:3,attrs:{href: _2},class:_1,on:{}};
var vn3 = h('a', p3, c3); var vn3 = h('a', p3, c3);
result = vn3;
if (!context['navigate']) { if (!context['navigate']) {
throw new Error('Missing handler \\\\'' + 'navigate' + \`\\\\' when evaluating template '__owl__-router-link'\`) throw new Error('Missing handler \\\\'' + 'navigate' + \`\\\\' when evaluating template '__template__1'\`)
} }
extra.handlers['click' + 3] = extra.handlers['click' + 3] || context['navigate'].bind(owner); extra.handlers['click' + 3] = extra.handlers['click' + 3] || context['navigate'].bind(owner);
p3.on['click'] = extra.handlers['click' + 3]; p3.on['click'] = extra.handlers['click' + 3];
const slot4 = this.slots[context.__owl__.slotId + '_' + 'default']; const slot4 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot4) { if (slot4) {
slot4(context.__owl__.parent, Object.assign({}, extra, {parentNode: c3, vars: extra.vars, parent: owner})); slot4.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c3, vars: extra.vars, parent: owner}));
} }
return vn3; return vn3;
}" }"
@@ -0,0 +1,45 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RouteComponent can render simple cases 1`] = `
"function anonymous(context,extra
) {
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
let result;
var h = this.h;
if (context['routeComponent']) {
//COMPONENT
let key4 = 'key' + context['env'].router.currentRouteName;
let def2;
let templateId5 = key4;
let w3 = templateId5 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId5]] : false;
let vn6 = {};
result = vn6;
let props3 = Object.assign({}, context['env'].router.currentParams);
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
if (utils.shallowEqual(props3, w3.__owl__.currentFiber.props)) {
def2 = w3.__owl__.currentFiber.promise;
} else {
w3.destroy();
w3 = false;
}
}
if (!w3) {
let componentKey3 = \`routeComponent\`;
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['routeComponent'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3);
parent.__owl__.cmap[templateId5] = w3.__owl__.id;
def2 = w3.__prepare(extra.fiber, undefined, undefined);
def2 = def2.then(vnode=>{if (w3.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: templateId5, hook: {insert(vn) {let nvn=w3.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}});utils.defineProxy(vn6, pvnode);w3.__owl__.pvnode = pvnode;});
} else {
def2 = def2 || w3.__updateProps(props3, extra.fiber, undefined, undefined);
def2 = def2.then(()=>{if (w3.__owl__.isDestroyed) {return};let pvnode=w3.__owl__.pvnode;utils.defineProxy(vn6, pvnode);});
}
extra.promises.push(def2);
}
return result;
}"
`;
@@ -1,159 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`router directive t-routecomponent can render parameterized route 1`] = `
"function anonymous(context,extra
) {
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['env'].router.currentRouteName==='book') {
//COMPONENT
let def3;
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {title:context['env'].router.currentParams.title};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false;
}
}
if (!w4) {
let componentKey4 = \`__component__book\`;
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare();
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
}
return vn1;
}"
`;
exports[`router directive t-routecomponent can render parameterized route with suffixes 1`] = `
"function anonymous(context,extra
) {
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['env'].router.currentRouteName==='book') {
//COMPONENT
let def3;
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {title:context['env'].router.currentParams.title,val:context['env'].router.currentParams.val};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false;
}
}
if (!w4) {
let componentKey4 = \`__component__book\`;
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare();
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
}
return vn1;
}"
`;
exports[`router directive t-routecomponent can render simple cases 1`] = `
"function anonymous(context,extra
) {
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let owner = context;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
if (context['env'].router.currentRouteName==='about') {
//COMPONENT
let def3;
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
} else {
w4.destroy();
w4 = false;
}
}
if (!w4) {
let componentKey4 = \`__component__about\`;
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare();
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
}
else if (context['env'].router.currentRouteName==='users') {
//COMPONENT
let def6;
let w7 = 7 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[7]] : false;
let _5_index = c1.length;
c1.push(null);
let props7 = {};
if (w7 && w7.__owl__.renderPromise && !w7.__owl__.vnode) {
if (utils.shallowEqual(props7, w7.__owl__.renderProps)) {
def6 = w7.__owl__.renderPromise;
} else {
w7.destroy();
w7 = false;
}
}
if (!w7) {
let componentKey7 = \`__component__users\`;
let W7 = context.components && context.components[componentKey7] || QWeb.components[componentKey7];
if (!W7) {throw new Error('Cannot find the definition of component \\"' + componentKey7 + '\\"')}
w7 = new W7(parent, props7);
parent.__owl__.cmap[7] = w7.__owl__.id;
def6 = w7.__prepare();
def6 = def6.then(vnode=>{let pvnode=h(vnode.sel, {key: 7, hook: {insert(vn) {let nvn=w7.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w7.destroy();}}});c1[_5_index]=pvnode;w7.__owl__.pvnode = pvnode;});
} else {
def6 = def6 || w7.__updateProps(props7, extra.forceUpdate, extra.patchQueue);
def6 = def6.then(()=>{if (w7.__owl__.isDestroyed) {return};let pvnode=w7.__owl__.pvnode;c1[_5_index]=pvnode;});
}
extra.promises.push(def6);
}
return vn1;
}"
`;
+94 -16
View File
@@ -1,5 +1,5 @@
import { Destination, Router, RouterEnv, Route } from "../../src/router/Router"; import { Destination, RouterEnv, Route } from "../../src/router/Router";
import { makeTestEnv } from "../helpers"; import { makeTestEnv, nextTick } from "../helpers";
import { TestRouter } from "./TestRouter"; import { TestRouter } from "./TestRouter";
let env: RouterEnv; let env: RouterEnv;
@@ -25,18 +25,58 @@ describe("router miscellaneous", () => {
]); ]);
}).toThrow(`Invalid destination: {"abc":"hey"}`); }).toThrow(`Invalid destination: {"abc":"hey"}`);
}); });
test("navigate to same route but with different params should trigger update", async () => {
router = new TestRouter(env, [{ name: "users", path: "/users/{{id}}" }]);
env.qweb.forceUpdate = jest.fn();
await router.navigate({ to: "users", params: { id: 3 } });
expect(window.location.pathname).toBe("/users/3");
expect(env.qweb.forceUpdate).toHaveBeenCalledTimes(1);
await router.navigate({ to: "users", params: { id: 5 } });
expect(window.location.pathname).toBe("/users/5");
expect(env.qweb.forceUpdate).toHaveBeenCalledTimes(2);
});
test("changing url to same route but with different params should trigger update (hash mode)", async () => {
env.qweb.forceUpdate = jest.fn();
router = new TestRouter(env, [{ name: "users", path: "/users/{{id}}" }], { mode: "hash" });
await router.start();
await router.navigate({ to: "users", params: { id: 3 } });
expect(window.location.hash).toBe("#/users/3");
expect(env.qweb.forceUpdate).toHaveBeenCalledTimes(1);
window.location.hash = "/users/5";
window.dispatchEvent(new Event("hashchange"));
await nextTick();
expect(window.location.hash).toBe("#/users/5");
expect(env.qweb.forceUpdate).toHaveBeenCalledTimes(2);
});
}); });
describe("routeToPath", () => { describe("routeToPath", () => {
const routeToPath = Router.prototype["routeToPath"];
test("simple non parameterized path", () => { test("simple non parameterized path", () => {
expect(routeToPath({path: "/abc"} as Route, {})).toBe("/abc"); router = new TestRouter(env, []);
expect(routeToPath({path: "/abc/def"} as Route, {})).toBe("/abc/def"); expect(router["routeToPath"]({ path: "/abc" } as Route, {})).toBe("/abc");
expect(routeToPath({path: "/abc"} as Route, { val: 12 })).toBe("/abc"); expect(router["routeToPath"]({ path: "/abc/def" } as Route, {})).toBe("/abc/def");
expect(router["routeToPath"]({ path: "/abc" } as Route, { val: 12 })).toBe("/abc");
}); });
test("simple parameterized path", () => { test("simple parameterized path", () => {
expect(routeToPath({path: "/abc/{{def}}"} as Route, { def: 34 })).toBe("/abc/34"); router = new TestRouter(env, []);
expect(router["routeToPath"]({ path: "/abc/{{def}}" } as Route, { def: 34 })).toBe("/abc/34");
});
test("simple non parameterized path, mode = hash", () => {
router = new TestRouter(env, [], { mode: "hash" });
expect(router["routeToPath"]({ path: "/abc" } as Route, {})).toBe("#/abc");
expect(router["routeToPath"]({ path: "/abc/def" } as Route, {})).toBe("#/abc/def");
expect(router["routeToPath"]({ path: "/abc" } as Route, { val: 12 })).toBe("#/abc");
});
test("simple parameterized path, mode=hash", () => {
router = new TestRouter(env, [], { mode: "hash" });
expect(router["routeToPath"]({ path: "/abc/{{def}}" } as Route, { def: 34 })).toBe("#/abc/34");
}); });
}); });
@@ -58,26 +98,60 @@ describe("destToPath", () => {
}); });
describe("getRouteParams", () => { describe("getRouteParams", () => {
const getRouteParams = Router.prototype["getRouteParams"];
test("properly match simple routes", () => { test("properly match simple routes", () => {
router = new TestRouter(env, []);
// simple route // simple route
expect(getRouteParams({path: "/home"} as Route, "/home")).toEqual({}); expect(router["getRouteParams"]({ path: "/home" } as Route, "/home")).toEqual({});
// no match // no match
expect(getRouteParams({path: "/home"} as Route, "/otherpath")).toEqual(false); expect(router["getRouteParams"]({ path: "/home" } as Route, "/otherpath")).toEqual(false);
// fallback route // fallback route
expect(getRouteParams({path: "*"} as Route, "somepath")).toEqual({}); expect(router["getRouteParams"]({ path: "*" } as Route, "somepath")).toEqual({});
});
test("properly match simple routes, mode hash", () => {
router = new TestRouter(env, [], { mode: "hash" });
// simple route
expect(router["getRouteParams"]({ path: "/home" } as Route, "#/home")).toEqual({});
// no match
expect(router["getRouteParams"]({ path: "/home" } as Route, "#/otherpath")).toEqual(false);
// fallback route
expect(router["getRouteParams"]({ path: "*" } as Route, "#/somepath")).toEqual({});
}); });
test("match some parameterized routes", () => { test("match some parameterized routes", () => {
expect(getRouteParams({path: "/invoices/{{id}}"} as Route, "/invoices/3")).toEqual({ router = new TestRouter(env, []);
expect(router["getRouteParams"]({ path: "/invoices/{{id}}" } as Route, "/invoices/3")).toEqual({
id: "3" id: "3"
}); });
}); });
test("match some parameterized routes, mode hash", () => {
router = new TestRouter(env, [], { mode: "hash" });
expect(router["getRouteParams"]({ path: "/invoices/{{id}}" } as Route, "#/invoices/3")).toEqual(
{
id: "3"
}
);
});
test("can convert to number if needed", () => { test("can convert to number if needed", () => {
expect(getRouteParams({path: "/invoices/{{id.number}}"} as Route, "/invoices/3")).toEqual({ router = new TestRouter(env, []);
expect(
router["getRouteParams"]({ path: "/invoices/{{id.number}}" } as Route, "/invoices/3")
).toEqual({
id: 3
});
});
test("can convert to number if needed, mode: hash", () => {
router = new TestRouter(env, [], { mode: "hash" });
expect(
router["getRouteParams"]({ path: "/invoices/{{id.number}}" } as Route, "#/invoices/3")
).toEqual({
id: 3 id: 3
}); });
}); });
@@ -147,11 +221,13 @@ describe("beforeRouteEnter", () => {
test("navigation is redirected if guard decides so", async () => { test("navigation is redirected if guard decides so", async () => {
expect(window.location.pathname).toBe("/"); expect(window.location.pathname).toBe("/");
const guard = jest.fn(() => {return {to: "routec"}}); const guard = jest.fn(() => {
return { to: "routec" };
});
router = new TestRouter(env, [ router = new TestRouter(env, [
{ name: "routea", path: "/some/patha" }, { name: "routea", path: "/some/patha" },
{ name: "routeb", path: "/some/pathb", beforeRouteEnter: guard }, { name: "routeb", path: "/some/pathb", beforeRouteEnter: guard },
{ name: "routec", path: "/some/pathc"}, { name: "routec", path: "/some/pathc" }
]); ]);
await router.start(); await router.start();
@@ -166,7 +242,9 @@ describe("beforeRouteEnter", () => {
test("navigation is initially redirected if guard decides so", async () => { test("navigation is initially redirected if guard decides so", async () => {
expect(window.location.pathname).toBe("/"); expect(window.location.pathname).toBe("/");
const guard = jest.fn(() => {return {to: "otherroute"}}); const guard = jest.fn(() => {
return { to: "otherroute" };
});
router = new TestRouter(env, [ router = new TestRouter(env, [
{ name: "landing", path: "/", beforeRouteEnter: guard }, { name: "landing", path: "/", beforeRouteEnter: guard },
{ name: "otherroute", path: "/some/other/route" } { name: "otherroute", path: "/some/other/route" }
@@ -15,3 +15,7 @@ exports[`connecting a component to store deep and shallow connecting a component
exports[`connecting a component to store deep and shallow connecting a component 3`] = `"<div><span>Bertinchamps</span></div>"`; exports[`connecting a component to store deep and shallow connecting a component 3`] = `"<div><span>Bertinchamps</span></div>"`;
exports[`connecting a component to store deep and shallow connecting a component 4`] = `"<div><span>Kasteel</span></div>"`; exports[`connecting a component to store deep and shallow connecting a component 4`] = `"<div><span>Kasteel</span></div>"`;
exports[`various scenarios scenarios with async store updates and some components events 1`] = `"<div><button>Do stuff</button><div><span>Attachment 100</span><span>Name: text.txt</span></div></div>"`;
exports[`various scenarios scenarios with async store updates and some components events 2`] = `"<div><button>Do stuff</button></div>"`;
+177 -95
View File
@@ -1,6 +1,7 @@
import { Component, Env } from "../../src/component/component"; import { Component, Env } from "../../src/component/component";
import { ConnectedComponent } from "../../src/store/connected_component"; import { ConnectedComponent } from "../../src/store/connected_component";
import { Store } from "../../src/store/store"; import { Store } from "../../src/store/store";
import { useState } from "../../src/hooks";
import { makeTestEnv, makeTestFixture, nextTick, makeDeferred } from "../helpers"; import { makeTestEnv, makeTestFixture, nextTick, makeDeferred } from "../helpers";
describe("connecting a component to store", () => { describe("connecting a component to store", () => {
@@ -27,13 +28,13 @@ describe("connecting a component to store", () => {
<span t-name="Todo"><t t-esc="props.msg"/></span> <span t-name="Todo"><t t-esc="props.msg"/></span>
</templates> </templates>
`); `);
class App extends ConnectedComponent<any, any, any> { class Todo extends Component<any, any> {}
components = { Todo }; class App extends ConnectedComponent<any, any> {
static components = { Todo };
static mapStoreToProps(s) { static mapStoreToProps(s) {
return { todos: s.todos }; return { todos: s.todos };
} }
} }
class Todo extends Component<any, any, any> {}
const state = { todos: [] }; const state = { todos: [] };
const actions = { const actions = {
addTodo({ state }, msg) { addTodo({ state }, msg) {
@@ -70,7 +71,7 @@ describe("connecting a component to store", () => {
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
class App extends ConnectedComponent<any, any, any> { class App extends ConnectedComponent<any, any> {
static mapStoreToProps(s) { static mapStoreToProps(s) {
return { todos: s.todos }; return { todos: s.todos };
} }
@@ -111,7 +112,7 @@ describe("connecting a component to store", () => {
<span t-name="Todo"><t t-esc="props.msg"/></span> <span t-name="Todo"><t t-esc="props.msg"/></span>
</templates> </templates>
`); `);
class Todo extends Component<any, any, any> {} class Todo extends Component<any, any> {}
const store = new Store({ const store = new Store({
state: { todos: [] }, state: { todos: [] },
@@ -121,8 +122,8 @@ describe("connecting a component to store", () => {
} }
} }
}); });
class App extends ConnectedComponent<any, any, any> { class App extends ConnectedComponent<any, any> {
components = { Todo }; static components = { Todo };
static mapStoreToProps(s) { static mapStoreToProps(s) {
return { todos: s.todos }; return { todos: s.todos };
} }
@@ -160,7 +161,7 @@ describe("connecting a component to store", () => {
}); });
(<any>env).store = store; (<any>env).store = store;
class App extends ConnectedComponent<any, any, any> { class App extends ConnectedComponent<any, any> {
static mapStoreToProps(s) { static mapStoreToProps(s) {
return { value: s.value }; return { value: s.value };
} }
@@ -187,7 +188,7 @@ describe("connecting a component to store", () => {
<div t-name="Child"/> <div t-name="Child"/>
</templates> </templates>
`); `);
class Child extends ConnectedComponent<any, any, any> { class Child extends ConnectedComponent<any, any> {
static mapStoreToProps(s) { static mapStoreToProps(s) {
return s; return s;
} }
@@ -199,12 +200,12 @@ describe("connecting a component to store", () => {
} }
} }
class Parent extends Component<any, any, any> { class Parent extends Component<any, any> {
components = { Child }; static components = { Child };
state: any;
constructor(env: Env) { constructor(env: Env) {
super(env); super(env);
this.state = { child: true }; this.state = useState({ child: true });
} }
} }
@@ -240,15 +241,15 @@ describe("connecting a component to store", () => {
</div> </div>
</templates> </templates>
`); `);
class TodoItem extends ConnectedComponent<any, any, any> { class TodoItem extends ConnectedComponent<any, any> {
static mapStoreToProps(state, props) { static mapStoreToProps(state, props) {
const todo = state.todos.find(t => t.id === props.id); const todo = state.todos.find(t => t.id === props.id);
return todo; return todo;
} }
} }
class TodoList extends ConnectedComponent<any, any, any> { class TodoList extends ConnectedComponent<any, any> {
components = { TodoItem }; static components = { TodoItem };
static mapStoreToProps(state) { static mapStoreToProps(state) {
return { todos: state.todos }; return { todos: state.todos };
} }
@@ -294,7 +295,7 @@ describe("connecting a component to store", () => {
</templates> </templates>
`); `);
class TodoItem extends ConnectedComponent<any, any, any> { class TodoItem extends ConnectedComponent<any, any> {
static mapStoreToProps(state, props, getters) { static mapStoreToProps(state, props, getters) {
const todo = state.todos.find(t => t.id === props.id); const todo = state.todos.find(t => t.id === props.id);
return { return {
@@ -304,8 +305,8 @@ describe("connecting a component to store", () => {
} }
} }
class TodoList extends ConnectedComponent<any, any, any> { class TodoList extends ConnectedComponent<any, any> {
components = { TodoItem }; static components = { TodoItem };
static mapStoreToProps(state) { static mapStoreToProps(state) {
return { todos: state.todos }; return { todos: state.todos };
} }
@@ -330,15 +331,15 @@ describe("connecting a component to store", () => {
</templates> </templates>
`); `);
class Beer extends ConnectedComponent<any, any, any> { class Beer extends ConnectedComponent<any, any> {
static mapStoreToProps(state, props) { static mapStoreToProps(state, props) {
return state.beers[props.id]; return state.beers[props.id];
} }
} }
class App extends Component<any, any, any> { class App extends Component<any, any> {
components = { Beer }; static components = { Beer };
state = { beerId: 1 }; state = useState({ beerId: 1 });
} }
const state = { beers: { 1: { name: "jupiler" }, 2: { name: "kwak" } } }; const state = { beers: { 1: { name: "jupiler" }, 2: { name: "kwak" } } };
@@ -363,7 +364,7 @@ describe("connecting a component to store", () => {
</templates> </templates>
`); `);
class App extends ConnectedComponent<any, any, any> { class App extends ConnectedComponent<any, any> {
static mapStoreToProps(state) { static mapStoreToProps(state) {
return { beers: state.beers, otherKey: 1 }; return { beers: state.beers, otherKey: 1 };
} }
@@ -403,7 +404,7 @@ describe("connecting a component to store", () => {
</templates> </templates>
`); `);
class Beer extends ConnectedComponent<any, any, any> { class Beer extends ConnectedComponent<any, any> {
static mapStoreToProps(state, props) { static mapStoreToProps(state, props) {
return { return {
selected: state.beers[props.id], selected: state.beers[props.id],
@@ -413,9 +414,9 @@ describe("connecting a component to store", () => {
} }
} }
class App extends Component<any, any, any> { class App extends Component<any, any> {
components = { Beer }; static components = { Beer };
state = { beerId: 0 }; state = useState({ beerId: 0 });
} }
const actions = { const actions = {
@@ -470,7 +471,7 @@ describe("connecting a component to store", () => {
</templates> </templates>
`); `);
class Beer extends ConnectedComponent<any, any, any> { class Beer extends ConnectedComponent<any, any> {
static mapStoreToProps(storeState, props) { static mapStoreToProps(storeState, props) {
return { return {
selected: storeState.beers[props.id], selected: storeState.beers[props.id],
@@ -480,9 +481,9 @@ describe("connecting a component to store", () => {
} }
} }
class App extends Component<any, any, any> { class App extends Component<any, any> {
components = { Beer }; static components = { Beer };
state = { beerId: 0 }; state = useState({ beerId: 0 });
} }
const actions = { const actions = {
@@ -559,20 +560,19 @@ describe("connecting a component to store", () => {
</templates> </templates>
`); `);
class Parent extends ConnectedComponent<any, any, any> { class Child extends ConnectedComponent<any, any> {
components = { Child };
static mapStoreToProps(s) {
steps.push("parent");
return { current: s.current, isvisible: s.isvisible };
}
}
class Child extends ConnectedComponent<any, any, any> {
static mapStoreToProps(s, props) { static mapStoreToProps(s, props) {
steps.push("child"); steps.push("child");
return { msg: s.msg[props.key] }; return { msg: s.msg[props.key] };
} }
} }
class Parent extends ConnectedComponent<any, any> {
static components = { Child };
static mapStoreToProps(s) {
steps.push("parent");
return { current: s.current, isvisible: s.isvisible };
}
}
const state = { current: "a", msg: { a: "a", b: "b" } }; const state = { current: "a", msg: { a: "a", b: "b" } };
const actions = { const actions = {
@@ -609,8 +609,14 @@ describe("connecting a component to store", () => {
</templates> </templates>
`); `);
class Parent extends ConnectedComponent<any, any, any> { class Child extends ConnectedComponent<any, any> {
components = { Child }; static mapStoreToProps(s, props) {
steps.push("child");
return { msg: s.messages[props.someId] };
}
}
class Parent extends ConnectedComponent<any, any> {
static components = { Child };
static mapStoreToProps(s) { static mapStoreToProps(s) {
steps.push("parent"); steps.push("parent");
return { flag: s.flag, someId: s.someId }; return { flag: s.flag, someId: s.someId };
@@ -621,13 +627,6 @@ describe("connecting a component to store", () => {
} }
} }
class Child extends ConnectedComponent<any, any, any> {
static mapStoreToProps(s, props) {
steps.push("child");
return { msg: s.messages[props.someId] };
}
}
const state = { someId: 1, flag: true, messages: { 1: "abc" } }; const state = { someId: 1, flag: true, messages: { 1: "abc" } };
const actions = { const actions = {
setFlagToFalse({ state }) { setFlagToFalse({ state }) {
@@ -685,19 +684,10 @@ describe("connecting a component to store", () => {
</templates> </templates>
`); `);
class TodoApp extends ConnectedComponent<any, any, any> {
components = { TodoItem };
static mapStoreToProps(state) {
return {
todos: state.todos
};
}
}
let renderCount = 0; let renderCount = 0;
let fCount = 0; let fCount = 0;
class TodoItem extends ConnectedComponent<any, any, any> { class TodoItem extends ConnectedComponent<any, any> {
state = { isEditing: false }; state = { isEditing: false };
static mapStoreToProps(state, ownProps) { static mapStoreToProps(state, ownProps) {
fCount++; fCount++;
@@ -709,9 +699,17 @@ describe("connecting a component to store", () => {
editTodo() { editTodo() {
this.env.store.dispatch("editTodo"); this.env.store.dispatch("editTodo");
} }
__render(...args) { __render(f) {
renderCount++; renderCount++;
return super.__render(...args); return super.__render(f);
}
}
class TodoApp extends ConnectedComponent<any, any> {
static components = { TodoItem };
static mapStoreToProps(state) {
return {
todos: state.todos
};
} }
} }
@@ -764,19 +762,10 @@ describe("connecting a component to store", () => {
</templates> </templates>
`); `);
class TodoApp extends ConnectedComponent<any, any, any> {
components = { TodoItem };
static mapStoreToProps(state) {
return {
todos: state.todos
};
}
}
let renderCount = 0; let renderCount = 0;
let fCount = 0; let fCount = 0;
class TodoItem extends ConnectedComponent<any, any, any> { class TodoItem extends ConnectedComponent<any, any> {
state = { isEditing: false }; state = { isEditing: false };
static mapStoreToProps(state, ownProps) { static mapStoreToProps(state, ownProps) {
@@ -788,9 +777,18 @@ describe("connecting a component to store", () => {
removeTodo() { removeTodo() {
this.env.store.dispatch("removeTodo"); this.env.store.dispatch("removeTodo");
} }
__render(...args) { __render(f) {
renderCount++; renderCount++;
return super.__render(...args); return super.__render(f);
}
}
class TodoApp extends ConnectedComponent<any, any> {
static components = { TodoItem };
static mapStoreToProps(state) {
return {
todos: state.todos
};
} }
} }
@@ -820,7 +818,7 @@ describe("connecting a component to store", () => {
</templates> </templates>
`); `);
class App extends ConnectedComponent<any, any, any> { class App extends ConnectedComponent<any, any> {
static mapStoreToProps(s) { static mapStoreToProps(s) {
return { msg: s.msg }; return { msg: s.msg };
} }
@@ -874,12 +872,12 @@ describe("connected components and default values", () => {
</templates> </templates>
`); `);
class Greeter extends ConnectedComponent<any, any, any> { class Greeter extends ConnectedComponent<any, any> {
static defaultProps = { recipient: "John" }; static defaultProps = { recipient: "John" };
} }
class App extends Component<any, any, any> { class App extends Component<any, any> {
components = { Greeter }; static components = { Greeter };
} }
const store = new Store({ state: {} }); const store = new Store({ state: {} });
@@ -898,12 +896,12 @@ describe("connected components and default values", () => {
</templates> </templates>
`); `);
class Greeter extends ConnectedComponent<any, any, any> { class Greeter extends ConnectedComponent<any, any> {
static defaultProps = { recipient: "John" }; static defaultProps = { recipient: "John" };
} }
class App extends Component<any, any, any> { class App extends Component<any, any> {
components = { Greeter }; static components = { Greeter };
} }
const store = new Store({ state: {} }); const store = new Store({ state: {} });
@@ -913,11 +911,12 @@ describe("connected components and default values", () => {
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>Hello, John</div></div>"); expect(fixture.innerHTML).toBe("<div><div>Hello, John</div></div>");
await app.__updateProps({ initialRecipient: "James" }, true); const fiber = app.__createFiber(true, undefined, undefined, undefined);
await app.__updateProps({ initialRecipient: "James" }, fiber);
await app.render(); await app.render();
expect(fixture.innerHTML).toBe("<div><div>Hello, James</div></div>"); expect(fixture.innerHTML).toBe("<div><div>Hello, James</div></div>");
await app.__updateProps({ initialRecipient: undefined }, true); await app.__updateProps({ initialRecipient: undefined }, fiber);
await app.render(); await app.render();
expect(fixture.innerHTML).toBe("<div><div>Hello, John</div></div>"); expect(fixture.innerHTML).toBe("<div><div>Hello, John</div></div>");
}); });
@@ -938,7 +937,7 @@ describe("connected components and default values", () => {
</templates> </templates>
`); `);
class Message extends ConnectedComponent<any, any, any> { class Message extends ConnectedComponent<any, any> {
static defaultProps = { showId: true }; static defaultProps = { showId: true };
static mapStoreToProps = function(state, ownProps) { static mapStoreToProps = function(state, ownProps) {
return { return {
@@ -947,8 +946,8 @@ describe("connected components and default values", () => {
}; };
} }
class Thread extends ConnectedComponent<any, any, any> { class Thread extends ConnectedComponent<any, any> {
components = { Message }; static components = { Message };
static defaultProps = { showMessages: true }; static defaultProps = { showMessages: true };
static mapStoreToProps = function(state, ownProps) { static mapStoreToProps = function(state, ownProps) {
const thread = state.threads[ownProps.threadId]; const thread = state.threads[ownProps.threadId];
@@ -958,8 +957,8 @@ describe("connected components and default values", () => {
}; };
} }
class App extends Component<any, any, any> { class App extends Component<any, any> {
components = { Thread }; static components = { Thread };
static defaultProps = { threadId: 1 }; static defaultProps = { threadId: 1 };
} }
@@ -1000,7 +999,8 @@ describe("connected components and default values", () => {
"<div><div><div>100Message100</div><div>101Message101</div></div></div>" "<div><div><div>100Message100</div><div>101Message101</div></div></div>"
); );
await app.__updateProps({ threadId: 2 }, true); const fiber = app.__createFiber(true, undefined, undefined, undefined);
await app.__updateProps({ threadId: 2 }, fiber);
await app.render(); await app.render();
expect(fixture.innerHTML).toBe("<div><div><div>200Message200</div></div></div>"); expect(fixture.innerHTML).toBe("<div><div><div>200Message200</div></div></div>");
@@ -1019,15 +1019,15 @@ describe("connected components and default values", () => {
<div t-name="Child"><t t-esc="storeProps.val"/></div> <div t-name="Child"><t t-esc="storeProps.val"/></div>
</templates> </templates>
`); `);
class Child extends ConnectedComponent<any, any, any> { class Child extends ConnectedComponent<any, any> {
static mapStoreToProps(s) { static mapStoreToProps(s) {
return s; return s;
} }
} }
class Parent extends Component<any, any, any> { class Parent extends Component<any, any> {
components = { Child }; static components = { Child };
state = { child: true }; state = useState({ child: true });
} }
class TestStore extends Store { class TestStore extends Store {
@@ -1063,7 +1063,7 @@ describe("connected components and default values", () => {
</templates> </templates>
`); `);
class App extends ConnectedComponent<any, any, any> { class App extends ConnectedComponent<any, any> {
static mapStoreToProps = function(state) { static mapStoreToProps = function(state) {
return { return {
counter: state.counter counter: state.counter
@@ -1088,9 +1088,91 @@ describe("connected components and default values", () => {
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>0</div>"); expect(fixture.innerHTML).toBe("<div>0</div>");
const res = app.dispatch('inc'); const res = app.dispatch("inc");
expect(res).toBe(1); expect(res).toBe(1);
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe("<div>1</div>"); expect(fixture.innerHTML).toBe("<div>1</div>");
}); });
}); });
describe("various scenarios", () => {
let fixture: HTMLElement;
let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
});
afterEach(() => {
fixture.remove();
});
test("scenarios with async store updates and some components events", async () => {
const actions = {
async deleteAttachment({ state }) {
await Promise.resolve();
delete state.attachments[100];
state.messages[10].attachmentIds = [];
}
};
const state = {
attachments: {
100: {
id: 100,
name: "text.txt"
}
},
messages: {
10: {
attachmentIds: [100],
id: 10
}
}
};
const store = new Store({ actions, state });
env.qweb.addTemplates(`
<templates>
<div t-name="Message">
<button t-on-click="doStuff">Do stuff</button>
<Attachment t-foreach="storeProps.attachmentIds" t-key="attachmentId" t-as="attachmentId" id="attachmentId"/>
</div>
<div t-name="Attachment">
<span>Attachment <t t-esc="props.id"/></span>
<span>Name: <t t-esc="storeProps.name"/></span>
</div>
</templates>
`);
class Attachment extends ConnectedComponent<any, any> {
static mapStoreToProps(state, ownProps) {
return {
name: state.attachments[ownProps.id].name
};
}
}
class Message extends ConnectedComponent<any, any> {
static mapStoreToProps(state) {
return {
attachmentIds: state.messages[10].attachmentIds
};
}
static components = { Attachment };
state = { isAttachmentDeleted: false };
doStuff() {
this.dispatch("deleteAttachment", 100);
this.state.isAttachmentDeleted = true;
}
}
(<any>env).store = store;
const message = new Message(env);
await message.mount(fixture);
expect(fixture.innerHTML).toMatchSnapshot();
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.innerHTML).toMatchSnapshot();
});
});
+7 -11
View File
@@ -1,5 +1,5 @@
import { h, patch } from "../src/vdom"; import { h, patch } from "../src/vdom";
import { init } from "../src/vdom/vdom"; import { init, addNS } from "../src/vdom/vdom";
function map(list, fn) { function map(list, fn) {
var ret: any[] = []; var ret: any[] = [];
@@ -206,19 +206,15 @@ describe("snabbdom", function() {
expect(elm.firstChild.namespaceURI).toBe(SVGNamespace); expect(elm.firstChild.namespaceURI).toBe(SVGNamespace);
// verify that svg tag automatically gets svg namespace // verify that svg tag automatically gets svg namespace
elm = patch(vnode0, h("svg", [h("foreignObject", [h("div", ["I am HTML embedded in SVG"])])])) const vnode = h("svg", [h("foreignObject", [h("div", ["I am HTML embedded in SVG"])])]);
.elm; // need to add namespace manually. it is usually done by the template
// compiler
addNS(vnode.data, (vnode as any).children, vnode.sel);
elm = patch(vnode0, vnode).elm;
expect(elm.namespaceURI).toBe(SVGNamespace); expect(elm.namespaceURI).toBe(SVGNamespace);
expect(elm.firstChild.namespaceURI).toBe(SVGNamespace); expect(elm.firstChild.namespaceURI).toBe(SVGNamespace);
expect(elm.firstChild.firstChild.namespaceURI).toBe(XHTMLNamespace); expect(elm.firstChild.firstChild.namespaceURI).toBe(XHTMLNamespace);
// verify that svg tag with extra selectors gets svg namespace
elm = patch(vnode0, h("svg#some-id")).elm;
expect(elm.namespaceURI).toBe(SVGNamespace);
// verify that non-svg tag beginning with 'svg' does NOT get namespace
elm = patch(vnode0, h("svg-custom-el")).elm;
expect(elm.namespaceURI).not.toBe(SVGNamespace);
}); });
test("receives classes in selector", function() { test("receives classes in selector", function() {
+153
View File
@@ -0,0 +1,153 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
static components = { Counter };
shouldUpdate(nextProps) {
return nextProps.message !== this.props.message;
}
removeMessage() {
this.trigger("remove-message", {
id: this.props.message.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
static components = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
mounted() {
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
}
benchmark(message, fn, callback) {
if (this.state.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.state.clearAfterFlag) {
this._benchmark(
"clear",
() => {
this.state.messages = [];
},
finalize,
false
);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
}
_benchmark(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
fn();
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
}
addMessages(n) {
this.benchmark("add " + n, () => {
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
});
}
clear() {
this._benchmark("clear", () => {
this.state.messages = [];
});
}
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
messages[i] = msg;
}
});
}
removeMessage(event) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === event.detail.id);
this.state.messages.splice(index, 1);
});
}
log(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.21.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+50
View File
@@ -0,0 +1,50 @@
<templates>
<div t-name="App" class="main">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1k messages</button>
<button t-on-click="addMessages(10000)">Add 10k messages</button>
<button t-on-click="addMessages(30000)">Add 30k messages</button>
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-model="multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-model="clearAfterFlag" />
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
<hr/>
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
<div class="log">
<div class="log-content" t-ref="log"/>
</div>
</div>
<div class="right-thing">
<div class="content" t-on-remove-message="removeMessage">
<t t-foreach="state.messages" t-as="message">
<Message t-key="message.id" message="message"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.message.author"/></span>
<span class="msg"><t t-esc="props.message.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<Counter/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
+9 -7
View File
@@ -1,10 +1,11 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js"; import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
const { useState, useRef } = owl.hooks;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Likes Counter Widget // Likes Counter Widget
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class Counter extends owl.Component { class Counter extends owl.Component {
state = { counter: 0 }; state = useState({ counter: 0 });
increment() { increment() {
this.state.counter++; this.state.counter++;
@@ -15,7 +16,7 @@ class Counter extends owl.Component {
// Message Widget // Message Widget
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class Message extends owl.Component { class Message extends owl.Component {
components = { Counter }; static components = { Counter };
shouldUpdate(nextProps) { shouldUpdate(nextProps) {
return nextProps.message !== this.props.message; return nextProps.message !== this.props.message;
@@ -31,8 +32,9 @@ class Message extends owl.Component {
// Root Widget // Root Widget
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class App extends owl.Component { class App extends owl.Component {
components = { Message }; static components = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false }; state = useState({ messages: [], multipleFlag: false, clearAfterFlag: false });
logRef = useRef("log");
mounted() { mounted() {
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`); this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
@@ -129,12 +131,12 @@ class App extends owl.Component {
div.classList.add("bold"); div.classList.add("bold");
} }
div.textContent = `> ${str}`; div.textContent = `> ${str}`;
this.refs.log.appendChild(div); this.logRef.el.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight; this.logRef.el.scrollTop = this.logRef.el.scrollHeight;
} }
clearLog() { clearLog() {
this.refs.log.innerHTML = ""; this.logRef.el.innerHTML = "";
} }
} }
+2 -2
View File
@@ -12,11 +12,11 @@
</div> </div>
<div class="flags"> <div class="flags">
<div> <div>
<input type="checkbox" id="multipleflag" t-model="multipleFlag"/> <input type="checkbox" id="multipleflag" t-model="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label> <label for="multipleflag">Do it 20x</label>
</div> </div>
<div> <div>
<input type="checkbox" id="clearFlag" t-model="clearAfterFlag" /> <input type="checkbox" id="clearFlag" t-model="state.clearAfterFlag" />
<label for="clearFlag">Clear after</label> <label for="clearFlag">Clear after</label>
</div> </div>
</div> </div>
+1
View File
@@ -39,6 +39,7 @@
<li><a href="benchmarks/owl-0.16.0">OWL 0.16.0</a></li> <li><a href="benchmarks/owl-0.16.0">OWL 0.16.0</a></li>
<li><a href="benchmarks/owl-0.17.0">OWL 0.17.0</a></li> <li><a href="benchmarks/owl-0.17.0">OWL 0.17.0</a></li>
<li><a href="benchmarks/owl-0.18.0">OWL 0.18.0</a></li> <li><a href="benchmarks/owl-0.18.0">OWL 0.18.0</a></li>
<li><a href="benchmarks/owl-0.21.0">OWL 0.21.0</a></li>
<li><a href="benchmarks/owl-master">OWL Master</a></li> <li><a href="benchmarks/owl-master">OWL Master</a></li>
</ul> </ul>
<ul> <ul>
+128 -126
View File
@@ -1,5 +1,5 @@
import { SAMPLES } from "./samples.js"; import { SAMPLES } from "./samples.js";
const {useState, useRef} = owl.hooks;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Constants, helpers, utils // Constants, helpers, utils
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -150,145 +150,24 @@ Promise.all([loadTemplates(), owl.utils.whenReady()]).then(start);
return zip.generateAsync({ type: "blob" }); return zip.generateAsync({ type: "blob" });
} }
//------------------------------------------------------------------------------
// MAIN APP
//------------------------------------------------------------------------------
class App extends owl.Component {
constructor(...args) {
super(...args);
this.version = owl.__info__.version;
this.SAMPLES = SAMPLES;
this.components = { TabbedEditor };
this.state = {
js: SAMPLES[0].code,
css: SAMPLES[0].css || "",
xml: SAMPLES[0].xml || DEFAULT_XML,
error: false,
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);
}
displayError(error) {
this.state.error = error;
if (error) {
setTimeout(() => {
this.refs.content.innerHTML = "";
});
return;
}
}
runCode() {
this.state.displayWelcome = false;
let subiframe;
let error = false;
const errorHandler = e => this.displayError(e.message || e.reason.message);
try {
const { js, css, xml } = this.state;
subiframe = makeCodeIframe(js, css, xml, errorHandler);
} catch (e) {
//probably problem with the templates
error = e;
// we still log the error, always useful to have it available
console.error(e);
}
if (error) {
this.displayError(error.message);
return;
} else {
this.state.error = false;
}
this.refs.content.innerHTML = "";
this.refs.content.appendChild(subiframe);
}
setSample(ev) {
const sample = 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;
}
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) {
this.state[ev.detail.type] = ev.detail.value;
}
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");
}
}
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Tabbed editor // Tabbed editor
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class TabbedEditor extends owl.Component { class TabbedEditor extends owl.Component {
constructor(parent, props) { constructor(parent, props) {
super(parent, props); super(parent, props);
this.state = { this.state = useState({
currentTab: props.js ? "js" : props.xml ? "xml" : "css" currentTab: props.js ? "js" : props.xml ? "xml" : "css"
}; });
this.setTab = owl.utils.debounce(this.setTab, 250, true); this.setTab = owl.utils.debounce(this.setTab, 250, true);
this.sessions = {}; this.sessions = {};
this._setupSessions(props); this._setupSessions(props);
this.editor = null; this.editorNode = useRef("editor");
} }
mounted() { mounted() {
this.editor = this.editor || ace.edit(this.refs.editor); this.editor = this.editor || ace.edit(this.editorNode.el);
this.editor.setValue(this.props[this.state.currentTab], -1); this.editor.setValue(this.props[this.state.currentTab], -1);
this.editor.setFontSize("12px"); this.editor.setFontSize("12px");
@@ -362,6 +241,129 @@ class TabbedEditor extends owl.Component {
} }
} }
//------------------------------------------------------------------------------
// MAIN APP
//------------------------------------------------------------------------------
class App extends owl.Component {
constructor(...args) {
super(...args);
this.version = owl.__info__.version;
this.SAMPLES = SAMPLES;
this.state = useState({
js: SAMPLES[0].code,
css: SAMPLES[0].css || "",
xml: SAMPLES[0].xml || DEFAULT_XML,
error: false,
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");
}
displayError(error) {
this.state.error = error;
if (error) {
setTimeout(() => {
this.content.el.innerHTML = "";
});
return;
}
}
runCode() {
this.state.displayWelcome = false;
let subiframe;
let error = false;
const errorHandler = e => this.displayError(e.message || e.reason.message);
try {
const { js, css, xml } = this.state;
subiframe = makeCodeIframe(js, css, xml, errorHandler);
} catch (e) {
//probably problem with the templates
error = e;
// we still log the error, always useful to have it available
console.error(e);
}
if (error) {
this.displayError(error.message);
return;
} else {
this.state.error = false;
}
this.content.el.innerHTML = "";
this.content.el.appendChild(subiframe);
}
setSample(ev) {
const sample = 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;
}
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) {
this.state[ev.detail.type] = ev.detail.value;
}
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 // Application initialization
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
+162 -78
View File
@@ -1,7 +1,8 @@
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 } = owl;
class Greeter extends owl.Component { class Greeter extends Component {
state = { word: 'Hello' }; state = useState({ word: 'Hello' });
toggle() { toggle() {
this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi' this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi'
@@ -9,9 +10,9 @@ class Greeter extends owl.Component {
} }
// Main root component // Main root component
class App extends owl.Component { class App extends Component {
components = { Greeter }; static components = { Greeter };
state = { name: 'World'}; state = useState({ name: 'World'});
} }
// Application setup // Application setup
@@ -45,18 +46,19 @@ 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 } = owl;
class Counter extends owl.Component { class Counter extends Component {
state = { value: 0 }; state = useState({ value: 0 });
increment() { increment() {
this.state.value++; this.state.value++;
} }
} }
class App extends owl.Component { class App extends Component {
state = { flag: false, componentFlag: false, numbers: [] }; state = useState({ flag: false, componentFlag: false, numbers: [] });
components = { Counter }; static components = { Counter };
toggle(key) { toggle(key) {
this.state[key] = !this.state[key]; this.state[key] = !this.state[key];
@@ -183,11 +185,12 @@ 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 } = owl;
class DemoComponent extends owl.Component { class DemoComponent extends Component {
constructor() { constructor() {
super(...arguments); super(...arguments);
this.state = { n: 0 }; this.state = useState({ n: 0 });
console.log("constructor"); console.log("constructor");
} }
async willStart() { async willStart() {
@@ -213,9 +216,9 @@ class DemoComponent extends owl.Component {
} }
} }
class App extends owl.Component { class App extends Component {
components = { DemoComponent }; static components = { DemoComponent };
state = { n: 0, flag: true }; state = useState({ n: 0, flag: true });
increment() { increment() {
this.state.n++; this.state.n++;
@@ -259,12 +262,70 @@ const LIFECYCLE_CSS = `button {
width: 250px; width: 250px;
}`; }`;
const HOOKS_DEMO = `// In this example, we show how hooks can be used or defined.
const {useState, onMounted, onWillUnmount} = owl.hooks;
// We define here a custom behaviour: this hook tracks the state of the mouse
// position
function useMouse() {
const position = useState({x:0, y: 0});
function update(e) {
position.x = e.clientX;
position.y = e.clientY;
}
onMounted(() => {
window.addEventListener('mousemove', update);
});
onWillUnmount(() => {
window.removeEventListener('mousemove', update);
});
return position;
}
// Main root component
class App extends owl.Component {
// simple state hook (reactive object)
counter = useState({ value: 0 });
// this hooks is bound to the 'mouse' property.
mouse = useMouse();
increment() {
this.counter.value++;
}
}
// Application setup
const qweb = new owl.QWeb(TEMPLATES);
const app = new App({ qweb });
app.mount(document.body);
`;
const HOOKS_DEMO_XML = `<templates>
<div t-name="App">
<button t-on-click="increment">Click! <t t-esc="counter.value"/></button>
<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>
</div>
</templates>
`;
const HOOKS_CSS = `button {
width: 120px;
height: 35px;
font-size: 16px;
}`;
const TODO_APP_STORE = `// This example is an implementation of the TodoList application, from the const TODO_APP_STORE = `// This example is an implementation of the TodoList application, from the
// www.todomvc.com project. This is a non trivial application with some // www.todomvc.com project. This is a non trivial application with some
// interesting user interactions. It uses the local storage for persistence. // interesting user interactions. It uses the local storage for persistence.
// //
// 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 } = owl;
const { useRef } = owl.hooks;
const ENTER_KEY = 13; const ENTER_KEY = 13;
const ESC_KEY = 27; const ESC_KEY = 27;
@@ -335,8 +396,9 @@ function makeStore() {
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// TodoItem // TodoItem
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class TodoItem extends owl.Component { class TodoItem extends Component {
state = { isEditing: false }; state = useState({ isEditing: false });
inputRef = useRef("input");
removeTodo() { removeTodo() {
this.env.store.dispatch("removeTodo", this.props.id); this.env.store.dispatch("removeTodo", this.props.id);
@@ -351,9 +413,9 @@ class TodoItem extends owl.Component {
} }
focusInput() { focusInput() {
this.refs.input.value = ""; this.inputRef.el.value = "";
this.refs.input.focus(); this.inputRef.el.focus();
this.refs.input.value = this.props.title; this.inputRef.el.value = this.props.title;
} }
handleKeyup(ev) { handleKeyup(ev) {
@@ -388,8 +450,8 @@ class TodoItem extends owl.Component {
// TodoApp // TodoApp
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class TodoApp extends owl.store.ConnectedComponent { class TodoApp extends owl.store.ConnectedComponent {
components = { TodoItem }; static components = { TodoItem };
state = { filter: "all" }; state = useState({ filter: "all" });
static mapStoreToProps(state) { static mapStoreToProps(state) {
return { return {
@@ -896,25 +958,24 @@ const RESPONSIVE = `// In this example, we show how we can modify keys in the gl
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class Navbar extends owl.Component {} class Navbar extends owl.Component {}
class ControlPanel extends owl.Component { class MobileSearchView extends owl.Component {}
components = { MobileSearchView };
}
class FormView extends owl.Component { class ControlPanel extends owl.Component {
components = { AdvancedComponent }; static components = { MobileSearchView };
} }
class AdvancedComponent extends owl.Component {} class AdvancedComponent extends owl.Component {}
class FormView extends owl.Component {
static components = { AdvancedComponent };
}
class Chatter extends owl.Component { class Chatter extends owl.Component {
messages = Array.from(Array(100).keys()); messages = Array.from(Array(100).keys());
} }
class MobileSearchView extends owl.Component {}
class App extends owl.Component { class App extends owl.Component {
components = { Navbar, ControlPanel, FormView, Chatter }; static components = { Navbar, ControlPanel, FormView, Chatter };
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -1053,17 +1114,18 @@ 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 } = owl;
class Card extends owl.Component { class Card extends Component {
state = { showContent: true }; state = useState({ showContent: true });
toggleDisplay() { toggleDisplay() {
this.state.showContent = !this.state.showContent; this.state.showContent = !this.state.showContent;
} }
} }
class Counter extends owl.Component { class Counter extends Component {
state = {val: 1}; state = useState({val: 1});
inc() { inc() {
this.state.val++; this.state.val++;
@@ -1071,9 +1133,9 @@ class Counter extends owl.Component {
} }
// Main root component // Main root component
class App extends owl.Component { class App extends Component {
components = {Card, Counter}; static components = {Card, Counter};
state = {a: 1, b: 3}; state = useState({a: 1, b: 3});
inc(key, delta) { inc(key, delta) {
this.state[key] += delta; this.state[key] += delta;
@@ -1169,10 +1231,21 @@ 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 't-asyncroot' directive for this // because of the slow component. We use the 't-asyncroot' directive for this
// purpose. Try removing it to see the difference. // purpose. Try removing it to see the difference.
const { Component, useState } = owl;
class App extends owl.Component { class SlowComponent extends Component {
components = {SlowComponent, NotificationList}; willUpdateProps() {
state = { value: 0, notifs: [] }; // simulate a component that needs to perform async stuff (e.g. an RPC)
// with the updated props before re-rendering itself
return new Promise(resolve => setTimeout(resolve, 1500));
}
}
class NotificationList extends Component {}
class App extends Component {
static components = {SlowComponent, NotificationList};
state = useState({ value: 0, notifs: [] });
increment() { increment() {
this.state.value++; this.state.value++;
@@ -1185,16 +1258,6 @@ class App extends owl.Component {
} }
} }
class SlowComponent extends owl.Component {
willUpdateProps() {
// simulate a component that needs to perform async stuff (e.g. an RPC)
// with the updated props before re-rendering itself
return new Promise(resolve => setTimeout(resolve, 1500));
}
}
class NotificationList extends owl.Component {}
const qweb = new owl.QWeb(TEMPLATES); const qweb = new owl.QWeb(TEMPLATES);
const app = new App({ qweb }); const app = new App({ qweb });
app.mount(document.body); app.mount(document.body);
@@ -1251,15 +1314,16 @@ 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 } = owl;
class Form extends owl.Component { class Form extends Component {
state = { state = useState({
text: "", text: "",
othertext: "", othertext: "",
number: 11, number: 11,
color: "", color: "",
bool: false bool: false
}; });
} }
// Application setup // Application setup
@@ -1272,20 +1336,20 @@ const FORM_XML = `<templates>
<div t-name="Form"> <div t-name="Form">
<h1>Form</h1> <h1>Form</h1>
<div> <div>
Text (immediate): <input t-model="text"/> Text (immediate): <input t-model="state.text"/>
</div> </div>
<div> <div>
Other text (lazy): <input t-model.lazy="othertext"/> Other text (lazy): <input t-model.lazy="state.othertext"/>
</div> </div>
<div> <div>
Number: <input t-model.number="number"/> Number: <input t-model.number="state.number"/>
</div> </div>
<div> <div>
Boolean: <input type="checkbox" t-model="bool"/> Boolean: <input type="checkbox" t-model="state.bool"/>
</div> </div>
<div> <div>
Color, with a select: Color, with a select:
<select t-model="color"> <select t-model="state.color">
<option value="">Select a color</option> <option value="">Select a color</option>
<option value="red">Red</option> <option value="red">Red</option>
<option value="blue">Blue</option> <option value="blue">Blue</option>
@@ -1293,8 +1357,8 @@ const FORM_XML = `<templates>
</div> </div>
<div> <div>
Color, with radio buttons: Color, with radio buttons:
<span><input type="radio" name="color" id="red" value="red" t-model="color"/><label for="red">Red</label></span> <span><input type="radio" name="color" id="red" value="red" t-model="state.color"/><label for="red">Red</label></span>
<span><input type="radio" name="color" id="blue" value="blue" t-model="color"/><label for="blue">Blue</label></span> <span><input type="radio" name="color" id="blue" value="blue" t-model="state.color"/><label for="blue">Blue</label></span>
</div> </div>
<hr/> <hr/>
<h1>State</h1> <h1>State</h1>
@@ -1317,26 +1381,31 @@ 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 } = owl;
const { useRef } = owl.hooks;
class HelloWorld extends owl.Component {} class HelloWorld extends Component {}
class Counter extends owl.Component { class Counter extends Component {
state = { value: 0 }; state = useState({ value: 0 });
inc() { inc() {
this.state.value++; this.state.value++;
} }
} }
class Window extends owl.Component { class Window extends Component {
get style() { get style() {
let { width, height, top, left, zindex } = this.props.info; let { width, height, top, left, zindex } = this.props.info;
return \`width: \${width}px;height: \${height}px;top:\${top}px;left:\${left}px;z-index:\${zindex}\`; return \`width: \${width}px;height: \${height}px;top:\${top}px;left:\${left}px;z-index:\${zindex}\`;
} }
close() { close() {
this.trigger("close-window", { id: this.props.info.id }); this.trigger("close-window", { id: this.props.info.id });
} }
startDragAndDrop(ev) { startDragAndDrop(ev) {
this.updateZIndex(); this.updateZIndex();
this.el.classList.add('dragging'); this.el.classList.add('dragging');
@@ -1362,44 +1431,52 @@ class Window extends owl.Component {
self.trigger("set-window-position", options); self.trigger("set-window-position", options);
} }
} }
updateZIndex() { updateZIndex() {
this.trigger("update-z-index", { id: this.props.info.id }); this.trigger("update-z-index", { id: this.props.info.id });
} }
} }
class WindowManager extends owl.Component { class WindowManager extends Component {
components = { Window }; static components = { Window };
windows = []; windows = [];
nextId = 1; nextId = 1;
currentZindex = 1; currentZindex = 1;
nextLeft = 0;
nextTop = 0;
addWindow(name) { addWindow(name) {
const info = this.env.windows.find(w => w.name === name); const info = this.env.windows.find(w => w.name === name);
const id = \`w\${this.nextId++}\`; this.nextLeft = this.nextLeft + 30;
this.nextTop = this.nextTop + 30;
this.windows.push({ this.windows.push({
id: id, id: this.nextId++,
title: info.title, title: info.title,
width: info.defaultWidth, width: info.defaultWidth,
height: info.defaultHeight, height: info.defaultHeight,
top: 0, top: this.nextTop,
left: 0, left: this.nextLeft,
zindex: this.currentZindex++ zindex: this.currentZindex++,
component: info.component
}); });
this.components[id] = info.component;
this.render(); this.render();
} }
closeWindow(ev) { closeWindow(ev) {
const id = ev.detail.id; const id = ev.detail.id;
delete this.components[id]; delete this.constructor.components[id];
const index = this.windows.findIndex(w => w.id === id); const index = this.windows.findIndex(w => w.id === id);
this.windows.splice(index, 1); this.windows.splice(index, 1);
this.render(); this.render();
} }
setWindowPosition(ev) { setWindowPosition(ev) {
const id = ev.detail.id; const id = ev.detail.id;
const w = this.windows.find(w => w.id === id); const w = this.windows.find(w => w.id === id);
w.top = ev.detail.top; w.top = ev.detail.top;
w.left = ev.detail.left; w.left = ev.detail.left;
} }
updateZIndex(ev) { updateZIndex(ev) {
const id = ev.detail.id; const id = ev.detail.id;
const w = this.windows.find(w => w.id === id); const w = this.windows.find(w => w.id === id);
@@ -1408,11 +1485,12 @@ class WindowManager extends owl.Component {
} }
} }
class App extends owl.Component { class App extends Component {
components = { WindowManager }; static components = { WindowManager };
wmRef = useRef("wm");
addWindow(name) { addWindow(name) {
this.refs.wm.addWindow(name); this.wmRef.comp.addWindow(name);
} }
} }
@@ -1453,7 +1531,7 @@ const WMS_XML = `<templates>
t-on-update-z-index="updateZIndex" t-on-update-z-index="updateZIndex"
t-on-set-window-position="setWindowPosition"> t-on-set-window-position="setWindowPosition">
<Window t-foreach="windows" t-as="w" t-key="w.id" info="w"> <Window t-foreach="windows" t-as="w" t-key="w.id" info="w">
<t t-component="{{w.id}}"/> <t t-component="w.component"/>
</Window> </Window>
</div> </div>
@@ -1571,6 +1649,12 @@ export const SAMPLES = [
xml: LIFECYCLE_DEMO_XML, xml: LIFECYCLE_DEMO_XML,
css: LIFECYCLE_CSS css: LIFECYCLE_CSS
}, },
{
description: "Hooks",
code: HOOKS_DEMO,
xml: HOOKS_DEMO_XML,
css: HOOKS_CSS
},
{ {
description: "Todo List App (with store)", description: "Todo List App (with store)",
code: TODO_APP_STORE, code: TODO_APP_STORE,