mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 46095b7a96 | |||
| c3453d35b9 | |||
| b0dd0252a4 | |||
| e2b2e54e29 | |||
| 766c378ac9 | |||
| 022b29b6a0 | |||
| 367725d194 | |||
| e82ed7dc8c | |||
| 63c3d4f6b0 | |||
| b859fcb8f0 | |||
| 3005756d89 | |||
| f6820bb8ac | |||
| 090880f478 | |||
| 5cdaa7b473 | |||
| 91b9e78182 | |||
| 5335fb8fba | |||
| 4513e3f31c | |||
| ecf12f6eee | |||
| 82a6961b3a | |||
| 78a4550ebb | |||
| cc82b3ffcd | |||
| bbbaf95c8e | |||
| caa63628dc | |||
| 7a49b9d94e | |||
| 01eb338e69 | |||
| 3048c6f961 | |||
| 8edc637033 | |||
| c9910077a4 | |||
| 6bf70f016c | |||
| 905619f80f |
@@ -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.
|
||||
@@ -1,26 +1,32 @@
|
||||
<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
|
||||
|
||||
The Odoo Web Library (OWL) is a small (~16kb gzipped) UI framework intended to be the basis for
|
||||
the [Odoo](https://www.odoo.com/) Web Client, and hopefully many other Odoo
|
||||
related projects. OWL's main features are:
|
||||
The Odoo Web Library (OWL) is a smallish (~17kb gzipped) UI framework intended to
|
||||
be the basis for the [Odoo](https://www.odoo.com/) Web Client. Owl is a modern
|
||||
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 store implementation (for state management)
|
||||
- a declarative component system,
|
||||
- a reactivity system based on hooks,
|
||||
- a store implementation (for state management),
|
||||
- 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.
|
||||
|
||||
**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 } from 'owl'
|
||||
import { xml } from 'owl/tags'
|
||||
import { Component, QWeb, useState } from "owl";
|
||||
import { xml } from "owl/tags";
|
||||
|
||||
class Counter extends Component {
|
||||
static template = xml`
|
||||
@@ -28,7 +34,7 @@ class Counter extends Component {
|
||||
Click Me! [<t t-esc="state.value"/>]
|
||||
</button>`;
|
||||
|
||||
state = { value: 0 };
|
||||
state = useState({ value: 0 });
|
||||
|
||||
increment() {
|
||||
this.state.value++;
|
||||
@@ -43,17 +49,20 @@ class App extends Component {
|
||||
</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.
|
||||
|
||||
## OWL's Design Principles
|
||||
## Design Principles
|
||||
|
||||
OWL is designed to be used in highly dynamic applications where changing
|
||||
requirements are common and code needs to be maintained by large teams.
|
||||
@@ -82,7 +91,7 @@ The complete documentation can be found [here](doc/readme.md). The most importan
|
||||
|
||||
- [Quick Start](doc/quick_start.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?
|
||||
Submit a PR!
|
||||
@@ -91,20 +100,159 @@ Submit a PR!
|
||||
|
||||
If you want to use a simple `<script>` tag, the last release can be downloaded here:
|
||||
|
||||
- [owl-0.21.0.js](https://github.com/odoo/owl/releases/download/v0.21.0/owl.js)
|
||||
- [owl-0.21.0.min.js](https://github.com/odoo/owl/releases/download/v0.21.0/owl.min.js)
|
||||
- [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 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 |
|
||||
| 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
|
||||
|
||||
|
||||
+193
-89
@@ -5,6 +5,7 @@
|
||||
- [Overview](#overview)
|
||||
- [Example](#example)
|
||||
- [Reference](#reference)
|
||||
- [Reactive System](#reactive-system)
|
||||
- [Properties](#properties)
|
||||
- [Static Properties](#static-properties)
|
||||
- [Methods](#methods)
|
||||
@@ -15,13 +16,14 @@
|
||||
- [Event Handling](#event-handling)
|
||||
- [Form Input Bindings](#form-input-bindings)
|
||||
- [`t-key` Directive](#t-key-directive)
|
||||
- [`t-mounted` Directive](#t-mounted-directive)
|
||||
- [Semantics](#semantics)
|
||||
- [Props Validation](#props-validation)
|
||||
- [References](#references)
|
||||
- [Slots](#slots)
|
||||
- [Asynchronous Rendering](#asynchronous-rendering)
|
||||
- [Error Handling](#error-handling)
|
||||
- [Functional Components](#functional-components)
|
||||
- [SVG components](#svg-components)
|
||||
|
||||
## 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
|
||||
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
|
||||
|
||||
Let us have a look at a simple component:
|
||||
|
||||
```javascript
|
||||
const { useState } = owl.hooks;
|
||||
|
||||
class ClickCounter extends owl.Component {
|
||||
state = { value: 0 };
|
||||
state = useState({ value: 0 });
|
||||
|
||||
increment() {
|
||||
this.state.value++;
|
||||
@@ -74,9 +75,8 @@ latest browsers without a transpilation step.
|
||||
This example show how a component should be defined: it simply subclasses the
|
||||
Component class. If no static `template` key is defined, then
|
||||
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
|
||||
is certainly encouraged. The state object is [observed](observer.md), and any
|
||||
change to it will cause a rerendering.
|
||||
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
|
||||
[observed](observer.md), and any change to it will cause a rerendering.
|
||||
|
||||
## 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
|
||||
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
|
||||
|
||||
- **`el`** (HTMLElement | null): reference to the DOM root node of the element. It is `null` when the
|
||||
@@ -96,18 +115,11 @@ find a template with the component name (or one of its ancestor).
|
||||
|
||||
- **`env`** (Object): the component environment, which contains a QWeb instance.
|
||||
|
||||
- **`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
|
||||
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.
|
||||
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
|
||||
|
||||
- **`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
|
||||
@@ -236,7 +248,7 @@ component.
|
||||
```javascript
|
||||
constructor(parent, props) {
|
||||
super(parent, props);
|
||||
this.state = {someValue: true};
|
||||
this.state = useState({someValue: true});
|
||||
this.template = 'mytemplate';
|
||||
}
|
||||
```
|
||||
@@ -246,7 +258,7 @@ implemented in most cases:
|
||||
|
||||
```javascript
|
||||
class ClickCounter extends owl.Component {
|
||||
state = { value: 0 };
|
||||
state = useState({ value: 0 });
|
||||
|
||||
...
|
||||
}
|
||||
@@ -271,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
|
||||
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` is called each time a component is attached to the
|
||||
@@ -287,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 parent, then all its children.
|
||||
|
||||
Note that the state is now observed. It is however allowed (but not encouraged)
|
||||
to modify the state in the `mounted` hook. Doing so will cause a rerender,
|
||||
which will not be perceptible by the user, but will slightly slow down the
|
||||
component.
|
||||
It is allowed (but not encouraged) to modify the state in the `mounted` hook.
|
||||
Doing so will cause a rerender, which will not be perceptible by the user, but
|
||||
will slightly slow down the component.
|
||||
|
||||
#### `willUpdateProps(nextProps)`
|
||||
|
||||
@@ -315,14 +323,11 @@ 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
|
||||
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
|
||||
DOM state. Also, it will not be called if the component is not in the DOM (this can
|
||||
happen with components with `t-keepalive`).
|
||||
|
||||
The return value of this method will be given as the first argument of the
|
||||
corresponding `patched` call.
|
||||
|
||||
#### `patched(snapshot)`
|
||||
|
||||
This hook is called whenever a component did actually update its DOM (most
|
||||
@@ -333,8 +338,6 @@ with the DOM (for example, through an external library) whenever the
|
||||
component was patched. Note that this hook will not be called if the compoent is
|
||||
not in the DOM (this can happen with components with `t-keepalive`).
|
||||
|
||||
The `snapshot` parameter is the result of the previous `willPatch` call.
|
||||
|
||||
Updating the compoent state in this hook is possible, but not encouraged.
|
||||
One need to be careful, because updates here will cause rerender, which in
|
||||
turn will cause other calls to patched. So, we need to be particularly
|
||||
@@ -397,9 +400,9 @@ easy to test a component.
|
||||
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
|
||||
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
|
||||
`update` event on the QWeb instance. For example, a responsive environment
|
||||
could be programmed like this:
|
||||
themselves whenever the environment is modified: one only needs to call the
|
||||
`forceUpdate` method on the QWeb instance. For example, a responsive environment
|
||||
could be done like this:
|
||||
|
||||
```js
|
||||
function setupResponsivePlugin(env) {
|
||||
@@ -408,7 +411,7 @@ function setupResponsivePlugin(env) {
|
||||
const updateEnv = owl.utils.debounce(() => {
|
||||
if (env.isMobile !== isMobile()) {
|
||||
env.isMobile = !env.isMobile;
|
||||
env.qweb.trigger("update");
|
||||
env.qweb.forceUpdate();
|
||||
}
|
||||
}, 15);
|
||||
window.addEventListener("resize", updateEnv);
|
||||
@@ -442,7 +445,8 @@ dynamic. If it is necessary to give a string, this can be done by quoting it:
|
||||
`someString="'somevalue'"`.
|
||||
|
||||
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
|
||||
<div t-name="ParentComponent">
|
||||
@@ -453,7 +457,7 @@ that the template can access `state`, `props`, `env`, or any methods defined in
|
||||
```js
|
||||
class ParentComponent {
|
||||
static components = { ChildComponent };
|
||||
state = { val: 4 };
|
||||
state = useState({ val: 4 });
|
||||
}
|
||||
```
|
||||
|
||||
@@ -502,6 +506,31 @@ class ParentComponent {
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
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.
|
||||
@@ -617,7 +646,7 @@ form!). A possible way to do this is to do it by hand:
|
||||
|
||||
```js
|
||||
class Form extends owl.Component {
|
||||
state = { text: "" };
|
||||
state = useState({ text: "" });
|
||||
|
||||
_updateInputValue(event) {
|
||||
this.state.text = event.target.value;
|
||||
@@ -637,8 +666,9 @@ plumbing code is slightly different if you need to interact with a checkbox,
|
||||
or with radio buttons, or with select tags.
|
||||
|
||||
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
|
||||
can write a shorter code, equivalent to the previous example:
|
||||
should be an observed value in the component (usually `state.someValue`). With
|
||||
the `t-model` directive, we can write a shorter code, equivalent to the previous
|
||||
example:
|
||||
|
||||
```js
|
||||
class Form extends owl.Component {
|
||||
@@ -648,7 +678,7 @@ class Form extends owl.Component {
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<input t-model="text" />
|
||||
<input t-model="state.text" />
|
||||
<span t-esc="state.text" />
|
||||
</div>
|
||||
```
|
||||
@@ -658,11 +688,11 @@ The `t-model` directive works with `<input>`, `<input type="checkbox">`,
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<div>Text in an input: <input t-model="someVal"/></div>
|
||||
<div>Textarea: <textarea t-model="otherVal"/></div>
|
||||
<div>Boolean value: <input type="checkbox" t-model="someFlag"/></div>
|
||||
<div>Text in an input: <input t-model="state.someVal"/></div>
|
||||
<div>Textarea: <textarea t-model="state.otherVal"/></div>
|
||||
<div>Boolean value: <input type="checkbox" t-model="state.someFlag"/></div>
|
||||
<div>Selection:
|
||||
<select t-model="color">
|
||||
<select t-model="state.color">
|
||||
<option value="">Select a color</option>
|
||||
<option value="red">Red</option>
|
||||
<option value="blue">Blue</option>
|
||||
@@ -671,11 +701,11 @@ The `t-model` directive works with `<input>`, `<input type="checkbox">`,
|
||||
<div>
|
||||
Selection with radio buttons:
|
||||
<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>
|
||||
</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>
|
||||
</span>
|
||||
</div>
|
||||
@@ -693,7 +723,7 @@ Like event handling, the `t-model` directive accepts some modifiers:
|
||||
For example:
|
||||
|
||||
```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
|
||||
@@ -725,24 +755,6 @@ There are three main use cases:
|
||||
- _animations_: give a different identity to a component. Ex: thread id with
|
||||
animations on add/remove message.
|
||||
|
||||
### `t-mounted` Directive
|
||||
|
||||
The `t-mounted` directive allows to register a callback to execute whenever the node
|
||||
is inserted into the DOM.
|
||||
|
||||
```xml
|
||||
<div><input t-ref="someInput" t-mounted="focusMe"/></div>
|
||||
```
|
||||
|
||||
```js
|
||||
class MyComponent extends owl.Component {
|
||||
...
|
||||
focusMe() {
|
||||
this.refs.someInput.focus();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Semantics
|
||||
|
||||
We give here an informal description of the way components are created/updated
|
||||
@@ -927,44 +939,60 @@ Examples:
|
||||
|
||||
### References
|
||||
|
||||
The `t-ref` directive helps a component keep reference to some inside part of it.
|
||||
Like the `t-on` directive, it can work either on a DOM node, or on a component:
|
||||
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. 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
|
||||
<div>
|
||||
<div t-ref="someDiv"/>
|
||||
<SubComponent t-ref="someComponent"/>
|
||||
<input t-ref="input"/>
|
||||
<button t-on-click="focusInput">Click</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
In this example, the component will be able to access the `div` and the component
|
||||
inside the special `refs` variable:
|
||||
|
||||
```js
|
||||
this.refs.someDiv;
|
||||
this.refs.someComponent;
|
||||
import { useRef } from "owl/hooks";
|
||||
|
||||
class SomeComponent extends Component {
|
||||
inputRef = useRef("input");
|
||||
|
||||
focusInput() {
|
||||
this.inputRef.el.focus();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is useful for various usecases: for example, integrating with an external
|
||||
library that needs to render itself inside an actual DOM node. Or for calling
|
||||
some method on a sub component.
|
||||
|
||||
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,
|
||||
The `useRef` hook can also be used to get a reference to an instance of a sub
|
||||
component rendered by Owl. In that case, we need to access it with the `comp`
|
||||
property instead of `el`:
|
||||
|
||||
```xml
|
||||
<div t-ref="component_{{id}}"/>
|
||||
<div>
|
||||
<SubComponent t-ref="sub"/>
|
||||
<button t-on-click="doSomething">Click</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
```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
|
||||
|
||||
To make generic components, it is useful to be able for a parent component to _inject_
|
||||
@@ -1092,8 +1120,8 @@ For example, here is how we could implement an `ErrorBoundary` component:
|
||||
```
|
||||
|
||||
```js
|
||||
class ErrorBoundary extends Widget {
|
||||
state = { error: false };
|
||||
class ErrorBoundary extends Component {
|
||||
state = useState({ error: false });
|
||||
|
||||
catchError() {
|
||||
this.state.error = true;
|
||||
@@ -1120,3 +1148,79 @@ env.qweb.on("error", null, function(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.
|
||||
|
||||
### SVG Components
|
||||
|
||||
Owl components can be used to generate dynamic SVG graphs:
|
||||
|
||||
```js
|
||||
class Node extends Component {
|
||||
static template = xml`
|
||||
<g>
|
||||
<circle t-att-cx="props.x" t-att-cy="props.y" r="4" fill="black"/>
|
||||
<text t-att-x="props.x - 5" t-att-y="props.y + 18"><t t-esc="props.node.label"/></text>
|
||||
<t t-set="childx" t-value="props.x + 100"/>
|
||||
<t t-set="height" t-value="props.height/(props.node.children || []).length"/>
|
||||
<t t-foreach="props.node.children || []" t-as="child">
|
||||
<t t-set="childy" t-value="props.y + child_index*height"/>
|
||||
<line t-att-x1="props.x" t-att-y1="props.y" t-att-x2="childx" t-att-y2="childy" stroke="black" />
|
||||
<Node x="childx" y="childy" node="child" height="height"/>
|
||||
</t>
|
||||
</g>
|
||||
`;
|
||||
static components = { Node };
|
||||
}
|
||||
|
||||
class RootNode extends Component {
|
||||
static template = xml`
|
||||
<svg height="180">
|
||||
<Node node="graph" x="10" y="20" height="180"/>
|
||||
</svg>
|
||||
`;
|
||||
static components = { Node };
|
||||
graph = {
|
||||
label: "a",
|
||||
children: [
|
||||
{ label: "b" },
|
||||
{ label: "c", children: [{ label: "d" }, { label: "e" }] },
|
||||
{ label: "f", children: [{ label: "g" }] }
|
||||
]
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
This `RootNode` component will then display a live SVG representation of the
|
||||
graph described by the `graph` property. Note that there is a recursive structure
|
||||
here: the `Node` component uses itself as a subcomponent.
|
||||
|
||||
Note that since SVG needs to be handled in a specific way (its namespace needs
|
||||
to be properly set), there is a small constraint for Owl components: if an owl
|
||||
component is supposed to be a part of an svg graph, then its root node needs to
|
||||
be a `g` tag, so Owl can properly set the namespace.
|
||||
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
# 🦉 Hooks 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Example: Mouse Position](#example-mouse-position)
|
||||
- [Example: Autofocus](#example-autofocus)
|
||||
- [Reference](#reference)
|
||||
- [One Rule](#one-rule)
|
||||
- [`useState`](#usestate)
|
||||
- [`onMounted`](#onmounted)
|
||||
- [`onWillUnmount`](#onwillunmount)
|
||||
- [`onWillPatch`](#onwillpatch)
|
||||
- [`onPatched`](#onpatched)
|
||||
- [`useRef`](#useref)
|
||||
- [`useSubEnv`](#useSubEnv)
|
||||
|
||||
## 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: mouse position
|
||||
|
||||
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.
|
||||
|
||||
## Example: autofocus
|
||||
|
||||
Hooks can be combined to create the desired effect. For example, the following
|
||||
hook combines the `useRef` hook with the `onPatched` and `onMounted` functions
|
||||
to create an easy way to focus an input whenever it appears in the DOM:
|
||||
|
||||
```js
|
||||
function useAutofocus(name) {
|
||||
let ref = useRef(name);
|
||||
let isInDom = false;
|
||||
function updateFocus() {
|
||||
if (!isInDom && ref.el) {
|
||||
isInDom = true;
|
||||
ref.el.focus();
|
||||
} else if (isInDom && !ref.el) {
|
||||
isInDom = false;
|
||||
}
|
||||
}
|
||||
onPatched(updateFocus);
|
||||
onMounted(updateFocus);
|
||||
}
|
||||
```
|
||||
|
||||
This hook takes the name of a valid `t-ref` directive, which should be present
|
||||
in the template. It then checks whenever the component is mounted or patched if
|
||||
the reference is not valid, and in this case, it will focus the node element.
|
||||
This hook can be used like this:
|
||||
|
||||
```js
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<input />
|
||||
<input t-ref="myinput"/>
|
||||
</div>`;
|
||||
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
useAutofocus("myinput");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 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 user hook, but is a building block designed to help make useful
|
||||
abstractions. `onMounted` registers a callback, which will be called when the component
|
||||
is mounted (see example on top of this page).
|
||||
|
||||
### `onWillUnmount`
|
||||
|
||||
`onWillUnmount` is not an user hook, but is a building block designed to help make useful
|
||||
abstractions. `onWillUnmount` registers a callback, which will be called when the component
|
||||
is unmounted (see example on top of this page).
|
||||
|
||||
|
||||
### `onWillPatch`
|
||||
|
||||
`onWillPatch` is not an user hook, but is a building block designed to help make useful
|
||||
abstractions. `onWillPatch` registers a callback, which will be called just
|
||||
before the component patched.
|
||||
|
||||
### `onPatched`
|
||||
|
||||
`onPatched` is not an user hook, but is a building block designed to help make useful
|
||||
abstractions. `onPatched` registers a callback, which will be called just
|
||||
after the component patched.
|
||||
|
||||
### `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`.
|
||||
|
||||
### `useSubEnv`
|
||||
|
||||
The environment is sometimes useful to share some common information between
|
||||
all components. But sometimes, we want to *scope* that knowledge to a subtree.
|
||||
|
||||
For example, if we have a form view component, maybe we would like to make some
|
||||
`model` object available to all sub component, but not to the whole application.
|
||||
This is where the `useSubEnv` hook may be useful: it let a component add some
|
||||
information to the environment in a way that only the component and its children
|
||||
can access it:
|
||||
|
||||
```js
|
||||
class FormComponent extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
const model = makeModel();
|
||||
useSubEnv({ model });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `useSubEnv` takes one argument: an object which contains some key/value that
|
||||
will be added to the parent environment. Note that it will extend, not replace
|
||||
the parent environment. And of course, the parent environment will not be
|
||||
affected.
|
||||
+3
-33
@@ -11,40 +11,10 @@ For example, this code will display `update` in the console:
|
||||
```javascript
|
||||
const observer = new owl.Observer();
|
||||
observer.notifyCB = () => console.log("update");
|
||||
observer.observe(obj);
|
||||
const obj = observer.observe({ a: { b: 1 } });
|
||||
|
||||
const obj = { a: { b: 1 } };
|
||||
obj.a.b = 2;
|
||||
```
|
||||
|
||||
## Technical Limitations
|
||||
|
||||
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
|
||||
```
|
||||
The observer is implemented with the native `Proxy` object. Note that this
|
||||
means that it will not work on older browsers.
|
||||
|
||||
+3
-1
@@ -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`:
|
||||
|
||||
```javascript
|
||||
const useState = owl.hooks.useState;
|
||||
|
||||
class ClickCounter extends owl.Component {
|
||||
static template = "clickcounter";
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.state = { value: 0 };
|
||||
this.state = useState({ value: 0 });
|
||||
}
|
||||
|
||||
increment() {
|
||||
|
||||
@@ -72,7 +72,6 @@ needs. Here is a list of all Owl specific directives:
|
||||
| `t-key` | [Defining a key (to help virtual dom reconciliation)](component.md#t-key-directive) |
|
||||
| `t-on-*` | [Event handling](component.md#event-handling) |
|
||||
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
|
||||
| `t-mounted` | [Callback when a node or component is mounted](component.md#t-mounted-directive) |
|
||||
| `t-slot` | [Rendering a slot](component.md#slots) |
|
||||
| `t-model` | [Form input bindings](component.md#form-input-bindings) |
|
||||
|
||||
|
||||
@@ -9,9 +9,18 @@ build applications. Here is a complete representation of its content:
|
||||
owl
|
||||
Component
|
||||
QWeb
|
||||
useState
|
||||
core
|
||||
EventBus
|
||||
Observer
|
||||
hooks
|
||||
onMounted
|
||||
onWillUnmount
|
||||
onWillPatch
|
||||
onPatched
|
||||
useState
|
||||
useRef
|
||||
useSubEnv
|
||||
router
|
||||
Link
|
||||
RouteComponent
|
||||
@@ -29,11 +38,14 @@ owl
|
||||
whenReady
|
||||
```
|
||||
|
||||
Note that for convenience, the `useState` hook is also exported at the root of the `owl` object.
|
||||
|
||||
## Reference
|
||||
|
||||
- [Animations](animations.md)
|
||||
- [Component](component.md)
|
||||
- [Event Bus](event_bus.md)
|
||||
- [Hooks](hooks.md)
|
||||
- [Observer](observer.md)
|
||||
- [QWeb](qweb.md)
|
||||
- [Router](router.md)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
- [Development Mode](#development-mode)
|
||||
- [Playground](#playground)
|
||||
- [Benchmarks](#benchmarks)
|
||||
- [Single File Component](#single-file-component)
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -56,3 +57,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
|
||||
frameworks, and in different versions of Owl. This is a simple internal tool,
|
||||
useful to compare various performance metrics on some tasks.
|
||||
|
||||
## Single File Component
|
||||
|
||||
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
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "owl-framework",
|
||||
"version": "0.21.0",
|
||||
"version": "0.22.0",
|
||||
"description": "Odoo Web Library (OWL)",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
|
||||
+128
-124
@@ -11,7 +11,8 @@ import "./props_validation";
|
||||
* contains:
|
||||
*
|
||||
* - 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
|
||||
*/
|
||||
|
||||
@@ -33,6 +34,27 @@ export interface Env {
|
||||
[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;
|
||||
props: Props;
|
||||
promise: Promise<VNode> | null;
|
||||
// handlers?: any;
|
||||
// mountedHandlers?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -48,26 +70,24 @@ interface Internal<T extends Env, Props> {
|
||||
|
||||
// parent and children keys are obviously useful to setup the parent-children
|
||||
// relationship.
|
||||
parent: Component<T, any, any> | null;
|
||||
children: { [key: number]: Component<T, any, any> };
|
||||
parent: Component<T, any> | null;
|
||||
children: { [key: number]: Component<T, any> };
|
||||
// children mapping: from templateID to componentID. templateID identifies a
|
||||
// place in a template. The t-component directive needs it to be able to get
|
||||
// the component instance back whenever the template is rerendered.
|
||||
cmap: { [key: number]: number };
|
||||
|
||||
renderId: number;
|
||||
|
||||
// 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;
|
||||
currentFiber: Fiber<Props> | null;
|
||||
|
||||
boundHandlers: { [key: number]: any };
|
||||
observer: Observer | null;
|
||||
render: CompiledTemplate | null;
|
||||
mountedHandlers: { [key: number]: Function };
|
||||
mountedCB: Function | null;
|
||||
willUnmountCB: Function | null;
|
||||
willPatchCB: Function | null;
|
||||
patchedCB: Function | null;
|
||||
classObj: { [key: string]: boolean } | null;
|
||||
refs: { [key: string]: Component<T, any> | HTMLElement | undefined } | null;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -75,10 +95,14 @@ interface Internal<T extends Env, Props> {
|
||||
//------------------------------------------------------------------------------
|
||||
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>;
|
||||
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:
|
||||
@@ -87,20 +111,10 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
get el(): HTMLElement | null {
|
||||
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
|
||||
}
|
||||
static components = {};
|
||||
|
||||
env: T;
|
||||
state?: State;
|
||||
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
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -124,8 +138,9 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
* hand. Other components should be created automatically by the framework (with
|
||||
* 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;
|
||||
Component._current = this;
|
||||
if (defaultProps) {
|
||||
props = this.__applyDefaultProps(props, defaultProps);
|
||||
}
|
||||
@@ -135,7 +150,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
// Pro: but creating component (by a template) is always unsafe anyway
|
||||
this.props = <Props>props || <Props>{};
|
||||
let id: number = nextId++;
|
||||
let p: Component<T, any, any> | null = null;
|
||||
let p: Component<T, any> | null = null;
|
||||
if (parent instanceof Component) {
|
||||
p = parent;
|
||||
this.env = parent.env;
|
||||
@@ -169,14 +184,16 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
parent: p,
|
||||
children: {},
|
||||
cmap: {},
|
||||
renderId: 1,
|
||||
renderPromise: null,
|
||||
renderProps: props || null,
|
||||
currentFiber: null,
|
||||
boundHandlers: {},
|
||||
mountedHandlers: {},
|
||||
mountedCB: null,
|
||||
willUnmountCB: null,
|
||||
willPatchCB: null,
|
||||
patchedCB: null,
|
||||
observer: null,
|
||||
render: null,
|
||||
classObj: null
|
||||
classObj: null,
|
||||
refs: null
|
||||
};
|
||||
}
|
||||
|
||||
@@ -223,8 +240,6 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
* It is not called on the initial render. This is useful to get some
|
||||
* information which are in the DOM. For example, the current position of the
|
||||
* scrollbar
|
||||
*
|
||||
* The return value of willPatch will be given to the patched function.
|
||||
*/
|
||||
willPatch(): any {}
|
||||
|
||||
@@ -240,10 +255,8 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
* One need to be careful, because updates here will cause rerender, which in
|
||||
* turn will cause other calls to updated. So, we need to be particularly
|
||||
* careful at avoiding endless cycles.
|
||||
*
|
||||
* The snapshot parameter is the result of the call to willPatch.
|
||||
*/
|
||||
patched(snapshot: any) {}
|
||||
patched() {}
|
||||
|
||||
/**
|
||||
* willUnmount is a hook that is called each time just before a component is
|
||||
@@ -279,17 +292,20 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
if (__owl__.isMounted) {
|
||||
return;
|
||||
}
|
||||
const fiber = this.__createFiber(false, undefined, undefined, undefined);
|
||||
if (!__owl__.vnode) {
|
||||
const vnode = await this.__prepare();
|
||||
fiber.promise = this.__prepareAndRender(fiber);
|
||||
const vnode = await fiber.promise;
|
||||
if (__owl__.isDestroyed) {
|
||||
// component was destroyed before we get here...
|
||||
return;
|
||||
}
|
||||
this.__patch(vnode);
|
||||
} else if (renderBeforeRemount) {
|
||||
const patchQueue = [];
|
||||
await this.__render(false, patchQueue, undefined, undefined);
|
||||
this.__applyPatchQueue(<any[]>patchQueue);
|
||||
fiber.patchQueue.push(fiber);
|
||||
fiber.promise = this.__render(fiber);
|
||||
await fiber.promise;
|
||||
this.__applyPatchQueue(fiber);
|
||||
}
|
||||
target.appendChild(this.el!);
|
||||
|
||||
@@ -323,15 +339,15 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
if (!__owl__.isMounted) {
|
||||
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;
|
||||
await this.__render(force, patchQueue, undefined, undefined);
|
||||
|
||||
if (__owl__.isMounted && renderId === __owl__.renderId) {
|
||||
if (__owl__.isMounted && fiber === __owl__.currentFiber) {
|
||||
// we only update the vnode and the actual DOM if no other rendering
|
||||
// occurred between now and when the render method was initially called.
|
||||
this.__applyPatchQueue(<any[]>patchQueue);
|
||||
this.__applyPatchQueue(fiber);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,26 +380,6 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is the correct way to update the environment of a component. Doing
|
||||
* this will cause a full rerender of the component and its children, so this is
|
||||
* an operation that should not be done frequently.
|
||||
*
|
||||
* A good usecase for updating the environment would be to update some mostly
|
||||
* static config keys, such as a boolean to determine if we are in mobile
|
||||
* mode or not.
|
||||
*/
|
||||
async updateEnv(nextEnv: Partial<T>): Promise<void> {
|
||||
const __owl__ = this.__owl__;
|
||||
if (__owl__.parent && __owl__.parent.env === this.env) {
|
||||
this.env = Object.create(this.env);
|
||||
}
|
||||
Object.assign(this.env, nextEnv);
|
||||
if (__owl__.isMounted) {
|
||||
await this.render(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a custom event of type 'eventType' with the given 'payload' on the
|
||||
* component's el, if it exists. However, note that the event will only bubble
|
||||
@@ -405,6 +401,27 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* This method is a helper to create a fiber element.
|
||||
*/
|
||||
__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 : [],
|
||||
props: this.props,
|
||||
promise: null
|
||||
};
|
||||
fiber.rootFiber = parent ? parent.rootFiber : fiber;
|
||||
this.__owl__.currentFiber = fiber;
|
||||
return fiber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Private helper to perform a full destroy, from the point of view of an Owl
|
||||
* component. It does not remove the el (this is done only once on the top
|
||||
@@ -416,7 +433,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
* Note that it does not call the __callWillUnmount method to avoid visiting
|
||||
* all children many times.
|
||||
*/
|
||||
__destroy(parent: Component<any, any, any> | null) {
|
||||
__destroy(parent: Component<any, any> | null) {
|
||||
const __owl__ = this.__owl__;
|
||||
const isMounted = __owl__.isMounted;
|
||||
if (isMounted) {
|
||||
@@ -446,20 +463,22 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
}
|
||||
}
|
||||
__owl__.isMounted = true;
|
||||
const handlers = __owl__.mountedHandlers;
|
||||
for (let key in handlers) {
|
||||
handlers[key]();
|
||||
}
|
||||
try {
|
||||
this.mounted();
|
||||
if (__owl__.mountedCB) {
|
||||
__owl__.mountedCB()
|
||||
}
|
||||
} catch (e) {
|
||||
errorHandler(e, this);
|
||||
}
|
||||
}
|
||||
|
||||
__callWillUnmount() {
|
||||
this.willUnmount();
|
||||
const __owl__ = this.__owl__;
|
||||
if (__owl__.willUnmountCB) {
|
||||
__owl__.willUnmountCB();
|
||||
}
|
||||
this.willUnmount();
|
||||
__owl__.isMounted = false;
|
||||
const children = __owl__.children;
|
||||
for (let id in children) {
|
||||
@@ -476,12 +495,11 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
*/
|
||||
async __updateProps(
|
||||
nextProps: Props,
|
||||
forceUpdate: boolean = false,
|
||||
patchQueue?: any[],
|
||||
parentFiber: Fiber<any>,
|
||||
scope?: any,
|
||||
vars?: any
|
||||
): Promise<void> {
|
||||
const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
|
||||
const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps);
|
||||
if (shouldUpdate) {
|
||||
const defaultProps = (<any>this.constructor).defaultProps;
|
||||
if (defaultProps) {
|
||||
@@ -489,7 +507,10 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
}
|
||||
await this.willUpdateProps(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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,14 +524,18 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
__owl__.vnode = patch(target, vnode);
|
||||
}
|
||||
|
||||
__prepare(scope?: Object, vars?: any): Promise<VNode> {
|
||||
const __owl__ = this.__owl__;
|
||||
__owl__.renderProps = this.props;
|
||||
__owl__.renderPromise = this.__prepareAndRender(scope, vars);
|
||||
return __owl__.renderPromise;
|
||||
/**
|
||||
* The __prepare method is only called by the t-component directive, when a
|
||||
* subcomponent is created. It gets its scope and vars, if any, from the
|
||||
* parent template.
|
||||
*/
|
||||
__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 {
|
||||
await this.willStart();
|
||||
} catch (e) {
|
||||
@@ -544,21 +569,12 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
}
|
||||
}
|
||||
__owl__.render = qweb.render.bind(qweb, p._template);
|
||||
this.__observeState();
|
||||
|
||||
return this.__render(false, [], scope, vars);
|
||||
return this.__render(fiber);
|
||||
}
|
||||
|
||||
__render(
|
||||
force: boolean = false,
|
||||
patchQueue: any[] = [],
|
||||
scope?: Object,
|
||||
vars?: any
|
||||
): Promise<VNode> {
|
||||
__render(fiber: Fiber<Props>): Promise<VNode> {
|
||||
const __owl__ = this.__owl__;
|
||||
const promises: Promise<void>[] = [];
|
||||
const patch: any[] = [this];
|
||||
patchQueue.push(patch);
|
||||
if (__owl__.observer) {
|
||||
__owl__.observer.allowMutations = false;
|
||||
}
|
||||
@@ -567,17 +583,13 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
vnode = __owl__.render!(this, {
|
||||
promises,
|
||||
handlers: __owl__.boundHandlers,
|
||||
mountedHandlers: __owl__.mountedHandlers,
|
||||
forceUpdate: force,
|
||||
patchQueue,
|
||||
scope,
|
||||
vars
|
||||
fiber: fiber
|
||||
});
|
||||
} catch (e) {
|
||||
vnode = __owl__.vnode || h("div");
|
||||
errorHandler(e, this);
|
||||
}
|
||||
patch.push(vnode);
|
||||
fiber.vnode = vnode;
|
||||
if (__owl__.observer) {
|
||||
__owl__.observer.allowMutations = true;
|
||||
}
|
||||
@@ -625,19 +637,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).
|
||||
*
|
||||
@@ -655,29 +654,34 @@ 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
|
||||
* Component instance and vn a VNode.
|
||||
* Apply the given patch queue from a fiber.
|
||||
* 1) Call 'willPatch' 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[]) {
|
||||
let component = this;
|
||||
__applyPatchQueue(fiber: Fiber<Props>) {
|
||||
const patchQueue = fiber.patchQueue;
|
||||
let component: Component<any, any> = this;
|
||||
try {
|
||||
const patchLen = patchQueue.length;
|
||||
for (let i = 0; i < patchLen; i++) {
|
||||
const patch = patchQueue[i];
|
||||
component = patch[0];
|
||||
patch.push(patch[0].willPatch());
|
||||
component = patchQueue[i].component;
|
||||
if (component.__owl__.willPatchCB) {
|
||||
component.__owl__.willPatchCB();
|
||||
}
|
||||
component.willPatch();
|
||||
}
|
||||
for (let i = 0; i < patchLen; i++) {
|
||||
const patch = patchQueue[i];
|
||||
patch[0].__patch(patch[1]);
|
||||
const fiber = patchQueue[i];
|
||||
component = fiber.component;
|
||||
component.__patch(fiber.vnode);
|
||||
}
|
||||
for (let i = patchLen - 1; i >= 0; i--) {
|
||||
const patch = patchQueue[i];
|
||||
component = patch[0];
|
||||
patch[0].patched(patch[2]);
|
||||
component = patchQueue[i].component;
|
||||
component.patched();
|
||||
if (component.__owl__.patchedCB) {
|
||||
component.__owl__.patchedCB();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
errorHandler(e, component);
|
||||
@@ -697,7 +701,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
* If there are no such component, we destroy everything. This is better than
|
||||
* 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 qweb = component.env.qweb;
|
||||
let root = component;
|
||||
|
||||
+33
-18
@@ -1,4 +1,5 @@
|
||||
import { QWeb } from "../qweb/index";
|
||||
import { INTERP_REGEXP } from "../qweb/context";
|
||||
import { MODS_CODE } from "../qweb/extensions";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -262,9 +263,10 @@ QWeb.addDirective({
|
||||
let refExpr = "";
|
||||
let refKey: string = "";
|
||||
if (ref) {
|
||||
ctx.rootContext.shouldDefineRefs = true;
|
||||
refKey = `ref${ctx.generateID()}`;
|
||||
ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`);
|
||||
refExpr = `context.refs[${refKey}] = w${componentID};`;
|
||||
refExpr = `context.__owl__.refs[${refKey}] = w${componentID};`;
|
||||
}
|
||||
let transitionsInsertCode = "";
|
||||
if (transition) {
|
||||
@@ -272,7 +274,7 @@ QWeb.addDirective({
|
||||
}
|
||||
let finalizeComponentCode = `w${componentID}.${keepAlive ? "unmount" : "destroy"}();`;
|
||||
if (ref && !keepAlive) {
|
||||
finalizeComponentCode += `delete context.refs[${refKey}];`;
|
||||
finalizeComponentCode += `delete context.__owl__.refs[${refKey}];`;
|
||||
}
|
||||
if (transition) {
|
||||
finalizeComponentCode = `let finalize = () => {
|
||||
@@ -356,8 +358,12 @@ QWeb.addDirective({
|
||||
ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`);
|
||||
}
|
||||
let shouldProxy = false;
|
||||
if (async || keepAlive) {
|
||||
ctx.addLine(
|
||||
`const fiber${componentID} = Object.assign(Object.create(extra.fiber), {patchQueue: []});`
|
||||
);
|
||||
}
|
||||
if (async) {
|
||||
ctx.addLine(`const patchQueue${componentID} = [];`);
|
||||
ctx.addLine(
|
||||
`c${ctx.parentNode}.push(w${componentID} && w${componentID}.__owl__.pvnode || null);`
|
||||
);
|
||||
@@ -380,10 +386,12 @@ QWeb.addDirective({
|
||||
ctx.addLine(`let props${componentID} = {${propStr}};`);
|
||||
}
|
||||
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.addLine(`def${defID} = w${componentID}.__owl__.renderPromise;`);
|
||||
ctx.addIf(
|
||||
`utils.shallowEqual(props${componentID}, w${componentID}.__owl__.currentFiber.props)`
|
||||
);
|
||||
ctx.addLine(`def${defID} = w${componentID}.__owl__.currentFiber.promise;`);
|
||||
ctx.addElse();
|
||||
ctx.addLine(`w${componentID}.destroy();`);
|
||||
ctx.addLine(`w${componentID} = false;`);
|
||||
@@ -392,10 +400,16 @@ QWeb.addDirective({
|
||||
|
||||
ctx.addIf(`!w${componentID}`);
|
||||
// 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(
|
||||
`let W${componentID} = context.constructor.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}];`
|
||||
`let W${componentID} = context.constructor.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}]${dynamicFallback};`
|
||||
);
|
||||
|
||||
// maybe only do this in dev mode...
|
||||
ctx.addLine(
|
||||
`if (!W${componentID}) {throw new Error('Cannot find the definition of component "' + componentKey${componentID} + '"')}`
|
||||
@@ -441,14 +455,15 @@ QWeb.addDirective({
|
||||
}
|
||||
}
|
||||
|
||||
let scopeVars = "";
|
||||
let scopeVars;
|
||||
if (hasSlots) {
|
||||
scopeVars += ctx.scopeVars.length ? `Object.assign({}, scope)` : varDefs.length ? `{}` : "";
|
||||
if (varDefs.length) {
|
||||
scopeVars += `, {${varDefs.join(",")}}`;
|
||||
}
|
||||
let scope = ctx.scopeVars.length ? `Object.assign({}, scope)` : `{}`;
|
||||
let vars = varDefs.length ? `{${varDefs.join(",")}}` : "undefined";
|
||||
scopeVars = `${scope}, ${vars}`;
|
||||
} else {
|
||||
scopeVars = "undefined, undefined";
|
||||
}
|
||||
ctx.addLine(`def${defID} = w${componentID}.__prepare(${scopeVars});`);
|
||||
ctx.addLine(`def${defID} = w${componentID}.__prepare(extra.fiber, ${scopeVars});`);
|
||||
// hack: specify empty remove hook to prevent the node from being removed from the DOM
|
||||
let registerCode = `c${ctx.parentNode}[_${dummyID}_index]=pvnode;`;
|
||||
if (shouldProxy) {
|
||||
@@ -460,19 +475,19 @@ QWeb.addDirective({
|
||||
|
||||
ctx.addElse();
|
||||
// need to update component
|
||||
let patchQueueCode = async ? `patchQueue${componentID}` : "extra.patchQueue";
|
||||
let patchQueueCode = async || keepAlive ? `fiber${componentID}` : "extra.fiber";
|
||||
if (keepAlive) {
|
||||
// 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
|
||||
// the willPatch/patched hooks of the component in this case, so we
|
||||
// disable the patch queue
|
||||
patchQueueCode = `w${componentID}.__owl__.isMounted ? ${patchQueueCode} : []`;
|
||||
patchQueueCode = `w${componentID}.__owl__.isMounted ? extra.fiber : fiber${componentID}`;
|
||||
}
|
||||
if (QWeb.dev) {
|
||||
ctx.addLine(`utils.validateProps(w${componentID}.constructor, props${componentID})`);
|
||||
}
|
||||
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});`
|
||||
);
|
||||
let keepAliveCode = "";
|
||||
@@ -492,7 +507,7 @@ QWeb.addDirective({
|
||||
|
||||
if (async) {
|
||||
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 {
|
||||
ctx.addLine(`extra.promises.push(def${defID});`);
|
||||
|
||||
@@ -33,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) {
|
||||
// fun fact: typeof null === 'object'
|
||||
return value;
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
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
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Life cycle hooks
|
||||
// -----------------------------------------------------------------------------
|
||||
function makeLifecycleHook(method: string, reverse: boolean = false) {
|
||||
return function(cb) {
|
||||
const component: Component<any, any> = Component._current;
|
||||
if (component.__owl__[method]) {
|
||||
const current = component.__owl__[method];
|
||||
if (reverse) {
|
||||
component.__owl__[method] = function() {
|
||||
current.call(component);
|
||||
cb.call(component);
|
||||
};
|
||||
} else {
|
||||
component.__owl__[method] = function() {
|
||||
cb.call(component);
|
||||
current.call(component);
|
||||
};
|
||||
}
|
||||
} else {
|
||||
component.__owl__[method] = cb;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const onMounted = makeLifecycleHook("mountedCB", true);
|
||||
export const onWillUnmount = makeLifecycleHook("willUnmountCB");
|
||||
export const onWillPatch = makeLifecycleHook("willPatchCB");
|
||||
export const onPatched = makeLifecycleHook("patchedCB", true);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// useRef
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// useSubEnv
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
/**
|
||||
* This hook is a simple way to let components use a sub environment. Note that
|
||||
* like for all hooks, it is important that this is only called in the
|
||||
* constructor method.
|
||||
*/
|
||||
export function useSubEnv(nextEnv) {
|
||||
const component = Component._current;
|
||||
component.env = Object.assign(Object.create(component.env), nextEnv);
|
||||
}
|
||||
@@ -11,17 +11,21 @@ import { ConnectedComponent } from "./store/connected_component";
|
||||
import { Store } from "./store/store";
|
||||
import * as _utils from "./utils";
|
||||
import * as _tags from "./tags";
|
||||
import * as _hooks from "./hooks";
|
||||
import { Link } from "./router/Link";
|
||||
import { RouteComponent } from "./router/RouteComponent";
|
||||
import { Router } from "./router/Router";
|
||||
|
||||
export { Component } from "./component/component";
|
||||
export { QWeb };
|
||||
|
||||
export const useState = _hooks.useState;
|
||||
export const core = { EventBus, Observer };
|
||||
export const router = { Router, RouteComponent, Link };
|
||||
export const store = { Store, ConnectedComponent };
|
||||
export const utils = _utils;
|
||||
export const tags = _tags;
|
||||
export const hooks = _hooks;
|
||||
export const __info__ = {};
|
||||
|
||||
Object.defineProperty(__info__, "mode", {
|
||||
|
||||
@@ -244,7 +244,7 @@ QWeb.addDirective({
|
||||
varCode = `{${content}}`;
|
||||
}
|
||||
ctx.addLine(
|
||||
`this.recursiveFns['${subTemplateName}'].call(this, context, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, vars: ${varCode}, scope}));`
|
||||
`this.recursiveFns['${subTemplateName}'].call(this, context, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, fiber: {vars: ${varCode}, scope}}));`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
+6
-1
@@ -1,5 +1,6 @@
|
||||
import { compileExpr, QWebVar } from "./expression_parser";
|
||||
|
||||
export const INTERP_REGEXP = /\{\{.*?\}\}/g;
|
||||
//------------------------------------------------------------------------------
|
||||
// Compilation Context
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -19,6 +20,7 @@ export class Context {
|
||||
shouldDefineParent: boolean = false;
|
||||
shouldDefineQWeb: boolean = false;
|
||||
shouldDefineUtils: boolean = false;
|
||||
shouldDefineRefs: boolean = false;
|
||||
shouldDefineResult: boolean = true;
|
||||
shouldProtectContext: boolean = false;
|
||||
shouldTrackScope: boolean = false;
|
||||
@@ -61,6 +63,9 @@ export class Context {
|
||||
if (this.shouldDefineResult) {
|
||||
this.code.unshift(" let result;");
|
||||
}
|
||||
if (this.shouldDefineRefs) {
|
||||
this.code.unshift(" context.__owl__.refs = context.__owl__.refs || {};");
|
||||
}
|
||||
if (this.shouldDefineOwner) {
|
||||
// this is necessary to prevent some directives (t-forach for ex) to
|
||||
// pollute the rendering context by adding some keys in it.
|
||||
@@ -163,7 +168,7 @@ export class Context {
|
||||
* '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b')
|
||||
*/
|
||||
interpolate(s: string): string {
|
||||
let matches = s.match(/\{\{.*?\}\}/g);
|
||||
let matches = s.match(INTERP_REGEXP);
|
||||
if (matches && matches[0].length === s.length) {
|
||||
return `(${this.formatExpression(s.slice(2, -2))})`;
|
||||
}
|
||||
|
||||
+3
-38
@@ -79,9 +79,10 @@ QWeb.addDirective({
|
||||
name: "ref",
|
||||
priority: 95,
|
||||
atNodeCreation({ ctx, value, addNodeHook }) {
|
||||
ctx.rootContext.shouldDefineRefs = true;
|
||||
const refKey = `ref${ctx.generateID()}`;
|
||||
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
|
||||
addNodeHook("create", `context.refs[${refKey}] = n.elm;`);
|
||||
addNodeHook("create", `context.__owl__.refs[${refKey}] = n.elm;`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -182,42 +183,6 @@ QWeb.addDirective({
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-mounted
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "mounted",
|
||||
priority: 97,
|
||||
atNodeCreation({ ctx, fullName, value, nodeID, addNodeHook }) {
|
||||
ctx.rootContext.shouldDefineOwner = true;
|
||||
const eventName = fullName.slice(5);
|
||||
if (!eventName) {
|
||||
throw new Error("Missing event name with t-on directive");
|
||||
}
|
||||
let extraArgs;
|
||||
let handler = value.replace(/\(.*\)/, function(args) {
|
||||
extraArgs = args.slice(1, -1);
|
||||
return "";
|
||||
});
|
||||
let error = `(function () {throw new Error('Missing handler \\'' + '${handler}' + \`\\' when evaluating template '${ctx.templateName.replace(
|
||||
/`/g,
|
||||
"'"
|
||||
)}'\`)})()`;
|
||||
if (extraArgs) {
|
||||
ctx.addLine(
|
||||
`extra.mountedHandlers[${nodeID}] = (context['${handler}'] || ${error}).bind(owner, ${ctx.formatExpression(
|
||||
extraArgs
|
||||
)});`
|
||||
);
|
||||
} else {
|
||||
ctx.addLine(
|
||||
`extra.mountedHandlers[${nodeID}] = extra.mountedHandlers[${nodeID}] || (context['${handler}'] || ${error}).bind(owner);`
|
||||
);
|
||||
}
|
||||
addNodeHook("insert", `if (context.__owl__.isMounted) { extra.mountedHandlers[${nodeID}](); }`);
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-slot
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -254,7 +219,7 @@ QWeb.addDirective({
|
||||
const type = node.getAttribute("type");
|
||||
let handler;
|
||||
let event = fullName.includes(".lazy") ? "change" : "input";
|
||||
const expr = ctx.formatExpression(`state.${value}`);
|
||||
const expr = ctx.formatExpression(value);
|
||||
if (node.tagName === "select") {
|
||||
ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
|
||||
addNodeHook("create", `n.elm.value=${expr};`);
|
||||
|
||||
+26
-8
@@ -2,6 +2,7 @@ import { EventBus } from "../core/event_bus";
|
||||
import { h, patch, VNode } from "../vdom/index";
|
||||
import { Context } from "./context";
|
||||
import { shallowEqual } from "../utils";
|
||||
import { addNS } from "../vdom/vdom";
|
||||
|
||||
/**
|
||||
* Owl QWeb Engine
|
||||
@@ -93,11 +94,17 @@ const UTILS: Utils = {
|
||||
}
|
||||
return expr;
|
||||
},
|
||||
shallowEqual
|
||||
shallowEqual,
|
||||
addNameSpace(vnode) {
|
||||
addNS(vnode.data, vnode.children, vnode.sel);
|
||||
}
|
||||
};
|
||||
|
||||
function parseXML(xml: string): Document {
|
||||
const parser = new DOMParser();
|
||||
|
||||
// we remove comments from the xml string
|
||||
xml = xml.replace(/<!--[\s\S]*?-->/g, "");
|
||||
const doc = parser.parseFromString(xml, "text/xml");
|
||||
if (doc.getElementsByTagName("parsererror").length) {
|
||||
let msg = "Invalid XML in template.";
|
||||
@@ -241,7 +248,7 @@ export class QWeb extends EventBus {
|
||||
this._processTemplate(elem);
|
||||
const template = {
|
||||
elem,
|
||||
fn: (context, extra) => {
|
||||
fn: function(this: QWeb, context, extra) {
|
||||
const compiledFunction = this._compile(name, elem);
|
||||
template.fn = compiledFunction;
|
||||
return compiledFunction.call(this, context, extra);
|
||||
@@ -309,8 +316,8 @@ export class QWeb extends EventBus {
|
||||
* to render a full component tree, since this is an asynchronous operation.
|
||||
* This method can only render templates without components.
|
||||
*/
|
||||
renderToString(name: string, context: EvalContext = {}): string {
|
||||
const vnode = this.render(name, context);
|
||||
renderToString(name: string, context: EvalContext = {}, extra?: any): string {
|
||||
const vnode = this.render(name, context, extra);
|
||||
if (vnode.sel === undefined) {
|
||||
return vnode.text!;
|
||||
}
|
||||
@@ -344,8 +351,8 @@ export class QWeb extends EventBus {
|
||||
if (parentContext) {
|
||||
ctx.templates = Object.create(parentContext.templates);
|
||||
ctx.variables = Object.create(parentContext.variables);
|
||||
ctx.nextID = parentContext.parentNode! + 1;
|
||||
ctx.parentNode = parentContext.parentNode!;
|
||||
ctx.nextID = parentContext.nextID + 1;
|
||||
ctx.parentNode = parentContext.parentNode || ctx.nextID++;
|
||||
ctx.allowMultipleRoots = true;
|
||||
ctx.hasParentWidget = true;
|
||||
ctx.shouldDefineResult = false;
|
||||
@@ -354,12 +361,12 @@ export class QWeb extends EventBus {
|
||||
for (let v in parentContext.variables) {
|
||||
let variable = <any>parentContext.variables[v];
|
||||
if (variable.id) {
|
||||
ctx.addLine(`let ${variable.id} = extra.vars.${variable.id}`);
|
||||
ctx.addLine(`let ${variable.id} = extra.fiber.vars.${variable.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (parentContext) {
|
||||
ctx.addLine(" Object.assign(context, extra.scope);");
|
||||
ctx.addLine(" Object.assign(context, extra.fiber.scope);");
|
||||
}
|
||||
this._compileNode(elem, ctx);
|
||||
|
||||
@@ -534,6 +541,17 @@ export class QWeb extends EventBus {
|
||||
}
|
||||
|
||||
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) {
|
||||
if (directive.finalize) {
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import { Destination, RouterEnv } from "./Router";
|
||||
|
||||
type Props = Destination;
|
||||
|
||||
export class Link<Env extends RouterEnv> extends Component<Env, Props, {}> {
|
||||
export class Link<Env extends RouterEnv> extends Component<Env, Props> {
|
||||
static template = xml`
|
||||
<a t-att-class="{'router-link-active': isActive }"
|
||||
t-att-href="href"
|
||||
|
||||
@@ -1,27 +1,18 @@
|
||||
import { Component } from "../component/component";
|
||||
import { xml } from "../tags";
|
||||
|
||||
export class RouteComponent extends Component<any, {}, {}> {
|
||||
export class RouteComponent extends Component<any, {}> {
|
||||
static template = xml`
|
||||
<t t-foreach="routes" t-as="route">
|
||||
<t t-if="env.router.currentRouteName === route.name">
|
||||
<t t-component="{{route.component}}" t-props="env.router.currentParams"/>
|
||||
</t>
|
||||
<t>
|
||||
<t
|
||||
t-if="routeComponent"
|
||||
t-component="routeComponent"
|
||||
t-key="env.router.currentRouteName"
|
||||
t-props="env.router.currentParams" />
|
||||
</t>
|
||||
`;
|
||||
|
||||
routes: any[] = [];
|
||||
constructor(parent, props) {
|
||||
super(parent, props);
|
||||
const router = this.env.router;
|
||||
for (let name of router.routeIds) {
|
||||
const route = router.routes[name];
|
||||
if (route.component) {
|
||||
this.routes.push({
|
||||
name: route.name,
|
||||
component: "__component__" + route.name
|
||||
});
|
||||
}
|
||||
}
|
||||
get routeComponent(): any {
|
||||
return this.env.router.currentRoute && this.env.router.currentRoute.component;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component, Env } from "../component/component";
|
||||
import { Component, Env, Fiber } from "../component/component";
|
||||
import { VNode } from "../vdom/index";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Connect function
|
||||
@@ -6,7 +7,7 @@ import { Component, Env } from "../component/component";
|
||||
|
||||
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;
|
||||
getStore(env) {
|
||||
return env.store;
|
||||
@@ -46,10 +47,7 @@ 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
|
||||
*/
|
||||
async __prepareAndRender(
|
||||
scope?: Object,
|
||||
vars?: any
|
||||
): ReturnType<Component<any, any, any>["__prepareAndRender"]> {
|
||||
async __prepareAndRender(fiber: Fiber<P>): Promise<VNode> {
|
||||
const store = this.getStore(this.env);
|
||||
const ownProps = this.props || {};
|
||||
this.storeProps = (<any>this.constructor).mapStoreToProps(store.state, ownProps, store.getters);
|
||||
@@ -62,7 +60,7 @@ export class ConnectedComponent<T extends Env, P, S> extends Component<T, P, S>
|
||||
prevStoreProps: this.storeProps
|
||||
});
|
||||
(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
|
||||
@@ -107,9 +105,9 @@ export class ConnectedComponent<T extends Env, P, S> extends Component<T, P, S>
|
||||
return (this.__owl__ as any).renderPromise;
|
||||
}
|
||||
|
||||
async __updateProps(nextProps: P, f, p, s, v) {
|
||||
async __updateProps(nextProps: P, f, s, v) {
|
||||
this.__updateStoreProps(nextProps);
|
||||
return super.__updateProps(nextProps, f, p, s, v);
|
||||
return super.__updateProps(nextProps, f, s, v);
|
||||
}
|
||||
|
||||
__updateStoreProps(nextProps): boolean {
|
||||
|
||||
+3
-4
@@ -10,8 +10,6 @@ import { QWeb } from "./qweb/index";
|
||||
* 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:
|
||||
@@ -21,8 +19,9 @@ import { QWeb } from "./qweb/index";
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function xml(strings) {
|
||||
export function xml(strings, ...args) {
|
||||
const name = `__template__${QWeb.nextId++}`;
|
||||
QWeb.registerTemplate(name, strings[0]);
|
||||
const value = String.raw(strings, ...args);
|
||||
QWeb.registerTemplate(name, value);
|
||||
return name;
|
||||
}
|
||||
|
||||
+1
-3
@@ -48,9 +48,7 @@ export async function loadTemplates(url: string): Promise<string> {
|
||||
if (!result.ok) {
|
||||
throw new Error("Error while fetching xml templates");
|
||||
}
|
||||
let templates = await result.text();
|
||||
templates = templates.replace(/<!--[\s\S]*?-->/g, "");
|
||||
return templates;
|
||||
return await result.text();
|
||||
}
|
||||
|
||||
export function escape(str: string | number | undefined): string {
|
||||
|
||||
+7
-11
@@ -564,13 +564,17 @@ type VNodeChildElement = VNode | string | number | undefined | null;
|
||||
type ArrayOrElement<T> = T | T[];
|
||||
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";
|
||||
if (sel !== "foreignObject" && children !== undefined) {
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -17,9 +17,9 @@ exports[`animations t-transition combined with component 1`] = `
|
||||
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;
|
||||
if (w4 && w4.__owl__.currentFiber && !w4.__owl__.vnode) {
|
||||
if (utils.shallowEqual(props4, w4.__owl__.currentFiber.props)) {
|
||||
def3 = w4.__owl__.currentFiber.promise;
|
||||
} else {
|
||||
w4.destroy();
|
||||
w4 = false;
|
||||
@@ -27,17 +27,17 @@ exports[`animations t-transition combined with component 1`] = `
|
||||
}
|
||||
if (!w4) {
|
||||
let componentKey4 = \`Child\`;
|
||||
let W4 = context.constructor.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 + '\\"')}
|
||||
w4 = new W4(parent, props4);
|
||||
parent.__owl__.cmap[4] = w4.__owl__.id;
|
||||
def3 = w4.__prepare();
|
||||
def3 = w4.__prepare(extra.fiber, undefined, undefined);
|
||||
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();
|
||||
};
|
||||
utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
||||
} 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;});
|
||||
}
|
||||
extra.promises.push(def3);
|
||||
@@ -63,9 +63,9 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
|
||||
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;
|
||||
if (w4 && w4.__owl__.currentFiber && !w4.__owl__.vnode) {
|
||||
if (utils.shallowEqual(props4, w4.__owl__.currentFiber.props)) {
|
||||
def3 = w4.__owl__.currentFiber.promise;
|
||||
} else {
|
||||
w4.destroy();
|
||||
w4 = false;
|
||||
@@ -73,17 +73,17 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
|
||||
}
|
||||
if (!w4) {
|
||||
let componentKey4 = \`Child\`;
|
||||
let W4 = context.constructor.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 + '\\"')}
|
||||
w4 = new W4(parent, props4);
|
||||
parent.__owl__.cmap[4] = w4.__owl__.id;
|
||||
def3 = w4.__prepare();
|
||||
def3 = w4.__prepare(extra.fiber, undefined, undefined);
|
||||
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();
|
||||
};
|
||||
utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
||||
} 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;});
|
||||
}
|
||||
extra.promises.push(def3);
|
||||
|
||||
+11
-36
@@ -1,10 +1,10 @@
|
||||
import { Component, Env } from "../src/component/component";
|
||||
import { QWeb } from "../src/qweb/index";
|
||||
import { useState, useRef } from "../src/hooks";
|
||||
import {
|
||||
makeDeferred,
|
||||
makeTestFixture,
|
||||
makeTestEnv,
|
||||
nextTick,
|
||||
patchNextFrame,
|
||||
renderToDOM,
|
||||
unpatchNextFrame
|
||||
@@ -36,7 +36,7 @@ afterEach(() => {
|
||||
fixture.remove();
|
||||
});
|
||||
|
||||
class Widget extends Component<any, any, any> {}
|
||||
class Widget extends Component<any, any> {}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Tests
|
||||
@@ -106,7 +106,7 @@ describe("animations", () => {
|
||||
`<div><span t-if="!state.hide" t-transition="chimay">blue</span></div>`
|
||||
);
|
||||
class TestWidget extends Widget {
|
||||
state = { hide: false };
|
||||
state = useState({ hide: false });
|
||||
}
|
||||
const widget = new TestWidget(env);
|
||||
|
||||
@@ -148,7 +148,8 @@ describe("animations", () => {
|
||||
`<div><span t-ref="span" t-transition="chimay">blue</span></div>`
|
||||
);
|
||||
class TestWidget extends Widget {
|
||||
state = { hide: false };
|
||||
state = useState({ hide: false });
|
||||
span = useRef("span");
|
||||
}
|
||||
const widget = new TestWidget(env);
|
||||
|
||||
@@ -163,7 +164,7 @@ describe("animations", () => {
|
||||
});
|
||||
await widget.mount(fixture);
|
||||
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");
|
||||
await def; // wait for the mocked repaint to be done
|
||||
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||
@@ -217,7 +218,7 @@ describe("animations", () => {
|
||||
class Child extends Widget {}
|
||||
class Parent extends Widget {
|
||||
static components = { Child: Child };
|
||||
state = { display: true };
|
||||
state = useState({ display: true });
|
||||
}
|
||||
const widget = new Parent(env);
|
||||
|
||||
@@ -263,28 +264,6 @@ describe("animations", () => {
|
||||
expect(fixture.innerHTML).toBe("<div></div>");
|
||||
});
|
||||
|
||||
test("t-transition combined with t-mounted", async () => {
|
||||
env.qweb.addTemplate(
|
||||
"TestWidget",
|
||||
`<div><span t-if="state.flag" t-mounted="f" t-transition="chimay">blue</span></div>`
|
||||
);
|
||||
class TestWidget extends Widget {
|
||||
state = { flag: false };
|
||||
f() {}
|
||||
}
|
||||
const widget = new TestWidget(env);
|
||||
widget.f = jest.fn();
|
||||
await widget.mount(fixture);
|
||||
|
||||
patchNextFrame(cb => cb());
|
||||
expect(widget.f).toHaveBeenCalledTimes(0);
|
||||
|
||||
widget.state.flag = true;
|
||||
await nextTick();
|
||||
expect(widget.f).toHaveBeenCalledTimes(1);
|
||||
unpatchNextFrame();
|
||||
});
|
||||
|
||||
test("t-transition, remove and re-add before transitionend", async () => {
|
||||
expect.assertions(11);
|
||||
|
||||
@@ -297,10 +276,8 @@ describe("animations", () => {
|
||||
</templates>`
|
||||
);
|
||||
class Parent extends Widget {
|
||||
constructor(parent) {
|
||||
super(parent);
|
||||
this.state = { flag: false };
|
||||
}
|
||||
state = useState({ flag: false });
|
||||
|
||||
toggle() {
|
||||
this.state.flag = !this.state.flag;
|
||||
}
|
||||
@@ -357,10 +334,8 @@ describe("animations", () => {
|
||||
class Child extends Widget {}
|
||||
class Parent extends Widget {
|
||||
static components = { Child };
|
||||
constructor(parent) {
|
||||
super(parent);
|
||||
this.state = { flag: false };
|
||||
}
|
||||
state = useState({ flag: false });
|
||||
|
||||
toggle() {
|
||||
this.state.flag = !this.state.flag;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,9 +21,9 @@ exports[`props validation props are validated in dev mode (code snapshot) 1`] =
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let props4 = {message:1};
|
||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
||||
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
||||
def3 = w4.__owl__.renderPromise;
|
||||
if (w4 && w4.__owl__.currentFiber && !w4.__owl__.vnode) {
|
||||
if (utils.shallowEqual(props4, w4.__owl__.currentFiber.props)) {
|
||||
def3 = w4.__owl__.currentFiber.promise;
|
||||
} else {
|
||||
w4.destroy();
|
||||
w4 = false;
|
||||
@@ -31,16 +31,16 @@ exports[`props validation props are validated in dev mode (code snapshot) 1`] =
|
||||
}
|
||||
if (!w4) {
|
||||
let componentKey4 = \`Child\`;
|
||||
let W4 = context.constructor.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 + '\\"')}
|
||||
utils.validateProps(W4, props4)
|
||||
w4 = new W4(parent, props4);
|
||||
parent.__owl__.cmap[4] = w4.__owl__.id;
|
||||
def3 = w4.__prepare();
|
||||
def3 = w4.__prepare(extra.fiber, undefined, undefined);
|
||||
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 {
|
||||
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;});
|
||||
}
|
||||
extra.promises.push(def3);
|
||||
|
||||
+284
-352
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,7 @@ afterEach(() => {
|
||||
QWeb.dev = dev;
|
||||
});
|
||||
|
||||
class Widget extends Component<any, any, any> {}
|
||||
class Widget extends Component<any, any> {}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Tests
|
||||
@@ -334,7 +334,8 @@ describe("default props", () => {
|
||||
const w = new TestWidget(env, { p: 1 });
|
||||
await w.mount(fixture);
|
||||
expect(fixture.innerHTML).toMatchSnapshot();
|
||||
await w.__updateProps({});
|
||||
const fiber = w.__createFiber(false, undefined, undefined, undefined);
|
||||
await w.__updateProps({}, fiber);
|
||||
await w.render();
|
||||
expect(w.props.p).toBe(4);
|
||||
expect(fixture.innerHTML).toMatchSnapshot();
|
||||
|
||||
+8
-3
@@ -99,10 +99,15 @@ export function renderToDOM(
|
||||
* Note that the result of renderToString is guaranteed to be the same as the
|
||||
* one from QWeb.
|
||||
*/
|
||||
export function renderToString(qweb: QWeb, t: string, context: EvalContext = {}): string {
|
||||
const node = renderToDOM(qweb, t, context);
|
||||
export function renderToString(
|
||||
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;
|
||||
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");
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -0,0 +1,359 @@
|
||||
import { makeTestEnv, makeTestFixture, nextTick } from "./helpers";
|
||||
import { Component, Env } from "../src/component/component";
|
||||
import {
|
||||
useState,
|
||||
onMounted,
|
||||
onWillUnmount,
|
||||
useRef,
|
||||
onPatched,
|
||||
onWillPatch,
|
||||
useSubEnv
|
||||
} 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>");
|
||||
});
|
||||
|
||||
test("can use onPatched, onWillPatch", async () => {
|
||||
const steps: string[] = [];
|
||||
function useMyHook() {
|
||||
onWillPatch(() => {
|
||||
steps.push("willPatch");
|
||||
});
|
||||
onPatched(() => {
|
||||
steps.push("patched");
|
||||
});
|
||||
}
|
||||
|
||||
class MyComponent extends Component<any, any> {
|
||||
static template = xml`<div><t t-if="state.flag">hey</t></div>`;
|
||||
state = useState({ flag: true });
|
||||
|
||||
constructor(env) {
|
||||
super(env);
|
||||
useMyHook();
|
||||
}
|
||||
}
|
||||
|
||||
const component = new MyComponent(env);
|
||||
await component.mount(fixture);
|
||||
expect(component).not.toHaveProperty("patched");
|
||||
expect(component).not.toHaveProperty("willPatch");
|
||||
expect(steps).toEqual([]);
|
||||
|
||||
expect(fixture.innerHTML).toBe("<div>hey</div>");
|
||||
component.state.flag = false;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div></div>");
|
||||
|
||||
expect(steps).toEqual(["willPatch", "patched"]);
|
||||
});
|
||||
|
||||
test("patched, willPatch, onPatched, onWillPatch order", async () => {
|
||||
const steps: string[] = [];
|
||||
function useMyHook() {
|
||||
onPatched(() => {
|
||||
steps.push("hook:patched");
|
||||
});
|
||||
onWillPatch(() => {
|
||||
steps.push("hook:willPatch");
|
||||
});
|
||||
}
|
||||
class MyComponent extends Component<any, any> {
|
||||
static template = xml`<div><t t-if="state.flag">hey</t></div>`;
|
||||
state = useState({ flag: true });
|
||||
|
||||
constructor(env) {
|
||||
super(env);
|
||||
useMyHook();
|
||||
}
|
||||
willPatch() {
|
||||
steps.push("comp:willPatch");
|
||||
}
|
||||
patched() {
|
||||
steps.push("comp:patched");
|
||||
}
|
||||
}
|
||||
const component = new MyComponent(env);
|
||||
await component.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div>hey</div>");
|
||||
component.state.flag = false;
|
||||
await nextTick();
|
||||
|
||||
expect(steps).toEqual(["hook:willPatch", "comp:willPatch", "comp:patched", "hook:patched"]);
|
||||
});
|
||||
|
||||
test("two different call to willPatch/patched should work", async () => {
|
||||
const steps: string[] = [];
|
||||
function useMyHook(i) {
|
||||
onPatched(() => {
|
||||
steps.push("hook:patched" + i);
|
||||
});
|
||||
onWillPatch(() => {
|
||||
steps.push("hook:willPatch" + i);
|
||||
});
|
||||
}
|
||||
class MyComponent extends Component<any, any> {
|
||||
static template = xml`<div>hey<t t-esc="state.value"/></div>`;
|
||||
state = useState({ value: 1 });
|
||||
constructor(env) {
|
||||
super(env);
|
||||
useMyHook(1);
|
||||
useMyHook(2);
|
||||
}
|
||||
}
|
||||
const component = new MyComponent(env);
|
||||
await component.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div>hey1</div>");
|
||||
component.state.value++;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div>hey2</div>");
|
||||
|
||||
expect(steps).toEqual(["hook:willPatch2", "hook:willPatch1", "hook:patched1", "hook:patched2"]);
|
||||
});
|
||||
|
||||
describe("autofocus hook", () => {
|
||||
function useAutofocus(name) {
|
||||
let ref = useRef(name);
|
||||
let isInDom = false;
|
||||
function updateFocus() {
|
||||
if (!isInDom && ref.el) {
|
||||
isInDom = true;
|
||||
ref.el.focus();
|
||||
} else if (isInDom && !ref.el) {
|
||||
isInDom = false;
|
||||
}
|
||||
}
|
||||
onPatched(updateFocus);
|
||||
onMounted(updateFocus);
|
||||
}
|
||||
|
||||
test("simple input", async () => {
|
||||
class SomeComponent extends Component<any, any> {
|
||||
static template = xml`
|
||||
<div>
|
||||
<input t-ref="input1"/>
|
||||
<input t-ref="input2"/>
|
||||
</div>`;
|
||||
|
||||
constructor(env) {
|
||||
super(env);
|
||||
useAutofocus("input2");
|
||||
}
|
||||
}
|
||||
|
||||
const component = new SomeComponent(env);
|
||||
await component.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><input><input></div>");
|
||||
const input2 = fixture.querySelectorAll("input")[1];
|
||||
expect(input2).toBe(document.activeElement);
|
||||
});
|
||||
|
||||
test("input in a t-if", async () => {
|
||||
class SomeComponent extends Component<any, any> {
|
||||
static template = xml`
|
||||
<div>
|
||||
<input t-ref="input1"/>
|
||||
<t t-if="state.flag"><input t-ref="input2"/></t>
|
||||
</div>`;
|
||||
|
||||
state = useState({ flag: false });
|
||||
constructor(env) {
|
||||
super(env);
|
||||
useAutofocus("input2");
|
||||
}
|
||||
}
|
||||
|
||||
const component = new SomeComponent(env);
|
||||
await component.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><input></div>");
|
||||
expect(document.activeElement).toBe(document.body);
|
||||
|
||||
component.state.flag = true;
|
||||
await nextTick();
|
||||
const input2 = fixture.querySelectorAll("input")[1];
|
||||
expect(input2).toBe(document.activeElement);
|
||||
});
|
||||
});
|
||||
|
||||
test("can use sub env", async () => {
|
||||
class TestComponent extends Component<any, any> {
|
||||
static template = xml`<div><t t-esc="env.val"/></div>`;
|
||||
constructor(env) {
|
||||
super(env);
|
||||
useSubEnv({ val: 3 });
|
||||
}
|
||||
}
|
||||
const component = new TestComponent(env);
|
||||
await component.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div>3</div>");
|
||||
expect(env).not.toHaveProperty("val");
|
||||
expect(component.env).toHaveProperty("val");
|
||||
});
|
||||
|
||||
test("parent and child env", async () => {
|
||||
class Child extends Component<any, any> {
|
||||
static template = xml`<div><t t-esc="env.val"/></div>`;
|
||||
constructor(env) {
|
||||
super(env);
|
||||
useSubEnv({ val: 5 });
|
||||
}
|
||||
}
|
||||
|
||||
class Parent extends Component<any, any> {
|
||||
static template = xml`<div><t t-esc="env.val"/><Child/></div>`;
|
||||
static components = { Child}
|
||||
constructor(env) {
|
||||
super(env);
|
||||
useSubEnv({ val: 3 });
|
||||
}
|
||||
}
|
||||
const component = new Parent(env);
|
||||
await component.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe( "<div>3<div>5</div></div>");
|
||||
});
|
||||
});
|
||||
@@ -676,6 +676,54 @@ exports[`misc global 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`properly support svg add proper namespace to g tags 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let utils = this.constructor.utils;
|
||||
var h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('g', p1, c1);
|
||||
result = vn1;
|
||||
var _2 = '50';
|
||||
var _3 = '50';
|
||||
var _4 = '4';
|
||||
var _5 = 'green';
|
||||
var _6 = '1';
|
||||
var _7 = 'yellow';
|
||||
let c8 = [], p8 = {key:8,attrs:{cx: _2,cy: _3,r: _4,stroke: _5,\\"stroke-width\\": _6,fill: _7}};
|
||||
var vn8 = h('circle', p8, c8);
|
||||
c1.push(vn8);
|
||||
c1.push({text: \` \`});
|
||||
utils.addNameSpace(vn1);
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`properly support svg add proper namespace to svg 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let utils = this.constructor.utils;
|
||||
var h = this.h;
|
||||
var _1 = '100px';
|
||||
var _2 = '90px';
|
||||
let c3 = [], p3 = {key:3,attrs:{width: _1,height: _2}};
|
||||
var vn3 = h('svg', p3, c3);
|
||||
result = vn3;
|
||||
var _4 = '50';
|
||||
var _5 = '50';
|
||||
var _6 = '4';
|
||||
var _7 = 'green';
|
||||
var _8 = '1';
|
||||
var _9 = 'yellow';
|
||||
let c10 = [], p10 = {key:10,attrs:{cx: _4,cy: _5,r: _6,stroke: _7,\\"stroke-width\\": _8,fill: _9}};
|
||||
var vn10 = h('circle', p10, c10);
|
||||
c3.push(vn10);
|
||||
c3.push({text: \` \`});
|
||||
utils.addNameSpace(vn3);
|
||||
return vn3;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`special cases for some boolean html attributes/properties input type= checkbox, with t-att-checked 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
@@ -768,6 +816,18 @@ exports[`static templates empty div 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`static templates ignore comments 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
var h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
result = vn1;
|
||||
c1.push({text: \`hello owl\`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`static templates simple dynamic value 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
@@ -872,7 +932,7 @@ exports[`t-call (template calling recursive template, part 1 1`] = `
|
||||
c1.push(vn2);
|
||||
c2.push({text: \`hey\`});
|
||||
if (false) {
|
||||
this.recursiveFns['__3'].call(this, context, Object.assign({}, extra, {parentNode: c1, vars: {}, scope}));
|
||||
this.recursiveFns['__3'].call(this, context, Object.assign({}, extra, {parentNode: c1, fiber: {vars: {}, scope}}));
|
||||
}
|
||||
return vn1;
|
||||
}"
|
||||
@@ -884,16 +944,16 @@ exports[`t-call (template calling recursive template, part 1 2`] = `
|
||||
let owner = context;
|
||||
var h = this.h;
|
||||
let c1 = extra.parentNode;
|
||||
Object.assign(context, extra.scope);
|
||||
let c2 = [], p2 = {key:2};
|
||||
var vn2 = h('div', p2, c2);
|
||||
c1.push(vn2);
|
||||
let c3 = [], p3 = {key:3};
|
||||
var vn3 = h('span', p3, c3);
|
||||
c2.push(vn3);
|
||||
c3.push({text: \`hey\`});
|
||||
Object.assign(context, extra.fiber.scope);
|
||||
let c5 = [], p5 = {key:5};
|
||||
var vn5 = h('div', p5, c5);
|
||||
c1.push(vn5);
|
||||
let c6 = [], p6 = {key:6};
|
||||
var vn6 = h('span', p6, c6);
|
||||
c5.push(vn6);
|
||||
c6.push({text: \`hey\`});
|
||||
if (false) {
|
||||
this.recursiveFns['__3'].call(this, context, Object.assign({}, extra, {parentNode: c2, vars: {}, scope}));
|
||||
this.recursiveFns['__3'].call(this, context, Object.assign({}, extra, {parentNode: c5, fiber: {vars: {}, scope}}));
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -939,7 +999,7 @@ exports[`t-call (template calling recursive template, part 2 1`] = `
|
||||
scope.subtree = context.subtree;
|
||||
context.subtree_value = _8[i];
|
||||
scope.subtree_value = context.subtree_value;
|
||||
this.recursiveFns['__10'].call(this, context, Object.assign({}, extra, {parentNode: c3, vars: {_v0: context['subtree']}, scope}));
|
||||
this.recursiveFns['__10'].call(this, context, Object.assign({}, extra, {parentNode: c3, fiber: {vars: {_v0: context['subtree']}, scope}}));
|
||||
}
|
||||
}
|
||||
return vn1;
|
||||
@@ -954,38 +1014,38 @@ exports[`t-call (template calling recursive template, part 2 2`] = `
|
||||
const scope = Object.create(null);
|
||||
var h = this.h;
|
||||
let c3 = extra.parentNode;
|
||||
let _v0 = extra.vars._v0
|
||||
Object.assign(context, extra.scope);
|
||||
let c4 = [], p4 = {key:4};
|
||||
var vn4 = h('div', p4, c4);
|
||||
c3.push(vn4);
|
||||
let c5 = [], p5 = {key:5};
|
||||
var vn5 = h('p', p5, c5);
|
||||
c4.push(vn5);
|
||||
var _6 = _v0.val;
|
||||
if (_6 || _6 === 0) {
|
||||
c5.push({text: _6});
|
||||
let _v0 = extra.fiber.vars._v0
|
||||
Object.assign(context, extra.fiber.scope);
|
||||
let c12 = [], p12 = {key:12};
|
||||
var vn12 = h('div', p12, c12);
|
||||
c3.push(vn12);
|
||||
let c13 = [], p13 = {key:13};
|
||||
var vn13 = h('p', p13, c13);
|
||||
c12.push(vn13);
|
||||
var _14 = _v0.val;
|
||||
if (_14 || _14 === 0) {
|
||||
c13.push({text: _14});
|
||||
}
|
||||
var _7 = _v0.children||[];
|
||||
if (!_7) { throw new Error('QWeb error: Invalid loop expression')}
|
||||
var _8 = _9 = _7;
|
||||
if (!(_7 instanceof Array)) {
|
||||
_8 = Object.keys(_7);
|
||||
_9 = Object.values(_7);
|
||||
var _15 = _v0.children||[];
|
||||
if (!_15) { throw new Error('QWeb error: Invalid loop expression')}
|
||||
var _16 = _17 = _15;
|
||||
if (!(_15 instanceof Array)) {
|
||||
_16 = Object.keys(_15);
|
||||
_17 = Object.values(_15);
|
||||
}
|
||||
var _length8 = _8.length;
|
||||
for (let i = 0; i < _length8; i++) {
|
||||
var _length16 = _16.length;
|
||||
for (let i = 0; i < _length16; i++) {
|
||||
context.subtree_first = i === 0;
|
||||
scope.subtree_first = context.subtree_first;
|
||||
context.subtree_last = i === _length8 - 1;
|
||||
context.subtree_last = i === _length16 - 1;
|
||||
scope.subtree_last = context.subtree_last;
|
||||
context.subtree_index = i;
|
||||
scope.subtree_index = context.subtree_index;
|
||||
context.subtree = _8[i];
|
||||
context.subtree = _16[i];
|
||||
scope.subtree = context.subtree;
|
||||
context.subtree_value = _9[i];
|
||||
context.subtree_value = _17[i];
|
||||
scope.subtree_value = context.subtree_value;
|
||||
this.recursiveFns['__10'].call(this, context, Object.assign({}, extra, {parentNode: c4, vars: {_v0: context['subtree']}, scope}));
|
||||
this.recursiveFns['__10'].call(this, context, Object.assign({}, extra, {parentNode: c12, fiber: {vars: {_v0: context['subtree']}, scope}}));
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -1031,7 +1091,7 @@ exports[`t-call (template calling recursive template, part 3 1`] = `
|
||||
scope.subtree = context.subtree;
|
||||
context.subtree_value = _8[i];
|
||||
scope.subtree_value = context.subtree_value;
|
||||
this.recursiveFns['__10'].call(this, context, Object.assign({}, extra, {parentNode: c3, vars: {_v0: context['subtree']}, scope}));
|
||||
this.recursiveFns['__10'].call(this, context, Object.assign({}, extra, {parentNode: c3, fiber: {vars: {_v0: context['subtree']}, scope}}));
|
||||
}
|
||||
}
|
||||
return vn1;
|
||||
@@ -1046,38 +1106,38 @@ exports[`t-call (template calling recursive template, part 3 2`] = `
|
||||
const scope = Object.create(null);
|
||||
var h = this.h;
|
||||
let c3 = extra.parentNode;
|
||||
let _v0 = extra.vars._v0
|
||||
Object.assign(context, extra.scope);
|
||||
let c4 = [], p4 = {key:4};
|
||||
var vn4 = h('div', p4, c4);
|
||||
c3.push(vn4);
|
||||
let c5 = [], p5 = {key:5};
|
||||
var vn5 = h('p', p5, c5);
|
||||
c4.push(vn5);
|
||||
var _6 = _v0.val;
|
||||
if (_6 || _6 === 0) {
|
||||
c5.push({text: _6});
|
||||
let _v0 = extra.fiber.vars._v0
|
||||
Object.assign(context, extra.fiber.scope);
|
||||
let c12 = [], p12 = {key:12};
|
||||
var vn12 = h('div', p12, c12);
|
||||
c3.push(vn12);
|
||||
let c13 = [], p13 = {key:13};
|
||||
var vn13 = h('p', p13, c13);
|
||||
c12.push(vn13);
|
||||
var _14 = _v0.val;
|
||||
if (_14 || _14 === 0) {
|
||||
c13.push({text: _14});
|
||||
}
|
||||
var _7 = _v0.children||[];
|
||||
if (!_7) { throw new Error('QWeb error: Invalid loop expression')}
|
||||
var _8 = _9 = _7;
|
||||
if (!(_7 instanceof Array)) {
|
||||
_8 = Object.keys(_7);
|
||||
_9 = Object.values(_7);
|
||||
var _15 = _v0.children||[];
|
||||
if (!_15) { throw new Error('QWeb error: Invalid loop expression')}
|
||||
var _16 = _17 = _15;
|
||||
if (!(_15 instanceof Array)) {
|
||||
_16 = Object.keys(_15);
|
||||
_17 = Object.values(_15);
|
||||
}
|
||||
var _length8 = _8.length;
|
||||
for (let i = 0; i < _length8; i++) {
|
||||
var _length16 = _16.length;
|
||||
for (let i = 0; i < _length16; i++) {
|
||||
context.subtree_first = i === 0;
|
||||
scope.subtree_first = context.subtree_first;
|
||||
context.subtree_last = i === _length8 - 1;
|
||||
context.subtree_last = i === _length16 - 1;
|
||||
scope.subtree_last = context.subtree_last;
|
||||
context.subtree_index = i;
|
||||
scope.subtree_index = context.subtree_index;
|
||||
context.subtree = _8[i];
|
||||
context.subtree = _16[i];
|
||||
scope.subtree = context.subtree;
|
||||
context.subtree_value = _9[i];
|
||||
context.subtree_value = _17[i];
|
||||
scope.subtree_value = context.subtree_value;
|
||||
this.recursiveFns['__10'].call(this, context, Object.assign({}, extra, {parentNode: c4, vars: {_v0: context['subtree']}, scope}));
|
||||
this.recursiveFns['__10'].call(this, context, Object.assign({}, extra, {parentNode: c12, fiber: {vars: {_v0: context['subtree']}, scope}}));
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -1118,6 +1178,21 @@ exports[`t-call (template calling t-call with t-if 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-call (template calling t-call, global templates 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
var h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
result = vn1;
|
||||
let c2 = [], p2 = {key:2};
|
||||
var vn2 = h('span', p2, c2);
|
||||
c1.push(vn2);
|
||||
c2.push({text: \`desk\`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-call (template calling with unused body 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
@@ -1840,6 +1915,7 @@ exports[`t-raw variable 1`] = `
|
||||
exports[`t-ref can get a dynamic ref on a node 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
context.__owl__.refs = context.__owl__.refs || {};
|
||||
var h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
@@ -1850,7 +1926,7 @@ exports[`t-ref can get a dynamic ref on a node 1`] = `
|
||||
const ref3 = \`myspan\${context['id']}\`;
|
||||
p2.hook = {
|
||||
create: (_, n) => {
|
||||
context.refs[ref3] = n.elm;
|
||||
context.__owl__.refs[ref3] = n.elm;
|
||||
},
|
||||
};
|
||||
return vn1;
|
||||
@@ -1860,6 +1936,7 @@ exports[`t-ref can get a dynamic ref on a node 1`] = `
|
||||
exports[`t-ref can get a ref on a node 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
context.__owl__.refs = context.__owl__.refs || {};
|
||||
var h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
@@ -1870,7 +1947,7 @@ exports[`t-ref can get a ref on a node 1`] = `
|
||||
const ref3 = \`myspan\`;
|
||||
p2.hook = {
|
||||
create: (_, n) => {
|
||||
context.refs[ref3] = n.elm;
|
||||
context.__owl__.refs[ref3] = n.elm;
|
||||
},
|
||||
};
|
||||
return vn1;
|
||||
@@ -1880,6 +1957,7 @@ exports[`t-ref can get a ref on a node 1`] = `
|
||||
exports[`t-ref refs in a loop 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
context.__owl__.refs = context.__owl__.refs || {};
|
||||
context = Object.create(context);
|
||||
var h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
@@ -1906,7 +1984,7 @@ exports[`t-ref refs in a loop 1`] = `
|
||||
const ref6 = (context['item']);
|
||||
p5.hook = {
|
||||
create: (_, n) => {
|
||||
context.refs[ref6] = n.elm;
|
||||
context.__owl__.refs[ref6] = n.elm;
|
||||
},
|
||||
};
|
||||
var _7 = context['item'];
|
||||
|
||||
+42
-5
@@ -49,6 +49,11 @@ describe("static templates", () => {
|
||||
qweb.addTemplate("test", "<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", () => {
|
||||
@@ -631,7 +636,9 @@ describe("t-call (template calling", () => {
|
||||
`);
|
||||
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 })).toBe(expected);
|
||||
expect(renderToString(qweb, "Parent", { root }, { fiber: { vars: {}, scope: {} } })).toBe(
|
||||
expected
|
||||
);
|
||||
const recursiveFn = Object.values(qweb.recursiveFns)[0];
|
||||
expect(recursiveFn.toString()).toMatchSnapshot();
|
||||
});
|
||||
@@ -658,10 +665,17 @@ describe("t-call (template calling", () => {
|
||||
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 })).toBe(expected);
|
||||
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", () => {
|
||||
@@ -1078,14 +1092,14 @@ describe("t-ref", () => {
|
||||
test("can get a ref on a node", () => {
|
||||
qweb.addTemplate("test", `<div><span t-ref="myspan"/></div>`);
|
||||
let refs: any = {};
|
||||
renderToDOM(qweb, "test", { refs });
|
||||
renderToDOM(qweb, "test", { __owl__: { refs } });
|
||||
expect(refs.myspan.tagName).toBe("SPAN");
|
||||
});
|
||||
|
||||
test("can get a dynamic ref on a node", () => {
|
||||
qweb.addTemplate("test", `<div><span t-ref="myspan{{id}}"/></div>`);
|
||||
let refs: any = {};
|
||||
renderToDOM(qweb, "test", { refs, id: 3 });
|
||||
renderToDOM(qweb, "test", { id: 3, __owl__: { refs } });
|
||||
expect(refs.myspan3.tagName).toBe("SPAN");
|
||||
});
|
||||
|
||||
@@ -1100,7 +1114,7 @@ describe("t-ref", () => {
|
||||
</div>`
|
||||
);
|
||||
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"]);
|
||||
});
|
||||
});
|
||||
@@ -1298,3 +1312,26 @@ describe("global template registration", () => {
|
||||
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>`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,7 +30,7 @@ describe("Link component", () => {
|
||||
</div>
|
||||
</templates>
|
||||
`);
|
||||
class App extends Component<any, any, any> {
|
||||
class App extends Component<any, any> {
|
||||
static components = { Link: Link };
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ describe("Link component", () => {
|
||||
</div>
|
||||
</templates>
|
||||
`);
|
||||
class App extends Component<any, any, any> {
|
||||
class App extends Component<any, any> {
|
||||
static components = { Link: Link };
|
||||
}
|
||||
|
||||
|
||||
@@ -32,9 +32,9 @@ describe("RouteComponent", () => {
|
||||
<span t-name="Users">Users</span>
|
||||
</templates>
|
||||
`);
|
||||
class About extends Component<any, any, any> {}
|
||||
class Users extends Component<any, any, any> {}
|
||||
class App extends Component<any, any, any> {
|
||||
class About extends Component<any, any> {}
|
||||
class Users extends Component<any, any> {}
|
||||
class App extends Component<any, any> {
|
||||
static components = { RouteComponent };
|
||||
}
|
||||
|
||||
@@ -64,8 +64,8 @@ describe("RouteComponent", () => {
|
||||
<span t-name="Book">Book <t t-esc="props.title"/></span>
|
||||
</templates>
|
||||
`);
|
||||
class Book extends Component<any, any, any> {}
|
||||
class App extends Component<any, any, any> {
|
||||
class Book extends Component<any, any> {}
|
||||
class App extends Component<any, any> {
|
||||
static components = { RouteComponent };
|
||||
}
|
||||
|
||||
@@ -86,12 +86,12 @@ describe("RouteComponent", () => {
|
||||
<span t-name="Book">Book <t t-esc="props.title"/>|<t t-esc="incVal"/></span>
|
||||
</templates>
|
||||
`);
|
||||
class Book extends Component<any, any, any> {
|
||||
class Book extends Component<any, any> {
|
||||
get incVal() {
|
||||
return this.props.val + 1;
|
||||
}
|
||||
}
|
||||
class App extends Component<any, any, any> {
|
||||
class App extends Component<any, any> {
|
||||
static components = { RouteComponent };
|
||||
}
|
||||
|
||||
|
||||
@@ -8,52 +8,37 @@ exports[`RouteComponent can render simple cases 1`] = `
|
||||
let parent = context;
|
||||
let owner = context;
|
||||
let result;
|
||||
context = Object.create(context);
|
||||
var h = this.h;
|
||||
var _1 = context['routes'];
|
||||
if (!_1) { throw new Error('QWeb error: Invalid loop expression')}
|
||||
var _2 = _3 = _1;
|
||||
if (!(_1 instanceof Array)) {
|
||||
_2 = Object.keys(_1);
|
||||
_3 = Object.values(_1);
|
||||
}
|
||||
var _length2 = _2.length;
|
||||
for (let i = 0; i < _length2; i++) {
|
||||
context.route_first = i === 0;
|
||||
context.route_last = i === _length2 - 1;
|
||||
context.route_index = i;
|
||||
context.route = _2[i];
|
||||
context.route_value = _3[i];
|
||||
if (context['env'].router.currentRouteName===context['route'].name) {
|
||||
//COMPONENT
|
||||
let def5;
|
||||
let templateId7 = String(-6 - i);
|
||||
let w6 = templateId7 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId7]] : false;
|
||||
let vn8 = {};
|
||||
result = vn8;
|
||||
let props6 = Object.assign({}, context['env'].router.currentParams);
|
||||
if (w6 && w6.__owl__.renderPromise && !w6.__owl__.vnode) {
|
||||
if (utils.shallowEqual(props6, w6.__owl__.renderProps)) {
|
||||
def5 = w6.__owl__.renderPromise;
|
||||
} else {
|
||||
w6.destroy();
|
||||
w6 = false;
|
||||
}
|
||||
}
|
||||
if (!w6) {
|
||||
let componentKey6 = (context['route'].component);
|
||||
let W6 = context.constructor.components[componentKey6] || QWeb.components[componentKey6];
|
||||
if (!W6) {throw new Error('Cannot find the definition of component \\"' + componentKey6 + '\\"')}
|
||||
w6 = new W6(parent, props6);
|
||||
parent.__owl__.cmap[templateId7] = w6.__owl__.id;
|
||||
def5 = w6.__prepare();
|
||||
def5 = def5.then(vnode=>{if (w6.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: templateId7, hook: {insert(vn) {let nvn=w6.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w6.destroy();}}});utils.defineProxy(vn8, pvnode);w6.__owl__.pvnode = pvnode;});
|
||||
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 {
|
||||
def5 = def5 || w6.__updateProps(props6, extra.forceUpdate, extra.patchQueue);
|
||||
def5 = def5.then(()=>{if (w6.__owl__.isDestroyed) {return};let pvnode=w6.__owl__.pvnode;utils.defineProxy(vn8, pvnode);});
|
||||
w3.destroy();
|
||||
w3 = false;
|
||||
}
|
||||
extra.promises.push(def5);
|
||||
}
|
||||
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,6 +1,7 @@
|
||||
import { Component, Env } from "../../src/component/component";
|
||||
import { ConnectedComponent } from "../../src/store/connected_component";
|
||||
import { Store } from "../../src/store/store";
|
||||
import { useState } from "../../src/hooks";
|
||||
import { makeTestEnv, makeTestFixture, nextTick, makeDeferred } from "../helpers";
|
||||
|
||||
describe("connecting a component to store", () => {
|
||||
@@ -27,8 +28,8 @@ describe("connecting a component to store", () => {
|
||||
<span t-name="Todo"><t t-esc="props.msg"/></span>
|
||||
</templates>
|
||||
`);
|
||||
class Todo extends Component<any, any, any> {}
|
||||
class App extends ConnectedComponent<any, any, any> {
|
||||
class Todo extends Component<any, any> {}
|
||||
class App extends ConnectedComponent<any, any> {
|
||||
static components = { Todo };
|
||||
static mapStoreToProps(s) {
|
||||
return { todos: s.todos };
|
||||
@@ -70,7 +71,7 @@ describe("connecting a component to store", () => {
|
||||
};
|
||||
const store = new Store({ state, actions });
|
||||
|
||||
class App extends ConnectedComponent<any, any, any> {
|
||||
class App extends ConnectedComponent<any, any> {
|
||||
static mapStoreToProps(s) {
|
||||
return { todos: s.todos };
|
||||
}
|
||||
@@ -111,7 +112,7 @@ describe("connecting a component to store", () => {
|
||||
<span t-name="Todo"><t t-esc="props.msg"/></span>
|
||||
</templates>
|
||||
`);
|
||||
class Todo extends Component<any, any, any> {}
|
||||
class Todo extends Component<any, any> {}
|
||||
|
||||
const store = new Store({
|
||||
state: { todos: [] },
|
||||
@@ -121,7 +122,7 @@ describe("connecting a component to store", () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
class App extends ConnectedComponent<any, any, any> {
|
||||
class App extends ConnectedComponent<any, any> {
|
||||
static components = { Todo };
|
||||
static mapStoreToProps(s) {
|
||||
return { todos: s.todos };
|
||||
@@ -160,7 +161,7 @@ describe("connecting a component to store", () => {
|
||||
});
|
||||
(<any>env).store = store;
|
||||
|
||||
class App extends ConnectedComponent<any, any, any> {
|
||||
class App extends ConnectedComponent<any, any> {
|
||||
static mapStoreToProps(s) {
|
||||
return { value: s.value };
|
||||
}
|
||||
@@ -187,7 +188,7 @@ describe("connecting a component to store", () => {
|
||||
<div t-name="Child"/>
|
||||
</templates>
|
||||
`);
|
||||
class Child extends ConnectedComponent<any, any, any> {
|
||||
class Child extends ConnectedComponent<any, any> {
|
||||
static mapStoreToProps(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> {
|
||||
static components = { Child };
|
||||
|
||||
state: any;
|
||||
constructor(env: Env) {
|
||||
super(env);
|
||||
this.state = { child: true };
|
||||
this.state = useState({ child: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,14 +241,14 @@ describe("connecting a component to store", () => {
|
||||
</div>
|
||||
</templates>
|
||||
`);
|
||||
class TodoItem extends ConnectedComponent<any, any, any> {
|
||||
class TodoItem extends ConnectedComponent<any, any> {
|
||||
static mapStoreToProps(state, props) {
|
||||
const todo = state.todos.find(t => t.id === props.id);
|
||||
return todo;
|
||||
}
|
||||
}
|
||||
|
||||
class TodoList extends ConnectedComponent<any, any, any> {
|
||||
class TodoList extends ConnectedComponent<any, any> {
|
||||
static components = { TodoItem };
|
||||
static mapStoreToProps(state) {
|
||||
return { todos: state.todos };
|
||||
@@ -294,7 +295,7 @@ describe("connecting a component to store", () => {
|
||||
</templates>
|
||||
`);
|
||||
|
||||
class TodoItem extends ConnectedComponent<any, any, any> {
|
||||
class TodoItem extends ConnectedComponent<any, any> {
|
||||
static mapStoreToProps(state, props, getters) {
|
||||
const todo = state.todos.find(t => t.id === props.id);
|
||||
return {
|
||||
@@ -304,7 +305,7 @@ describe("connecting a component to store", () => {
|
||||
}
|
||||
}
|
||||
|
||||
class TodoList extends ConnectedComponent<any, any, any> {
|
||||
class TodoList extends ConnectedComponent<any, any> {
|
||||
static components = { TodoItem };
|
||||
static mapStoreToProps(state) {
|
||||
return { todos: state.todos };
|
||||
@@ -330,15 +331,15 @@ describe("connecting a component to store", () => {
|
||||
</templates>
|
||||
`);
|
||||
|
||||
class Beer extends ConnectedComponent<any, any, any> {
|
||||
class Beer extends ConnectedComponent<any, any> {
|
||||
static mapStoreToProps(state, props) {
|
||||
return state.beers[props.id];
|
||||
}
|
||||
}
|
||||
|
||||
class App extends Component<any, any, any> {
|
||||
class App extends Component<any, any> {
|
||||
static components = { Beer };
|
||||
state = { beerId: 1 };
|
||||
state = useState({ beerId: 1 });
|
||||
}
|
||||
|
||||
const state = { beers: { 1: { name: "jupiler" }, 2: { name: "kwak" } } };
|
||||
@@ -363,7 +364,7 @@ describe("connecting a component to store", () => {
|
||||
</templates>
|
||||
`);
|
||||
|
||||
class App extends ConnectedComponent<any, any, any> {
|
||||
class App extends ConnectedComponent<any, any> {
|
||||
static mapStoreToProps(state) {
|
||||
return { beers: state.beers, otherKey: 1 };
|
||||
}
|
||||
@@ -403,7 +404,7 @@ describe("connecting a component to store", () => {
|
||||
</templates>
|
||||
`);
|
||||
|
||||
class Beer extends ConnectedComponent<any, any, any> {
|
||||
class Beer extends ConnectedComponent<any, any> {
|
||||
static mapStoreToProps(state, props) {
|
||||
return {
|
||||
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> {
|
||||
static components = { Beer };
|
||||
state = { beerId: 0 };
|
||||
state = useState({ beerId: 0 });
|
||||
}
|
||||
|
||||
const actions = {
|
||||
@@ -470,7 +471,7 @@ describe("connecting a component to store", () => {
|
||||
</templates>
|
||||
`);
|
||||
|
||||
class Beer extends ConnectedComponent<any, any, any> {
|
||||
class Beer extends ConnectedComponent<any, any> {
|
||||
static mapStoreToProps(storeState, props) {
|
||||
return {
|
||||
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> {
|
||||
static components = { Beer };
|
||||
state = { beerId: 0 };
|
||||
state = useState({ beerId: 0 });
|
||||
}
|
||||
|
||||
const actions = {
|
||||
@@ -559,13 +560,13 @@ describe("connecting a component to store", () => {
|
||||
</templates>
|
||||
`);
|
||||
|
||||
class Child extends ConnectedComponent<any, any, any> {
|
||||
class Child extends ConnectedComponent<any, any> {
|
||||
static mapStoreToProps(s, props) {
|
||||
steps.push("child");
|
||||
return { msg: s.msg[props.key] };
|
||||
}
|
||||
}
|
||||
class Parent extends ConnectedComponent<any, any, any> {
|
||||
class Parent extends ConnectedComponent<any, any> {
|
||||
static components = { Child };
|
||||
static mapStoreToProps(s) {
|
||||
steps.push("parent");
|
||||
@@ -608,13 +609,13 @@ describe("connecting a component to store", () => {
|
||||
</templates>
|
||||
`);
|
||||
|
||||
class Child extends ConnectedComponent<any, any, any> {
|
||||
class Child extends ConnectedComponent<any, any> {
|
||||
static mapStoreToProps(s, props) {
|
||||
steps.push("child");
|
||||
return { msg: s.messages[props.someId] };
|
||||
}
|
||||
}
|
||||
class Parent extends ConnectedComponent<any, any, any> {
|
||||
class Parent extends ConnectedComponent<any, any> {
|
||||
static components = { Child };
|
||||
static mapStoreToProps(s) {
|
||||
steps.push("parent");
|
||||
@@ -686,7 +687,7 @@ describe("connecting a component to store", () => {
|
||||
let renderCount = 0;
|
||||
let fCount = 0;
|
||||
|
||||
class TodoItem extends ConnectedComponent<any, any, any> {
|
||||
class TodoItem extends ConnectedComponent<any, any> {
|
||||
state = { isEditing: false };
|
||||
static mapStoreToProps(state, ownProps) {
|
||||
fCount++;
|
||||
@@ -698,12 +699,12 @@ describe("connecting a component to store", () => {
|
||||
editTodo() {
|
||||
this.env.store.dispatch("editTodo");
|
||||
}
|
||||
__render(...args) {
|
||||
__render(f) {
|
||||
renderCount++;
|
||||
return super.__render(...args);
|
||||
return super.__render(f);
|
||||
}
|
||||
}
|
||||
class TodoApp extends ConnectedComponent<any, any, any> {
|
||||
class TodoApp extends ConnectedComponent<any, any> {
|
||||
static components = { TodoItem };
|
||||
static mapStoreToProps(state) {
|
||||
return {
|
||||
@@ -764,7 +765,7 @@ describe("connecting a component to store", () => {
|
||||
let renderCount = 0;
|
||||
let fCount = 0;
|
||||
|
||||
class TodoItem extends ConnectedComponent<any, any, any> {
|
||||
class TodoItem extends ConnectedComponent<any, any> {
|
||||
state = { isEditing: false };
|
||||
|
||||
static mapStoreToProps(state, ownProps) {
|
||||
@@ -776,13 +777,13 @@ describe("connecting a component to store", () => {
|
||||
removeTodo() {
|
||||
this.env.store.dispatch("removeTodo");
|
||||
}
|
||||
__render(...args) {
|
||||
__render(f) {
|
||||
renderCount++;
|
||||
return super.__render(...args);
|
||||
return super.__render(f);
|
||||
}
|
||||
}
|
||||
|
||||
class TodoApp extends ConnectedComponent<any, any, any> {
|
||||
class TodoApp extends ConnectedComponent<any, any> {
|
||||
static components = { TodoItem };
|
||||
static mapStoreToProps(state) {
|
||||
return {
|
||||
@@ -817,7 +818,7 @@ describe("connecting a component to store", () => {
|
||||
</templates>
|
||||
`);
|
||||
|
||||
class App extends ConnectedComponent<any, any, any> {
|
||||
class App extends ConnectedComponent<any, any> {
|
||||
static mapStoreToProps(s) {
|
||||
return { msg: s.msg };
|
||||
}
|
||||
@@ -871,11 +872,11 @@ describe("connected components and default values", () => {
|
||||
</templates>
|
||||
`);
|
||||
|
||||
class Greeter extends ConnectedComponent<any, any, any> {
|
||||
class Greeter extends ConnectedComponent<any, any> {
|
||||
static defaultProps = { recipient: "John" };
|
||||
}
|
||||
|
||||
class App extends Component<any, any, any> {
|
||||
class App extends Component<any, any> {
|
||||
static components = { Greeter };
|
||||
}
|
||||
|
||||
@@ -895,11 +896,11 @@ describe("connected components and default values", () => {
|
||||
</templates>
|
||||
`);
|
||||
|
||||
class Greeter extends ConnectedComponent<any, any, any> {
|
||||
class Greeter extends ConnectedComponent<any, any> {
|
||||
static defaultProps = { recipient: "John" };
|
||||
}
|
||||
|
||||
class App extends Component<any, any, any> {
|
||||
class App extends Component<any, any> {
|
||||
static components = { Greeter };
|
||||
}
|
||||
|
||||
@@ -910,11 +911,12 @@ describe("connected components and default values", () => {
|
||||
await app.mount(fixture);
|
||||
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();
|
||||
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();
|
||||
expect(fixture.innerHTML).toBe("<div><div>Hello, John</div></div>");
|
||||
});
|
||||
@@ -935,7 +937,7 @@ describe("connected components and default values", () => {
|
||||
</templates>
|
||||
`);
|
||||
|
||||
class Message extends ConnectedComponent<any, any, any> {
|
||||
class Message extends ConnectedComponent<any, any> {
|
||||
static defaultProps = { showId: true };
|
||||
static mapStoreToProps = function(state, ownProps) {
|
||||
return {
|
||||
@@ -944,7 +946,7 @@ describe("connected components and default values", () => {
|
||||
};
|
||||
}
|
||||
|
||||
class Thread extends ConnectedComponent<any, any, any> {
|
||||
class Thread extends ConnectedComponent<any, any> {
|
||||
static components = { Message };
|
||||
static defaultProps = { showMessages: true };
|
||||
static mapStoreToProps = function(state, ownProps) {
|
||||
@@ -955,7 +957,7 @@ describe("connected components and default values", () => {
|
||||
};
|
||||
}
|
||||
|
||||
class App extends Component<any, any, any> {
|
||||
class App extends Component<any, any> {
|
||||
static components = { Thread };
|
||||
static defaultProps = { threadId: 1 };
|
||||
}
|
||||
@@ -997,7 +999,8 @@ describe("connected components and default values", () => {
|
||||
"<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();
|
||||
expect(fixture.innerHTML).toBe("<div><div><div>200Message200</div></div></div>");
|
||||
|
||||
@@ -1016,15 +1019,15 @@ describe("connected components and default values", () => {
|
||||
<div t-name="Child"><t t-esc="storeProps.val"/></div>
|
||||
</templates>
|
||||
`);
|
||||
class Child extends ConnectedComponent<any, any, any> {
|
||||
class Child extends ConnectedComponent<any, any> {
|
||||
static mapStoreToProps(s) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
class Parent extends Component<any, any, any> {
|
||||
class Parent extends Component<any, any> {
|
||||
static components = { Child };
|
||||
state = { child: true };
|
||||
state = useState({ child: true });
|
||||
}
|
||||
|
||||
class TestStore extends Store {
|
||||
@@ -1060,7 +1063,7 @@ describe("connected components and default values", () => {
|
||||
</templates>
|
||||
`);
|
||||
|
||||
class App extends ConnectedComponent<any, any, any> {
|
||||
class App extends ConnectedComponent<any, any> {
|
||||
static mapStoreToProps = function(state) {
|
||||
return {
|
||||
counter: state.counter
|
||||
@@ -1141,14 +1144,14 @@ describe("various scenarios", () => {
|
||||
</div>
|
||||
</templates>
|
||||
`);
|
||||
class Attachment extends ConnectedComponent<any, any, any> {
|
||||
class Attachment extends ConnectedComponent<any, any> {
|
||||
static mapStoreToProps(state, ownProps) {
|
||||
return {
|
||||
name: state.attachments[ownProps.id].name
|
||||
};
|
||||
}
|
||||
}
|
||||
class Message extends ConnectedComponent<any, any, any> {
|
||||
class Message extends ConnectedComponent<any, any> {
|
||||
static mapStoreToProps(state) {
|
||||
return {
|
||||
attachmentIds: state.messages[10].attachmentIds
|
||||
|
||||
+7
-11
@@ -1,5 +1,5 @@
|
||||
import { h, patch } from "../src/vdom";
|
||||
import { init } from "../src/vdom/vdom";
|
||||
import { init, addNS } from "../src/vdom/vdom";
|
||||
|
||||
function map(list, fn) {
|
||||
var ret: any[] = [];
|
||||
@@ -206,19 +206,15 @@ describe("snabbdom", function() {
|
||||
expect(elm.firstChild.namespaceURI).toBe(SVGNamespace);
|
||||
|
||||
// verify that svg tag automatically gets svg namespace
|
||||
elm = patch(vnode0, h("svg", [h("foreignObject", [h("div", ["I am HTML embedded in SVG"])])]))
|
||||
.elm;
|
||||
const vnode = h("svg", [h("foreignObject", [h("div", ["I am HTML embedded in SVG"])])]);
|
||||
// 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.firstChild.namespaceURI).toBe(SVGNamespace);
|
||||
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() {
|
||||
|
||||
@@ -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();
|
||||
@@ -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
@@ -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>
|
||||
@@ -1,10 +1,11 @@
|
||||
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
|
||||
|
||||
const { useState, useRef } = owl.hooks;
|
||||
//------------------------------------------------------------------------------
|
||||
// Likes Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Counter extends owl.Component {
|
||||
state = { counter: 0 };
|
||||
state = useState({ counter: 0 });
|
||||
|
||||
increment() {
|
||||
this.state.counter++;
|
||||
@@ -32,7 +33,8 @@ class Message extends owl.Component {
|
||||
//------------------------------------------------------------------------------
|
||||
class App extends owl.Component {
|
||||
static components = { Message };
|
||||
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
|
||||
state = useState({ messages: [], multipleFlag: false, clearAfterFlag: false });
|
||||
logRef = useRef("log");
|
||||
|
||||
mounted() {
|
||||
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.textContent = `> ${str}`;
|
||||
this.refs.log.appendChild(div);
|
||||
this.refs.log.scrollTop = this.refs.log.scrollHeight;
|
||||
this.logRef.el.appendChild(div);
|
||||
this.logRef.el.scrollTop = this.logRef.el.scrollHeight;
|
||||
}
|
||||
|
||||
clearLog() {
|
||||
this.refs.log.innerHTML = "";
|
||||
this.logRef.el.innerHTML = "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,11 +12,11 @@
|
||||
</div>
|
||||
<div class="flags">
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
<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.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>
|
||||
</ul>
|
||||
<ul>
|
||||
|
||||
+11
-10
@@ -1,5 +1,5 @@
|
||||
import { SAMPLES } from "./samples.js";
|
||||
|
||||
const {useState, useRef} = owl.hooks;
|
||||
//------------------------------------------------------------------------------
|
||||
// Constants, helpers, utils
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -156,18 +156,18 @@ Promise.all([loadTemplates(), owl.utils.whenReady()]).then(start);
|
||||
class TabbedEditor extends owl.Component {
|
||||
constructor(parent, props) {
|
||||
super(parent, props);
|
||||
this.state = {
|
||||
this.state = useState({
|
||||
currentTab: props.js ? "js" : props.xml ? "xml" : "css"
|
||||
};
|
||||
});
|
||||
this.setTab = owl.utils.debounce(this.setTab, 250, true);
|
||||
|
||||
this.sessions = {};
|
||||
this._setupSessions(props);
|
||||
this.editor = null;
|
||||
this.editorNode = useRef("editor");
|
||||
}
|
||||
|
||||
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.setFontSize("12px");
|
||||
@@ -250,7 +250,7 @@ class App extends owl.Component {
|
||||
this.version = owl.__info__.version;
|
||||
this.SAMPLES = SAMPLES;
|
||||
|
||||
this.state = {
|
||||
this.state = useState({
|
||||
js: SAMPLES[0].code,
|
||||
css: SAMPLES[0].css || "",
|
||||
xml: SAMPLES[0].xml || DEFAULT_XML,
|
||||
@@ -259,18 +259,19 @@ class App extends owl.Component {
|
||||
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.refs.content.innerHTML = "";
|
||||
this.content.el.innerHTML = "";
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -296,8 +297,8 @@ class App extends owl.Component {
|
||||
} else {
|
||||
this.state.error = false;
|
||||
}
|
||||
this.refs.content.innerHTML = "";
|
||||
this.refs.content.appendChild(subiframe);
|
||||
this.content.el.innerHTML = "";
|
||||
this.content.el.appendChild(subiframe);
|
||||
}
|
||||
|
||||
setSample(ev) {
|
||||
|
||||
@@ -173,8 +173,10 @@ body {
|
||||
}
|
||||
|
||||
.right-pane .note {
|
||||
font-size: 20px;
|
||||
font-size: 18px;
|
||||
text-align: left;
|
||||
margin-top: 20px;
|
||||
padding: 5%;
|
||||
}
|
||||
|
||||
.right-pane .error {
|
||||
|
||||
+272
-53
@@ -1,17 +1,18 @@
|
||||
const COMPONENTS = `// In this example, we show how components can be defined and created.
|
||||
const { Component, useState } = owl;
|
||||
|
||||
class Greeter extends owl.Component {
|
||||
state = { word: 'Hello' };
|
||||
class Greeter extends Component {
|
||||
state = useState({ word: 'Hello' });
|
||||
|
||||
toggle() {
|
||||
this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi'
|
||||
this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi';
|
||||
}
|
||||
}
|
||||
|
||||
// Main root component
|
||||
class App extends owl.Component {
|
||||
class App extends Component {
|
||||
static components = { Greeter };
|
||||
state = { name: 'World'};
|
||||
state = useState({ name: 'World'});
|
||||
}
|
||||
|
||||
// Application setup
|
||||
@@ -45,17 +46,18 @@ const COMPONENTS_CSS = `.greeter {
|
||||
|
||||
const ANIMATION = `// The goal of this component is to see how the t-transition directive can be
|
||||
// used to generate simple transition effects.
|
||||
const { Component, useState } = owl;
|
||||
|
||||
class Counter extends owl.Component {
|
||||
state = { value: 0 };
|
||||
class Counter extends Component {
|
||||
state = useState({ value: 0 });
|
||||
|
||||
increment() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
|
||||
class App extends owl.Component {
|
||||
state = { flag: false, componentFlag: false, numbers: [] };
|
||||
class App extends Component {
|
||||
state = useState({ flag: false, componentFlag: false, numbers: [] });
|
||||
static components = { Counter };
|
||||
|
||||
toggle(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
|
||||
// clicking on the two main buttons, and look into the console to see what
|
||||
// happens.
|
||||
const { Component, useState } = owl;
|
||||
|
||||
class DemoComponent extends owl.Component {
|
||||
class DemoComponent extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.state = { n: 0 };
|
||||
this.state = useState({ n: 0 });
|
||||
console.log("constructor");
|
||||
}
|
||||
async willStart() {
|
||||
@@ -213,9 +216,9 @@ class DemoComponent extends owl.Component {
|
||||
}
|
||||
}
|
||||
|
||||
class App extends owl.Component {
|
||||
class App extends Component {
|
||||
static components = { DemoComponent };
|
||||
state = { n: 0, flag: true };
|
||||
state = useState({ n: 0, flag: true });
|
||||
|
||||
increment() {
|
||||
this.state.n++;
|
||||
@@ -259,12 +262,70 @@ const LIFECYCLE_CSS = `button {
|
||||
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
|
||||
// www.todomvc.com project. This is a non trivial application with some
|
||||
// interesting user interactions. It uses the local storage for persistence.
|
||||
//
|
||||
// In this implementation, we use the owl Store class to manage the state. It
|
||||
// is very similar to the VueX store.
|
||||
const { Component, useState } = owl;
|
||||
const { useRef } = owl.hooks;
|
||||
|
||||
const ENTER_KEY = 13;
|
||||
const ESC_KEY = 27;
|
||||
@@ -335,8 +396,9 @@ function makeStore() {
|
||||
//------------------------------------------------------------------------------
|
||||
// TodoItem
|
||||
//------------------------------------------------------------------------------
|
||||
class TodoItem extends owl.Component {
|
||||
state = { isEditing: false };
|
||||
class TodoItem extends Component {
|
||||
state = useState({ isEditing: false });
|
||||
inputRef = useRef("input");
|
||||
|
||||
removeTodo() {
|
||||
this.env.store.dispatch("removeTodo", this.props.id);
|
||||
@@ -351,9 +413,9 @@ class TodoItem extends owl.Component {
|
||||
}
|
||||
|
||||
focusInput() {
|
||||
this.refs.input.value = "";
|
||||
this.refs.input.focus();
|
||||
this.refs.input.value = this.props.title;
|
||||
this.inputRef.el.value = "";
|
||||
this.inputRef.el.focus();
|
||||
this.inputRef.el.value = this.props.title;
|
||||
}
|
||||
|
||||
handleKeyup(ev) {
|
||||
@@ -389,7 +451,7 @@ class TodoItem extends owl.Component {
|
||||
//------------------------------------------------------------------------------
|
||||
class TodoApp extends owl.store.ConnectedComponent {
|
||||
static components = { TodoItem };
|
||||
state = { filter: "all" };
|
||||
state = useState({ filter: "all" });
|
||||
|
||||
static mapStoreToProps(state) {
|
||||
return {
|
||||
@@ -1052,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
|
||||
// the context of the App component, even though it is inside the Card component
|
||||
const { Component, useState } = owl;
|
||||
|
||||
class Card extends owl.Component {
|
||||
state = { showContent: true };
|
||||
class Card extends Component {
|
||||
state = useState({ showContent: true });
|
||||
|
||||
toggleDisplay() {
|
||||
this.state.showContent = !this.state.showContent;
|
||||
}
|
||||
}
|
||||
|
||||
class Counter extends owl.Component {
|
||||
state = {val: 1};
|
||||
class Counter extends Component {
|
||||
state = useState({val: 1});
|
||||
|
||||
inc() {
|
||||
this.state.val++;
|
||||
@@ -1070,9 +1133,9 @@ class Counter extends owl.Component {
|
||||
}
|
||||
|
||||
// Main root component
|
||||
class App extends owl.Component {
|
||||
class App extends Component {
|
||||
static components = {Card, Counter};
|
||||
state = {a: 1, b: 3};
|
||||
state = useState({a: 1, b: 3});
|
||||
|
||||
inc(key, delta) {
|
||||
this.state[key] += delta;
|
||||
@@ -1168,8 +1231,9 @@ const ASYNC_COMPONENTS = `// This example will not work if your browser does not
|
||||
// However, we don't want renderings of the other sub component to be delayed
|
||||
// because of the slow component. We use the 't-asyncroot' directive for this
|
||||
// purpose. Try removing it to see the difference.
|
||||
const { Component, useState } = owl;
|
||||
|
||||
class SlowComponent extends owl.Component {
|
||||
class SlowComponent extends Component {
|
||||
willUpdateProps() {
|
||||
// simulate a component that needs to perform async stuff (e.g. an RPC)
|
||||
// with the updated props before re-rendering itself
|
||||
@@ -1177,11 +1241,11 @@ class SlowComponent extends owl.Component {
|
||||
}
|
||||
}
|
||||
|
||||
class NotificationList extends owl.Component {}
|
||||
class NotificationList extends Component {}
|
||||
|
||||
class App extends owl.Component {
|
||||
class App extends Component {
|
||||
static components = {SlowComponent, NotificationList};
|
||||
state = { value: 0, notifs: [] };
|
||||
state = useState({ value: 0, notifs: [] });
|
||||
|
||||
increment() {
|
||||
this.state.value++;
|
||||
@@ -1250,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.
|
||||
// Note that there are two controls with t-model="color": they are totally
|
||||
// synchronized.
|
||||
const { Component, useState } = owl;
|
||||
|
||||
class Form extends owl.Component {
|
||||
state = {
|
||||
class Form extends Component {
|
||||
state = useState({
|
||||
text: "",
|
||||
othertext: "",
|
||||
number: 11,
|
||||
color: "",
|
||||
bool: false
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Application setup
|
||||
@@ -1271,20 +1336,20 @@ const FORM_XML = `<templates>
|
||||
<div t-name="Form">
|
||||
<h1>Form</h1>
|
||||
<div>
|
||||
Text (immediate): <input t-model="text"/>
|
||||
Text (immediate): <input t-model="state.text"/>
|
||||
</div>
|
||||
<div>
|
||||
Other text (lazy): <input t-model.lazy="othertext"/>
|
||||
Other text (lazy): <input t-model.lazy="state.othertext"/>
|
||||
</div>
|
||||
<div>
|
||||
Number: <input t-model.number="number"/>
|
||||
Number: <input t-model.number="state.number"/>
|
||||
</div>
|
||||
<div>
|
||||
Boolean: <input type="checkbox" t-model="bool"/>
|
||||
Boolean: <input type="checkbox" t-model="state.bool"/>
|
||||
</div>
|
||||
<div>
|
||||
Color, with a select:
|
||||
<select t-model="color">
|
||||
<select t-model="state.color">
|
||||
<option value="">Select a color</option>
|
||||
<option value="red">Red</option>
|
||||
<option value="blue">Blue</option>
|
||||
@@ -1292,8 +1357,8 @@ const FORM_XML = `<templates>
|
||||
</div>
|
||||
<div>
|
||||
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="blue" value="blue" t-model="color"/><label for="blue">Blue</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="state.color"/><label for="blue">Blue</label></span>
|
||||
</div>
|
||||
<hr/>
|
||||
<h1>State</h1>
|
||||
@@ -1316,26 +1381,31 @@ const WMS = `// This example is slightly more complex than usual. We demonstrate
|
||||
// - minimal width/height
|
||||
// - 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 {
|
||||
state = { value: 0 };
|
||||
class Counter extends Component {
|
||||
state = useState({ value: 0 });
|
||||
|
||||
inc() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
|
||||
class Window extends owl.Component {
|
||||
class Window extends Component {
|
||||
|
||||
get style() {
|
||||
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}\`;
|
||||
}
|
||||
|
||||
close() {
|
||||
this.trigger("close-window", { id: this.props.info.id });
|
||||
}
|
||||
|
||||
startDragAndDrop(ev) {
|
||||
this.updateZIndex();
|
||||
this.el.classList.add('dragging');
|
||||
@@ -1361,31 +1431,37 @@ class Window extends owl.Component {
|
||||
self.trigger("set-window-position", options);
|
||||
}
|
||||
}
|
||||
|
||||
updateZIndex() {
|
||||
this.trigger("update-z-index", { id: this.props.info.id });
|
||||
}
|
||||
}
|
||||
|
||||
class WindowManager extends owl.Component {
|
||||
class WindowManager extends Component {
|
||||
static components = { Window };
|
||||
windows = [];
|
||||
nextId = 1;
|
||||
currentZindex = 1;
|
||||
nextLeft = 0;
|
||||
nextTop = 0;
|
||||
|
||||
addWindow(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({
|
||||
id: id,
|
||||
id: this.nextId++,
|
||||
title: info.title,
|
||||
width: info.defaultWidth,
|
||||
height: info.defaultHeight,
|
||||
top: 0,
|
||||
left: 0,
|
||||
zindex: this.currentZindex++
|
||||
top: this.nextTop,
|
||||
left: this.nextLeft,
|
||||
zindex: this.currentZindex++,
|
||||
component: info.component
|
||||
});
|
||||
this.constructor.components[id] = info.component;
|
||||
this.render();
|
||||
}
|
||||
|
||||
closeWindow(ev) {
|
||||
const id = ev.detail.id;
|
||||
delete this.constructor.components[id];
|
||||
@@ -1393,12 +1469,14 @@ class WindowManager extends owl.Component {
|
||||
this.windows.splice(index, 1);
|
||||
this.render();
|
||||
}
|
||||
|
||||
setWindowPosition(ev) {
|
||||
const id = ev.detail.id;
|
||||
const w = this.windows.find(w => w.id === id);
|
||||
w.top = ev.detail.top;
|
||||
w.left = ev.detail.left;
|
||||
}
|
||||
|
||||
updateZIndex(ev) {
|
||||
const id = ev.detail.id;
|
||||
const w = this.windows.find(w => w.id === id);
|
||||
@@ -1407,11 +1485,12 @@ class WindowManager extends owl.Component {
|
||||
}
|
||||
}
|
||||
|
||||
class App extends owl.Component {
|
||||
class App extends Component {
|
||||
static components = { WindowManager };
|
||||
wmRef = useRef("wm");
|
||||
|
||||
addWindow(name) {
|
||||
this.refs.wm.addWindow(name);
|
||||
this.wmRef.comp.addWindow(name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1452,7 +1531,7 @@ const WMS_XML = `<templates>
|
||||
t-on-update-z-index="updateZIndex"
|
||||
t-on-set-window-position="setWindowPosition">
|
||||
<Window t-foreach="windows" t-as="w" t-key="w.id" info="w">
|
||||
<t t-component="{{w.id}}"/>
|
||||
<t t-component="w.component"/>
|
||||
</Window>
|
||||
</div>
|
||||
|
||||
@@ -1546,6 +1625,135 @@ const WMS_CSS = `body {
|
||||
font-size: 20px;
|
||||
}`;
|
||||
|
||||
const MONKEYPATCH = `
|
||||
// In this example, we show a possible way components can be monkey patched.
|
||||
// This involves the functions patch/unpatch, which modifies the prototype of a
|
||||
// class (not only an owl component). Sadly, the patch code cannot make use of
|
||||
// the syntactic sugar that JS engines provides to classes, so it needs to use
|
||||
// the "this._super()" syntax instead of the standard "super.someMethod()".
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Patching Code
|
||||
//--------------------------------------------------------------------------
|
||||
const patchMap = new WeakMap();
|
||||
|
||||
function patch(C, patchName, patch) {
|
||||
let metadata = patchMap.get(C.prototype);
|
||||
if (!metadata) {
|
||||
metadata = {
|
||||
origMethods: {},
|
||||
patches: {},
|
||||
current: []
|
||||
};
|
||||
patchMap.set(C.prototype, metadata);
|
||||
}
|
||||
const proto = C.prototype;
|
||||
if (metadata.patches[patchName]) {
|
||||
throw new Error(\`Patch [\${patchName}] already exists\`);
|
||||
}
|
||||
metadata.patches[patchName] = patch;
|
||||
applyPatch(proto, patch);
|
||||
metadata.current.push(patchName);
|
||||
|
||||
function applyPatch(proto, patch) {
|
||||
Object.keys(patch).forEach(function(methodName) {
|
||||
const method = patch[methodName];
|
||||
if (typeof method === "function") {
|
||||
const original = proto[methodName];
|
||||
if (!(methodName in metadata.origMethods)) {
|
||||
metadata.origMethods[methodName] = original;
|
||||
}
|
||||
proto[methodName] = function(...args) {
|
||||
this._super = original;
|
||||
return method.call(this, ...args);
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// we define here an unpatch function. This is mostly useful if we want to
|
||||
// remove a patch. For example, for testing purposes
|
||||
function unpatch(C, patchName) {
|
||||
const proto = C.prototype;
|
||||
let metadata = patchMap.get(proto);
|
||||
if (!metdata) {
|
||||
return;
|
||||
}
|
||||
patchMap.delete(proto);
|
||||
|
||||
// reset to original
|
||||
for (let k in metadata.origMethods) {
|
||||
proto[k] = metadata.origMethods[k];
|
||||
}
|
||||
|
||||
// apply other patches
|
||||
for (let name of metadata.current) {
|
||||
if (name !== patchName) {
|
||||
patch(C, name, metadata.patches[name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Components
|
||||
//--------------------------------------------------------------------------
|
||||
const { Component, useState } = owl;
|
||||
|
||||
class Counter extends Component {
|
||||
state = useState({ value: 0 });
|
||||
|
||||
increment() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
|
||||
// Main root component
|
||||
class App extends Component {
|
||||
static components = { Counter };
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Patching code
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
// In an Odoo application, this code would be located in another odoo module,
|
||||
// to customize some behaviour in an existing class/component
|
||||
patch(Counter, "double_value", {
|
||||
increment() {
|
||||
this._super();
|
||||
this.state.value = 2 * this.state.value;
|
||||
}
|
||||
});
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Application setup
|
||||
//--------------------------------------------------------------------------
|
||||
const qweb = new owl.QWeb(TEMPLATES);
|
||||
const app = new App({ qweb });
|
||||
app.mount(document.body);
|
||||
`;
|
||||
|
||||
const MONKEYPATCH_XML = `
|
||||
<templates>
|
||||
<button t-name="Counter" t-on-click="increment">
|
||||
Click [<t t-esc="state.value"/>]
|
||||
</button>
|
||||
|
||||
<div t-name="App">
|
||||
<Counter />
|
||||
</div>
|
||||
</templates>
|
||||
`;
|
||||
|
||||
const MONKEYPATCH_CSS = `
|
||||
button {
|
||||
font-size: 20px;
|
||||
width: 100px;
|
||||
height: 50px;
|
||||
}`;
|
||||
|
||||
export const SAMPLES = [
|
||||
{
|
||||
description: "Components",
|
||||
@@ -1570,6 +1778,12 @@ export const SAMPLES = [
|
||||
xml: LIFECYCLE_DEMO_XML,
|
||||
css: LIFECYCLE_CSS
|
||||
},
|
||||
{
|
||||
description: "Hooks",
|
||||
code: HOOKS_DEMO,
|
||||
xml: HOOKS_DEMO_XML,
|
||||
css: HOOKS_CSS
|
||||
},
|
||||
{
|
||||
description: "Todo List App (with store)",
|
||||
code: TODO_APP_STORE,
|
||||
@@ -1599,5 +1813,10 @@ export const SAMPLES = [
|
||||
code: ASYNC_COMPONENTS,
|
||||
xml: ASYNC_COMPONENTS_XML,
|
||||
css: ASYNC_COMPONENTS_CSS
|
||||
}, {
|
||||
description: "Monkey patching components",
|
||||
code: MONKEYPATCH,
|
||||
xml: MONKEYPATCH_XML,
|
||||
css: MONKEYPATCH_CSS
|
||||
}
|
||||
];
|
||||
|
||||
@@ -45,7 +45,11 @@
|
||||
<div>🦉 Odoo Web Library 🦉</div>
|
||||
<div>v<t t-esc="version"/></div>
|
||||
<div class="url"><a href="https://github.com/odoo/owl">https://github.com/odoo/owl</a></div>
|
||||
<div class="note">Note: these examples require a recent browser to work without a transpilation step. </div>
|
||||
<div class="note">
|
||||
<p>Note: these examples are using recent features of Javascript, and require a recent browser to work without a transpilation step!
|
||||
For example, it makes use of class fields and class static fields. These examples should work in a recent Chrome version.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="state.error" class="error">
|
||||
<h3>Error</h3>
|
||||
|
||||
Reference in New Issue
Block a user