mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b859fcb8f0 | |||
| 3005756d89 | |||
| f6820bb8ac | |||
| 090880f478 | |||
| 5cdaa7b473 | |||
| 91b9e78182 | |||
| 5335fb8fba | |||
| 4513e3f31c | |||
| ecf12f6eee | |||
| 82a6961b3a | |||
| 78a4550ebb | |||
| cc82b3ffcd | |||
| bbbaf95c8e | |||
| caa63628dc | |||
| 7a49b9d94e | |||
| 01eb338e69 | |||
| 3048c6f961 | |||
| 8edc637033 | |||
| c9910077a4 | |||
| 6bf70f016c | |||
| 905619f80f | |||
| 14ceb382ef | |||
| 9846b2e997 | |||
| cfc2fb2ba2 | |||
| d57017b6ed | |||
| 5bf7a5c671 | |||
| 9b589af75a | |||
| 471fd49063 | |||
| 4fc3423682 | |||
| ffb3263c79 | |||
| e1acf66143 | |||
| 2a40907e35 | |||
| 1c0dd79955 |
@@ -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,58 +1,68 @@
|
||||
<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:
|
||||
|
||||
```xml
|
||||
<templates>
|
||||
<button t-name="Counter" t-on-click="increment">
|
||||
Click Me! [<t t-esc="state.value"/>]
|
||||
</button>
|
||||
<div t-name="App">
|
||||
<span>Hello Owl</span>
|
||||
<Counter />
|
||||
</div>
|
||||
</templates>
|
||||
```
|
||||
|
||||
```javascript
|
||||
class Counter extends owl.Component {
|
||||
state = { value: 0 };
|
||||
import { Component, QWeb, useState } from "owl";
|
||||
import { xml } from "owl/tags";
|
||||
|
||||
class Counter extends Component {
|
||||
static template = xml`
|
||||
<button t-on-click="increment">
|
||||
Click Me! [<t t-esc="state.value"/>]
|
||||
</button>`;
|
||||
|
||||
state = useState({ value: 0 });
|
||||
|
||||
increment() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
|
||||
class App extends owl.Component {
|
||||
components = { Counter };
|
||||
class App extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<span>Hello Owl</span>
|
||||
<Counter />
|
||||
</div>`;
|
||||
|
||||
static components = { Counter };
|
||||
}
|
||||
|
||||
const qweb = new owl.QWeb(TEMPLATES);
|
||||
const app = new App({ qweb });
|
||||
const app = new App({ qweb: new QWeb() });
|
||||
app.mount(document.body);
|
||||
```
|
||||
|
||||
Note that we assume here that the xml templates are available in the `TEMPLATES`
|
||||
string. More interesting examples can be found on the
|
||||
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.
|
||||
@@ -69,7 +79,7 @@ requirements are common and code needs to be maintained by large teams.
|
||||
it is not ok to use standard web tools based on `npm`.
|
||||
|
||||
Owl is not designed to be fast nor small (even though it is quite good on those
|
||||
two topics). It is a no nonsense framework to build applications. There is only
|
||||
two topics). It is a no nonsense framework to build applications. There is only
|
||||
one way to define components (with classes).
|
||||
|
||||
If you are interested in a comparison with React or Vue, you will
|
||||
@@ -81,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!
|
||||
@@ -90,20 +100,159 @@ Submit a PR!
|
||||
|
||||
If you want to use a simple `<script>` tag, the last release can be downloaded here:
|
||||
|
||||
- [owl-0.20.0.js](https://github.com/odoo/owl/releases/download/v0.20.0/owl.js)
|
||||
- [owl-0.20.0.min.js](https://github.com/odoo/owl/releases/download/v0.20.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
|
||||
|
||||
|
||||
+4
-4
@@ -31,11 +31,11 @@ than React and Vue. Also, jQuery is not the same kind of framework, but it is in
|
||||
|
||||
## Class Based
|
||||
|
||||
Both React and Vue moved away from defining components with classes. They prefer
|
||||
Both React and Vue moved away from defining components with classes. They prefer
|
||||
a more functional approach, in particular, with the new `hooks` mechanisms.
|
||||
|
||||
This has some advantages and disadvantages. But the end result is that React
|
||||
and Vue both offers multiple different ways of defining new components. In
|
||||
This has some advantages and disadvantages. But the end result is that React
|
||||
and Vue both offers multiple different ways of defining new components. In
|
||||
contrast, Owl has only one mechanism: class-based components. We believe that Owl
|
||||
components are fast enough for all our usecases, and making it as simple as
|
||||
possible for developers is more valuable (for us).
|
||||
@@ -233,4 +233,4 @@ class Counter extends owl.ConnectedComponent {
|
||||
}
|
||||
|
||||
const counter = new Counter({ store, qweb });
|
||||
```
|
||||
```
|
||||
|
||||
+181
-77
@@ -5,6 +5,7 @@
|
||||
- [Overview](#overview)
|
||||
- [Example](#example)
|
||||
- [Reference](#reference)
|
||||
- [Reactive System](#reactive-system)
|
||||
- [Properties](#properties)
|
||||
- [Static Properties](#static-properties)
|
||||
- [Methods](#methods)
|
||||
@@ -22,6 +23,7 @@
|
||||
- [Slots](#slots)
|
||||
- [Asynchronous Rendering](#asynchronous-rendering)
|
||||
- [Error Handling](#error-handling)
|
||||
- [Functional Components](#functional-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++;
|
||||
@@ -72,11 +73,10 @@ Note that this code is written in ESNext style, so it will only run on the
|
||||
latest browsers without a transpilation step.
|
||||
|
||||
This example show how a component should be defined: it simply subclasses the
|
||||
Component class. If no `template` key is defined, then
|
||||
Component class. If no static `template` key is defined, then
|
||||
Owl will use the component's name as template name. Here,
|
||||
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,24 +115,27 @@ find a template with the component name (or one of its ancestor).
|
||||
|
||||
- **`env`** (Object): the component environment, which contains a QWeb instance.
|
||||
|
||||
- **`template`** (string, optional): if given, this is the name of the QWeb template that will render
|
||||
the component.
|
||||
|
||||
- **`state`** (Object): this is the location of the component's state, if there is
|
||||
any. After the willStart method, the `state` property is observed, and each
|
||||
change will cause the component to rerender itself.
|
||||
|
||||
- **`props`** (Object): this is an object given (in the constructor) by the parent
|
||||
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
|
||||
|
||||
- **`props`** (Object, optional): if given, this is an object that describes the
|
||||
- **`template`** (string, optional): if given, this is the name of the QWeb template that will render the component. Note that there is a helper `xml` to
|
||||
make it easy to define an inline template.
|
||||
|
||||
* **`components`** (Object, optional): if given, this is an object that contains
|
||||
the classes of any sub components needed by the template. This is the main way
|
||||
used by Owl to be able to create sub components.
|
||||
|
||||
```js
|
||||
class ParentComponent extends owl.Component {
|
||||
static components = { SubComponent };
|
||||
}
|
||||
```
|
||||
|
||||
* **`props`** (Object, optional): if given, this is an object that describes the
|
||||
type and shape of the (actual) props given to the component. If Owl mode is
|
||||
`dev`, this will be used to validate the props each time the component is
|
||||
created/updated. See [Props Validation](#props-validation) for more information.
|
||||
@@ -127,7 +149,7 @@ find a template with the component name (or one of its ancestor).
|
||||
}
|
||||
```
|
||||
|
||||
* **`defaultProps`** (Object, optional): if given, this object define default
|
||||
- **`defaultProps`** (Object, optional): if given, this object define default
|
||||
values for (top-level) props. Whenever `props` are given to the object, they
|
||||
will be altered to add default value (if missing). Note that it does not
|
||||
change the initial object, a new object will be created instead.
|
||||
@@ -226,7 +248,7 @@ component.
|
||||
```javascript
|
||||
constructor(parent, props) {
|
||||
super(parent, props);
|
||||
this.state = {someValue: true};
|
||||
this.state = useState({someValue: true});
|
||||
this.template = 'mytemplate';
|
||||
}
|
||||
```
|
||||
@@ -236,7 +258,7 @@ implemented in most cases:
|
||||
|
||||
```javascript
|
||||
class ClickCounter extends owl.Component {
|
||||
state = { value: 0 };
|
||||
state = useState({ value: 0 });
|
||||
|
||||
...
|
||||
}
|
||||
@@ -261,9 +283,6 @@ At this point, the component is not yet rendered. Note that a slow `willStart` m
|
||||
interface. Therefore, some care should be made to make this method as
|
||||
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
|
||||
@@ -277,10 +296,9 @@ always be unmounted at some point in the future.
|
||||
The mounted method will be called recursively on each of its children. First,
|
||||
the 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)`
|
||||
|
||||
@@ -305,7 +323,7 @@ It is not called on the initial render. This is useful to read some
|
||||
information from the DOM. For example, the current position of the
|
||||
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`).
|
||||
@@ -387,9 +405,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) {
|
||||
@@ -398,7 +416,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);
|
||||
@@ -420,7 +438,7 @@ to be capitalized.
|
||||
|
||||
```js
|
||||
class ParentComponent extends owl.Component {
|
||||
components = { MyComponent: MyComponent};
|
||||
static components = { MyComponent: MyComponent};
|
||||
...
|
||||
}
|
||||
```
|
||||
@@ -432,7 +450,8 @@ dynamic. If it is necessary to give a string, this can be done by quoting it:
|
||||
`someString="'somevalue'"`.
|
||||
|
||||
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">
|
||||
@@ -442,16 +461,16 @@ that the template can access `state`, `props`, `env`, or any methods defined in
|
||||
|
||||
```js
|
||||
class ParentComponent {
|
||||
components = { ChildComponent };
|
||||
state = { val: 4 };
|
||||
static components = { ChildComponent };
|
||||
state = useState({ val: 4 });
|
||||
}
|
||||
```
|
||||
|
||||
Whenever the template is rendered, it will automatically create the subcomponent
|
||||
`ChildComponent` at the correct place. It needs to find the reference to the
|
||||
actual component class in the special `components` key, or the class registered in
|
||||
actual component class in the special static `components` key, or the class registered in
|
||||
QWeb's global registry (see `register` function of QWeb). It first looks inside
|
||||
the local `components` key, then fallbacks on the global registry.
|
||||
the static `components` key, then fallbacks on the global registry.
|
||||
|
||||
_Props_: In this example, the child component will receive the object `{count: 4}` in its
|
||||
constructor. This will be assigned to the `props` variable, which can be accessed
|
||||
@@ -461,7 +480,8 @@ the sub component will also be updated automatically.
|
||||
Note that there are some restrictions on prop names: `class`, `style` and any
|
||||
string which starts with `t-` are not allowed.
|
||||
|
||||
The `t-component` directive can also be used to accept dynamic values with string interpolation (like the [`t-attf-`](qweb.md#dynamic-attributes) directive):
|
||||
It is not common, but sometimes we need a dynamic component name and/or dynamic props. In this case,
|
||||
the `t-component` directive can also be used to accept dynamic values with string interpolation (like the [`t-attf-`](qweb.md#dynamic-attributes) directive):
|
||||
|
||||
```xml
|
||||
<div t-name="ParentComponent">
|
||||
@@ -471,11 +491,51 @@ The `t-component` directive can also be used to accept dynamic values with strin
|
||||
|
||||
```js
|
||||
class ParentComponent {
|
||||
components = { ChildComponent1, ChildComponent2 };
|
||||
static components = { ChildComponent1, ChildComponent2 };
|
||||
state = { id: 1 };
|
||||
}
|
||||
```
|
||||
|
||||
And the `t-props` directive can be used to specify totally dynamic props:
|
||||
|
||||
```xml
|
||||
<div t-name="ParentComponent">
|
||||
<Child t-props="some.obj"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
class ParentComponent {
|
||||
static components = { Child };
|
||||
some = { obj: { a: 1, b: 2 } };
|
||||
}
|
||||
```
|
||||
|
||||
There is an even more dynamic way to use `t-component`: its value can be an
|
||||
expression evaluating to an actual component class. In that case, this is the
|
||||
class that will be used to create the component:
|
||||
|
||||
```js
|
||||
class A extends Component<any, any, any> {
|
||||
static template = xml`<span>child a</span>`;
|
||||
}
|
||||
class B extends Component<any, any, any> {
|
||||
static template = xml`<span>child b</span>`;
|
||||
}
|
||||
class App extends Component<any, any, any> {
|
||||
static template = xml`<t t-component="myComponent" t-key="state.child"/>`;
|
||||
|
||||
state = { child: "a" };
|
||||
|
||||
get myComponent() {
|
||||
return this.state.child === "a" ? A : B;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In this example, the component `App` selects dynamically the concrete sub
|
||||
component class.
|
||||
|
||||
**CSS and style:** there is some specific support to allow the parent to declare
|
||||
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.
|
||||
@@ -591,7 +651,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;
|
||||
@@ -611,8 +671,9 @@ plumbing code is slightly different if you need to interact with a checkbox,
|
||||
or with radio buttons, or with select tags.
|
||||
|
||||
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 {
|
||||
@@ -622,7 +683,7 @@ class Form extends owl.Component {
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<input t-model="text" />
|
||||
<input t-model="state.text" />
|
||||
<span t-esc="state.text" />
|
||||
</div>
|
||||
```
|
||||
@@ -632,11 +693,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>
|
||||
@@ -645,11 +706,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>
|
||||
@@ -667,7 +728,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
|
||||
@@ -901,44 +962,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_
|
||||
@@ -1066,8 +1143,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;
|
||||
@@ -1094,3 +1171,30 @@ 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.
|
||||
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
# 🦉 Hooks 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Example](#example)
|
||||
- [Reference](#reference)
|
||||
- [One Rule](#one-rule)
|
||||
- [`useState`](#usestate)
|
||||
- [`onMounted`](#onmounted)
|
||||
- [`onWillUnmount`](#onwillunmount)
|
||||
- [`useRef`](#useref)
|
||||
|
||||
## Overview
|
||||
|
||||
Hooks were popularised by React as a way to solve the following issues:
|
||||
|
||||
- help reusing stateful logic between components
|
||||
- help organizing code by feature in complex components
|
||||
- use state in functional components, without writing a class.
|
||||
|
||||
Owl hooks serve the same purpose, except that they work for class components
|
||||
(note: React hooks do not work on class components, and maybe because of that,
|
||||
there seems to be the misconception that hooks are in opposition to class. This
|
||||
is clearly not true, as shown by Owl hooks).
|
||||
|
||||
Hooks works beautifully with Owl components: they solve the problems mentioned
|
||||
above, and in particular, they are the perfect way to make your component
|
||||
reactive.
|
||||
|
||||
## Example
|
||||
|
||||
Here is the classical example of a non trivial hook to track the mouse position.
|
||||
|
||||
```js
|
||||
const { useState, onMounted, onWillUnmount } = owl.hooks;
|
||||
|
||||
// We define here a custom behaviour: this hook tracks the state of the mouse
|
||||
// position
|
||||
function useMouse() {
|
||||
const position = useState({ x: 0, y: 0 });
|
||||
|
||||
function update(e) {
|
||||
position.x = e.clientX;
|
||||
position.y = e.clientY;
|
||||
}
|
||||
onMounted(() => {
|
||||
window.addEventListener("mousemove", update);
|
||||
});
|
||||
onWillUnmount(() => {
|
||||
window.removeEventListener("mousemove", update);
|
||||
});
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
// Main root component
|
||||
class App extends owl.Component {
|
||||
static template = xml`
|
||||
<div t-name="App">
|
||||
<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>
|
||||
</div>`;
|
||||
|
||||
// this hooks is bound to the 'mouse' property.
|
||||
mouse = useMouse();
|
||||
}
|
||||
```
|
||||
|
||||
Note that we use the prefix `use` for hooks, just like in React. This is just
|
||||
a convention.
|
||||
|
||||
## Reference
|
||||
|
||||
### One rule
|
||||
|
||||
There is only one rule: every hook for a component have to be called in the
|
||||
constructor (or in class fields):
|
||||
|
||||
```js
|
||||
// ok
|
||||
class SomeComponent extends Component {
|
||||
state = useState({value: 0});
|
||||
}
|
||||
|
||||
// also ok
|
||||
class SomeComponent extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.state = useState({value: 0});
|
||||
}
|
||||
}
|
||||
|
||||
// not ok: this is executed after the constructor is called
|
||||
class SomeComponent extends Component {
|
||||
async willStart() {
|
||||
this.state = useState({value: 0});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `useState`
|
||||
|
||||
The `useState` hook is certainly the most important hooks for Owl components:
|
||||
this is what enables component to be reactive, to react to state change.
|
||||
|
||||
The `useState` hook has to be given an object or an array, and will return
|
||||
an observed version of it (using a `Proxy`).
|
||||
|
||||
```javascript
|
||||
const { useState } = owl.hooks;
|
||||
|
||||
class Counter extends owl.Component {
|
||||
static template = xml`
|
||||
<button t-on-click="increment">
|
||||
Click Me! [<t t-esc="state.value"/>]
|
||||
</button>`;
|
||||
|
||||
state = useState({ value: 0 });
|
||||
|
||||
increment() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `onMounted`
|
||||
|
||||
`onMounted` is not an hook, but is a building block designed to help make useful
|
||||
hooks. `onMounted` register a callback, which will be called when the component
|
||||
is mounted (see example on top of this page).
|
||||
|
||||
### `onWillUnmount`
|
||||
|
||||
`onWillUnmount` is not an hook, but is a building block designed to help make useful
|
||||
hooks. `onWillUnmount` register a callback, which will be called when the component
|
||||
is unmounted (see example on top of this page).
|
||||
|
||||
### `useRef`
|
||||
|
||||
The `useRef` hook is useful when we need a way to interact with some inside part
|
||||
of a component, rendered by Owl. It can work either on a DOM node, or on a component,
|
||||
tagged by the `t-ref` directive:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<div t-ref="someDiv"/>
|
||||
<SubComponent t-ref="someComponent"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
In this example, the component will be able to access the `div` and the component
|
||||
`SubComponent` using the `useRef` hook:
|
||||
|
||||
```js
|
||||
class Parent extends Component {
|
||||
subRef = useRef("someComponent");
|
||||
divRef = useRef("someDiv");
|
||||
|
||||
someMethod() {
|
||||
// here, if component is mounted, refs are active:
|
||||
// - this.divRef.el is the div HTMLElement
|
||||
// - this.subRef.comp is the instance of the sub component
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
As shown by the example above, html elements are accessed by using the `el`
|
||||
key, and components references are accessed with `comp`.
|
||||
|
||||
Note: if used on a component, the reference will be set in the `refs`
|
||||
variable between `willPatch` and `patched`.
|
||||
|
||||
The `t-ref` directive also accepts dynamic values with string interpolation
|
||||
(like the [`t-attf-`](qweb.md#dynamic-attributes) and
|
||||
`t-component` directives). For example,
|
||||
|
||||
```xml
|
||||
<div t-ref="component_{{someCondition ? '1' : '2'}}"/>
|
||||
```
|
||||
|
||||
Here, the references needs to be set like this:
|
||||
|
||||
```js
|
||||
this.ref1 = useRef("component_1");
|
||||
this.ref2 = useRef("component_2");
|
||||
```
|
||||
|
||||
References are only guaranteed to be active while the parent component is mounted.
|
||||
If this is not the case, accessing `el` or `comp` on it will return `null`.
|
||||
+3
-33
@@ -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.
|
||||
|
||||
+4
-2
@@ -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.template = "clickcounter";
|
||||
this.state = { value: 0 };
|
||||
this.state = useState({ value: 0 });
|
||||
}
|
||||
|
||||
increment() {
|
||||
|
||||
+22
-13
@@ -65,16 +65,16 @@ We present here a list of all standard QWeb directives:
|
||||
The component system in Owl requires additional directives, to express various
|
||||
needs. Here is a list of all Owl specific directives:
|
||||
|
||||
| Name | Description |
|
||||
| ------------------------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| `t-component`, `t-keepalive`, `t-asyncroot` | [Defining a sub component](component.md#composition) |
|
||||
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#references) |
|
||||
| `t-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) |
|
||||
| Name | Description |
|
||||
| ------------------------------------------------------ | ----------------------------------------------------------------------------------- |
|
||||
| `t-component`, `t-props`, `t-keepalive`, `t-asyncroot` | [Defining a sub component](component.md#composition) |
|
||||
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#references) |
|
||||
| `t-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) |
|
||||
|
||||
## QWeb Engine
|
||||
|
||||
@@ -101,7 +101,7 @@ It's API is quite simple:
|
||||
qweb.addTemplate("mytemplate", "<div>hello</div>");
|
||||
```
|
||||
|
||||
If the optional `allowDuplicate` is set to `true`, then `QWeb` will simply return whenever a template is added for a second time. Otherwise, `QWeb` will crash.
|
||||
If the optional `allowDuplicate` is set to `true`, then `QWeb` will simply return whenever a template is added for a second time. Otherwise, `QWeb` will crash.
|
||||
|
||||
- **`addTemplates(xmlStr)`**: add a list of templates (identified by `t-name`
|
||||
attribute).
|
||||
@@ -129,14 +129,23 @@ It's API is quite simple:
|
||||
const str = qweb.renderToString("someTemplate", somecontext);
|
||||
```
|
||||
|
||||
- **`register(name, Component)`**: static function to register an OWL Component
|
||||
- **`registerTemplate(name, template)`**: static function to register an global
|
||||
QWeb template. This is useful for commonly used components accross the
|
||||
application, and for making a template available to an application without
|
||||
having a reference to the actual QWeb instance.
|
||||
|
||||
```js
|
||||
QWeb.registerTemplate("mytemplate", `<div>some template`);
|
||||
```
|
||||
|
||||
- **`registerComponent(name, Component)`**: static function to register an OWL Component
|
||||
to QWeb's global registry. Globally registered Components can be used in
|
||||
templates (see the `t-component` directive). This is useful for commonly used
|
||||
components accross the application.
|
||||
|
||||
```js
|
||||
class Dialog extends owl.Component { ... }
|
||||
QWeb.register("Dialog", Dialog);
|
||||
QWeb.registerComponent("Dialog", Dialog);
|
||||
|
||||
...
|
||||
|
||||
|
||||
+15
-2
@@ -3,21 +3,30 @@
|
||||
## Owl Content
|
||||
|
||||
Owl is a javascript library that contains some core classes and function to help
|
||||
build applications. Here is a complete representation of its content:
|
||||
build applications. Here is a complete representation of its content:
|
||||
|
||||
```
|
||||
owl
|
||||
Component
|
||||
QWeb
|
||||
useState
|
||||
core
|
||||
EventBus
|
||||
Observer
|
||||
hooks
|
||||
onMounted
|
||||
onWillUnmount
|
||||
useState
|
||||
useRef
|
||||
router
|
||||
Link
|
||||
RouteComponent
|
||||
Router
|
||||
store
|
||||
Store
|
||||
ConnectedComponent
|
||||
tags
|
||||
xml
|
||||
utils
|
||||
debounce
|
||||
escape
|
||||
@@ -26,15 +35,19 @@ 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)
|
||||
- [Store](store.md)
|
||||
- [Tags](tags.md)
|
||||
- [Utils](utils.md)
|
||||
- [Virtual DOM](vdom.md)
|
||||
|
||||
@@ -46,4 +59,4 @@ owl
|
||||
|
||||
- [Comparison with React/Vue](comparison.md)
|
||||
- [Tooling](tooling.md)
|
||||
- [Templates to start Owl applications (external link)](https://github.com/ged-odoo/owl-templates)
|
||||
- [Templates to start Owl applications (external link)](https://github.com/ged-odoo/owl-templates)
|
||||
|
||||
+7
-8
@@ -8,7 +8,7 @@
|
||||
- [Route Definition](#route-definition)
|
||||
- [Router](#router)
|
||||
- [Navigation Guards](#navigation-guards)
|
||||
- [`t-routecomponent`](#t-routecomponent)
|
||||
- [RouteComponent](#routecomponent)
|
||||
- [Link](#link)
|
||||
|
||||
## Overview
|
||||
@@ -26,8 +26,8 @@ The Owl router support the following features:
|
||||
- route redirection
|
||||
- navigation guards
|
||||
- parameterized routes
|
||||
- a `<Link>` component
|
||||
- a `t-routecomponent` directive
|
||||
- a `<Link/>` component
|
||||
- a `<RouteComponent/>` component
|
||||
|
||||
Note that it is still in early stage of developments, and there are probably
|
||||
still some issues.
|
||||
@@ -125,7 +125,7 @@ The router also has a `navigate` method, useful to programmatically change the
|
||||
application to another state (and the url):
|
||||
|
||||
```js
|
||||
router.navigate({to: 'USER', params: {id: 51}});
|
||||
router.navigate({ to: "USER", params: { id: 51 } });
|
||||
```
|
||||
|
||||
### Navigation Guards
|
||||
@@ -148,19 +148,18 @@ async function protectRoute({ env, to }) {
|
||||
A navigation guard is a function that returns a promise, which either resolves
|
||||
to `true` (the navigation is accepted), or to another destination object.
|
||||
|
||||
### `t-routecomponent`
|
||||
### `RouteComponent`
|
||||
|
||||
The `t-routecomponent` directive directs Owl to render the component associated
|
||||
The `RouteComponent` component directs Owl to render the component associated
|
||||
to the currently active route (if any):
|
||||
|
||||
```xml
|
||||
<div t-name="App">
|
||||
<NavBar />
|
||||
<t t-routecomponent="1"/>
|
||||
<RouteComponent />
|
||||
</div>
|
||||
```
|
||||
|
||||
|
||||
### `Link`
|
||||
|
||||
The `Link` component is a Owl component which render as a `<a>` tag with any
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
# 🦉 Tags 🦉
|
||||
|
||||
Tags are very small helper to make it easy to write inline templates. There is
|
||||
only one currently available tag: `xml`, but we plan to add other tags later,
|
||||
such as a `css` tag, which will be used to write single file components.
|
||||
|
||||
## XML tag
|
||||
|
||||
Without tags, creating a standalone component would look like this:
|
||||
|
||||
```js
|
||||
import { Component } from 'owl'
|
||||
|
||||
const name = 'some-unique-name';
|
||||
const template = `
|
||||
<div>
|
||||
<span t-if="somecondition">text</span>
|
||||
<button t-on-click="someMethod">Click</button>
|
||||
</div>
|
||||
`;
|
||||
QWeb.registerTemplate(name, template);
|
||||
|
||||
class MyComponent extends Component {
|
||||
static template = name;
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
With tags, this process is slightly simplified. The name is uniquely generated,
|
||||
and the template is automatically registered:
|
||||
|
||||
```js
|
||||
import { Component } from 'owl'
|
||||
import { xml } from 'owl/tags'
|
||||
|
||||
class MyComponent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<span t-if="somecondition">text</span>
|
||||
<button t-on-click="someMethod">Click</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
+29
-1
@@ -38,7 +38,7 @@ Note that templates compiled with the `prod` settings will not be recompiled.
|
||||
So, changing this setting is best done at startup.
|
||||
|
||||
An important job done by the `dev` mode is to validate props for each component
|
||||
creation and update. Also, extra props will cause an error.
|
||||
creation and update. Also, extra props will cause an error.
|
||||
|
||||
## Playground
|
||||
|
||||
@@ -56,3 +56,31 @@ Note: This is more an internal tool, useful for people working on Owl.
|
||||
The benchmarks application is a very small application, implemented in different
|
||||
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
-3
@@ -18,9 +18,7 @@ not ready yet, resolved directly otherwise). If called with a callback as
|
||||
argument, it executes it as soon as the DOM ready (or directly).
|
||||
|
||||
```js
|
||||
Promise.all([loadTemplates(), owl.utils.whenReady()]).then(function([
|
||||
templates
|
||||
]) {
|
||||
Promise.all([loadTemplates(), owl.utils.whenReady()]).then(function([templates]) {
|
||||
const qweb = new owl.QWeb(templates);
|
||||
const app = new App({ qweb });
|
||||
app.mount(document.body);
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "owl",
|
||||
"version": "0.20.0",
|
||||
"name": "owl-framework",
|
||||
"version": "0.22.0",
|
||||
"description": "Odoo Web Library (OWL)",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
@@ -21,11 +21,12 @@
|
||||
"url": "git+https://github.com/odoo/owl.git"
|
||||
},
|
||||
"author": "Odoo",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"license": "LGPL",
|
||||
"bugs": {
|
||||
"url": "https://github.com/odoo/owl/issues"
|
||||
},
|
||||
"homepage": "https://github.com/odoo/owl#readme",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^23.3.12",
|
||||
"cpx": "^1.5.0",
|
||||
@@ -42,7 +43,6 @@
|
||||
"typescript": "^3.2.2",
|
||||
"uglify-es": "^3.3.9"
|
||||
},
|
||||
"dependencies": {},
|
||||
"jest": {
|
||||
"roots": [
|
||||
"<rootDir>/src",
|
||||
|
||||
+130
-115
@@ -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,28 @@ 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;
|
||||
willPatchResult: any;
|
||||
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,42 +71,37 @@ 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 };
|
||||
willUnmountCB: Function | null;
|
||||
classObj: { [key: string]: boolean } | null;
|
||||
refs: { [key: string]: Component<T, any> | HTMLElement | undefined } | null;
|
||||
}
|
||||
|
||||
// If a component does not define explicitely a template
|
||||
// key, it needs to find a template with its name (or a parent's). This is
|
||||
// qweb dependant, so we need a place to store this information indexed by
|
||||
// qweb instances.
|
||||
const TEMPLATE_MAP: { [key: number]: { [name: string]: string } } = {};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Component
|
||||
//------------------------------------------------------------------------------
|
||||
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>;
|
||||
template?: string;
|
||||
static template?: string | null = null;
|
||||
static _template?: string | null = null;
|
||||
static _current?: any | null = null;
|
||||
static components = {};
|
||||
static props?: any;
|
||||
static defaultProps?: any;
|
||||
|
||||
/**
|
||||
* The `el` is the root element of the component. Note that it could be null:
|
||||
@@ -94,17 +112,8 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
}
|
||||
|
||||
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
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -128,8 +137,9 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
* hand. Other components should be created automatically by the framework (with
|
||||
* 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);
|
||||
}
|
||||
@@ -139,7 +149,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
// Pro: but creating component (by a template) is always unsafe anyway
|
||||
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;
|
||||
@@ -173,14 +183,14 @@ 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: {},
|
||||
willUnmountCB: null,
|
||||
observer: null,
|
||||
render: null,
|
||||
classObj: null
|
||||
classObj: null,
|
||||
refs: null
|
||||
};
|
||||
}
|
||||
|
||||
@@ -283,17 +293,20 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
if (__owl__.isMounted) {
|
||||
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!);
|
||||
|
||||
@@ -327,18 +340,37 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
__createFiber(force, scope, vars, parent?: Fiber<any>): Fiber<Props> {
|
||||
const fiber: Fiber<Props> = {
|
||||
force,
|
||||
scope,
|
||||
vars,
|
||||
rootFiber: null,
|
||||
isCancelled: false,
|
||||
component: this,
|
||||
vnode: null,
|
||||
patchQueue: parent ? parent.patchQueue : [],
|
||||
willPatchResult: null,
|
||||
props: this.props,
|
||||
promise: null
|
||||
};
|
||||
fiber.rootFiber = parent ? parent.rootFiber : fiber;
|
||||
this.__owl__.currentFiber = fiber;
|
||||
return fiber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the component. This operation is quite complex:
|
||||
* - it recursively destroy all children
|
||||
@@ -420,7 +452,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
* Note that it does not call the __callWillUnmount method to avoid visiting
|
||||
* 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) {
|
||||
@@ -451,19 +483,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();
|
||||
for (let key in handlers) {
|
||||
handlers[key]();
|
||||
}
|
||||
} 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) {
|
||||
@@ -480,12 +515,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) {
|
||||
@@ -493,7 +527,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -507,14 +544,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) {
|
||||
@@ -526,45 +567,34 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
return Promise.resolve(h("div"));
|
||||
}
|
||||
const qweb = this.env.qweb;
|
||||
if (!this.template) {
|
||||
let tmap = TEMPLATE_MAP[qweb.id];
|
||||
if (!tmap) {
|
||||
tmap = {};
|
||||
TEMPLATE_MAP[qweb.id] = tmap;
|
||||
}
|
||||
let p = (<any>this).constructor;
|
||||
let name: string = p.name;
|
||||
let template = tmap[name];
|
||||
if (template) {
|
||||
this.template = template;
|
||||
let p = (<any>this).constructor;
|
||||
// console.warn(p, p.template, p._template, 'template' in p, p.hasOwnProperty('template'))
|
||||
if (!p.hasOwnProperty("_template")) {
|
||||
if (p.template) {
|
||||
p._template = p.template;
|
||||
} else {
|
||||
// here, the component and none of its superclasses defines a static `template`
|
||||
// key. So we fall back on looking for a template matching its name (or
|
||||
// one of its subclass).
|
||||
|
||||
let template: string;
|
||||
while ((template = p.name) && !(template in qweb.templates) && p !== Component) {
|
||||
p = p.__proto__;
|
||||
}
|
||||
if (p === Component) {
|
||||
throw new Error(`Could not find template for component "${this.constructor.name}"`);
|
||||
} else {
|
||||
tmap[name] = template;
|
||||
this.template = template;
|
||||
p._template = template;
|
||||
}
|
||||
}
|
||||
}
|
||||
__owl__.render = qweb.render.bind(qweb, this.template);
|
||||
this.__observeState();
|
||||
|
||||
return this.__render(false, [], scope, vars);
|
||||
__owl__.render = qweb.render.bind(qweb, p._template);
|
||||
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;
|
||||
}
|
||||
@@ -574,16 +604,13 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
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;
|
||||
}
|
||||
@@ -631,19 +658,6 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable the observe feature on the state. We only create an observer if
|
||||
* there is some state to be observed.
|
||||
*/
|
||||
__observeState() {
|
||||
if (this.state) {
|
||||
const __owl__ = this.__owl__;
|
||||
__owl__.observer = new Observer();
|
||||
this.state = __owl__.observer.observe(this.state);
|
||||
__owl__.observer.notifyCB = this.render.bind(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply default props (only top level).
|
||||
*
|
||||
@@ -661,29 +675,30 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the given patch queue. A patch is a pair [c, vn], where c is a
|
||||
* 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());
|
||||
const fiber = patchQueue[i];
|
||||
component = fiber.component;
|
||||
fiber.willPatchResult = 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]);
|
||||
const fiber = patchQueue[i];
|
||||
component = fiber.component;
|
||||
component.patched(fiber.willPatchResult);
|
||||
}
|
||||
} catch (e) {
|
||||
errorHandler(e, component);
|
||||
@@ -703,7 +718,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
* If there are no such component, we destroy everything. This is better than
|
||||
* 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;
|
||||
|
||||
+43
-22
@@ -1,4 +1,5 @@
|
||||
import { QWeb } from "../qweb/index";
|
||||
import { INTERP_REGEXP } from "../qweb/context";
|
||||
import { MODS_CODE } from "../qweb/extensions";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -194,6 +195,7 @@ QWeb.addDirective({
|
||||
ctx.rootContext.shouldDefineParent = true;
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
let keepAlive = node.getAttribute("t-keepalive") ? true : false;
|
||||
let hasDynamicProps = node.getAttribute("t-props") ? true : false;
|
||||
let async = node.getAttribute("t-asyncroot") ? true : false;
|
||||
|
||||
// t-on- events and t-transition
|
||||
@@ -261,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) {
|
||||
@@ -271,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 = () => {
|
||||
@@ -355,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);`
|
||||
);
|
||||
@@ -372,12 +379,19 @@ QWeb.addDirective({
|
||||
ctx.addLine(`result = vn${id};`);
|
||||
}
|
||||
}
|
||||
ctx.addLine(`let props${componentID} = {${propStr}};`);
|
||||
if (hasDynamicProps) {
|
||||
const dynamicProp = ctx.formatExpression(node.getAttribute("t-props")!);
|
||||
ctx.addLine(`let props${componentID} = Object.assign({${propStr}}, ${dynamicProp});`);
|
||||
} else {
|
||||
ctx.addLine(`let props${componentID} = {${propStr}};`);
|
||||
}
|
||||
ctx.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;`);
|
||||
@@ -386,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.components && context.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}];`
|
||||
`let W${componentID} = context.constructor.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}]${dynamicFallback};`
|
||||
);
|
||||
|
||||
// maybe only do this in dev mode...
|
||||
ctx.addLine(
|
||||
`if (!W${componentID}) {throw new Error('Cannot find the definition of component "' + componentKey${componentID} + '"')}`
|
||||
@@ -413,7 +433,7 @@ QWeb.addDirective({
|
||||
|
||||
const clone = <Element>node.cloneNode(true);
|
||||
const slotNodes = clone.querySelectorAll("[t-set]");
|
||||
const slotId = qweb.nextSlotId++;
|
||||
const slotId = QWeb.nextSlotId++;
|
||||
ctx.addLine(`w${componentID}.__owl__.slotId = ${slotId};`);
|
||||
if (slotNodes.length) {
|
||||
for (let i = 0, length = slotNodes.length; i < length; i++) {
|
||||
@@ -422,7 +442,7 @@ QWeb.addDirective({
|
||||
const key = slotNode.getAttribute("t-set")!;
|
||||
slotNode.removeAttribute("t-set");
|
||||
const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx);
|
||||
qweb.slots[`${slotId}_${key}`] = slotFn;
|
||||
QWeb.slots[`${slotId}_${key}`] = slotFn;
|
||||
}
|
||||
}
|
||||
if (clone.childNodes.length) {
|
||||
@@ -431,18 +451,19 @@ QWeb.addDirective({
|
||||
t.appendChild(child);
|
||||
}
|
||||
const slotFn = qweb._compile(`slot_default_template`, t, ctx);
|
||||
qweb.slots[`${slotId}_default`] = slotFn;
|
||||
QWeb.slots[`${slotId}_default`] = slotFn;
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -454,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 = "";
|
||||
@@ -486,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;
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { Component } from "./component/component";
|
||||
import { Observer } from "./core/observer";
|
||||
|
||||
/**
|
||||
* Owl Hook System
|
||||
*
|
||||
* This file introduces the concept of hooks, similar to React or Vue hooks.
|
||||
* We have currently an implementation of:
|
||||
* - useState (reactive state)
|
||||
* - onMounted
|
||||
* - onWillUnmount
|
||||
* - useRef
|
||||
*/
|
||||
|
||||
/**
|
||||
* useState hook
|
||||
*
|
||||
* This is the main way a component can be made reactive. The useState hook
|
||||
* will return an observed object (or array). Changes to that value will then
|
||||
* trigger a rerendering of the current component.
|
||||
*/
|
||||
export function useState<T>(state: T): T {
|
||||
const component: Component<any, any> = Component._current;
|
||||
const __owl__ = component.__owl__;
|
||||
if (!__owl__.observer) {
|
||||
__owl__.observer = new Observer();
|
||||
__owl__.observer.notifyCB = component.render.bind(component);
|
||||
}
|
||||
return __owl__.observer.observe(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mounted hook. The callback will be called when the current component is
|
||||
* mounted. Note that the component mounted method is called first.
|
||||
*/
|
||||
let nextID = 1;
|
||||
|
||||
export function onMounted(cb) {
|
||||
const component: Component<any, any> = Component._current;
|
||||
component.__owl__.mountedHandlers[`h${nextID++}`] = cb;
|
||||
}
|
||||
|
||||
/**
|
||||
* willUnmount hook. The callback will be called when the current component is
|
||||
* willUnmounted. Note that the component mounted method is called last.
|
||||
*/
|
||||
export function onWillUnmount(cb) {
|
||||
const component: Component<any, any> = Component._current;
|
||||
if (component.__owl__.willUnmountCB) {
|
||||
const current = component.__owl__.willUnmountCB;
|
||||
component.__owl__.willUnmountCB = function() {
|
||||
cb.call(component);
|
||||
current.call(component);
|
||||
};
|
||||
} else {
|
||||
component.__owl__.willUnmountCB = cb;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* useRef hook
|
||||
*
|
||||
* The purpose of this hook is to allow components to get a reference to a sub
|
||||
* html node or component.
|
||||
*/
|
||||
interface Ref {
|
||||
el: HTMLElement | null;
|
||||
comp: Component<any, any> | null;
|
||||
}
|
||||
|
||||
export function useRef(name: string): Ref {
|
||||
const __owl__ = Component._current.__owl__;
|
||||
return {
|
||||
get el(): HTMLElement | null {
|
||||
const val = __owl__.refs && __owl__.refs[name];
|
||||
return val instanceof HTMLElement ? val : null;
|
||||
},
|
||||
get comp(): Component<any, any> | null {
|
||||
const val = __owl__.refs && __owl__.refs[name];
|
||||
return val instanceof Component ? val : null;
|
||||
}
|
||||
};
|
||||
}
|
||||
+8
-2
@@ -10,16 +10,22 @@ import { QWeb } from "./qweb/index";
|
||||
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, Link };
|
||||
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", {
|
||||
|
||||
@@ -55,6 +55,7 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Context) {
|
||||
ctx.rootContext.rootNode = nodeID;
|
||||
ctx.rootContext.parentTextNode = nodeID;
|
||||
ctx.addLine(`var vn${nodeID} = {text: ${exprID}};`);
|
||||
ctx.addLine(`result = vn${nodeID}`);
|
||||
}
|
||||
} else {
|
||||
let fragID = ctx.generateID();
|
||||
@@ -200,6 +201,7 @@ QWeb.addDirective({
|
||||
// extract variables from nodecopy
|
||||
const tempCtx = new Context();
|
||||
tempCtx.nextID = ctx.rootContext.nextID;
|
||||
tempCtx.allowMultipleRoots = true;
|
||||
qweb._compileNode(nodeCopy, tempCtx);
|
||||
const vars = Object.assign({}, ctx.variables, tempCtx.variables);
|
||||
ctx.rootContext.nextID = tempCtx.nextID;
|
||||
@@ -242,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;
|
||||
}
|
||||
|
||||
+10
-2
@@ -1,5 +1,6 @@
|
||||
import { compileExpr, QWebVar } from "./expression_parser";
|
||||
|
||||
export const INTERP_REGEXP = /\{\{.*?\}\}/g;
|
||||
//------------------------------------------------------------------------------
|
||||
// Compilation Context
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -19,7 +20,8 @@ export class Context {
|
||||
shouldDefineParent: boolean = false;
|
||||
shouldDefineQWeb: boolean = false;
|
||||
shouldDefineUtils: boolean = false;
|
||||
shouldDefineResult: boolean = false;
|
||||
shouldDefineRefs: boolean = false;
|
||||
shouldDefineResult: boolean = true;
|
||||
shouldProtectContext: boolean = false;
|
||||
shouldTrackScope: boolean = false;
|
||||
inLoop: 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.
|
||||
@@ -93,6 +98,9 @@ export class Context {
|
||||
if (!this.rootContext.rootNode) {
|
||||
this.rootContext.rootNode = node;
|
||||
}
|
||||
if (!this.parentNode) {
|
||||
this.addLine(`result = vn${node};`);
|
||||
}
|
||||
return this.subContext("parentNode", node);
|
||||
}
|
||||
|
||||
@@ -160,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))})`;
|
||||
}
|
||||
|
||||
@@ -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;`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -227,7 +228,9 @@ QWeb.addDirective({
|
||||
atNodeEncounter({ ctx, value }): boolean {
|
||||
const slotKey = ctx.generateID();
|
||||
ctx.rootContext.shouldDefineOwner = true;
|
||||
ctx.addLine(`const slot${slotKey} = this.slots[context.__owl__.slotId + '_' + '${value}'];`);
|
||||
ctx.addLine(
|
||||
`const slot${slotKey} = this.constructor.slots[context.__owl__.slotId + '_' + '${value}'];`
|
||||
);
|
||||
ctx.addIf(`slot${slotKey}`);
|
||||
ctx.addLine(
|
||||
`slot${slotKey}.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, vars: extra.vars, parent: owner}));`
|
||||
@@ -252,7 +255,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};`);
|
||||
|
||||
+54
-19
@@ -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.";
|
||||
@@ -125,13 +132,11 @@ function parseXML(xml: string): Document {
|
||||
return doc;
|
||||
}
|
||||
|
||||
let nextID = 1;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// QWeb rendering engine
|
||||
//------------------------------------------------------------------------------
|
||||
export class QWeb extends EventBus {
|
||||
templates: { [name: string]: Template } = {};
|
||||
templates: { [name: string]: Template };
|
||||
static utils = UTILS;
|
||||
static components = Object.create(null);
|
||||
|
||||
@@ -143,19 +148,18 @@ export class QWeb extends EventBus {
|
||||
};
|
||||
static DIRECTIVES: Directive[] = [];
|
||||
|
||||
static TEMPLATES: { [name: string]: Template } = {};
|
||||
|
||||
static nextId: number = 1;
|
||||
|
||||
h = h;
|
||||
// dev mode enables better error messages or more costly validations
|
||||
static dev: boolean = false;
|
||||
|
||||
// the id field is useful to be able to hash qweb instances. The current
|
||||
// use case is that component's templates are qweb dependant, and need to be
|
||||
// able to map a qweb instance to a template name.
|
||||
id = nextID++;
|
||||
|
||||
// slots contains sub templates defined with t-set inside t-component nodes, and
|
||||
// are meant to be used by the t-slot directive.
|
||||
slots = {};
|
||||
nextSlotId = 1;
|
||||
static slots = {};
|
||||
static nextSlotId = 1;
|
||||
|
||||
// recursiveTemplates contains sub templates called with t-call, but which
|
||||
// ends up in recursive situations. This is very similar to the slot situation,
|
||||
@@ -166,6 +170,7 @@ export class QWeb extends EventBus {
|
||||
|
||||
constructor(data?: string) {
|
||||
super();
|
||||
this.templates = Object.create(QWeb.TEMPLATES);
|
||||
if (data) {
|
||||
this.addTemplates(data);
|
||||
}
|
||||
@@ -183,13 +188,27 @@ export class QWeb extends EventBus {
|
||||
}
|
||||
}
|
||||
|
||||
static register(name: string, Component: any) {
|
||||
static registerComponent(name: string, Component: any) {
|
||||
if (QWeb.components[name]) {
|
||||
throw new Error(`Component '${name}' has already been registered`);
|
||||
}
|
||||
QWeb.components[name] = Component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register globally a template. All QWeb instances will obtain their
|
||||
* templates from their own template map, and then, from the global static
|
||||
* TEMPLATES property.
|
||||
*/
|
||||
static registerTemplate(name: string, template: string) {
|
||||
if (QWeb.TEMPLATES[name]) {
|
||||
throw new Error(`Template '${name}' has already been registered`);
|
||||
}
|
||||
const qweb = new QWeb();
|
||||
qweb.addTemplate(name, template);
|
||||
QWeb.TEMPLATES[name] = qweb.templates[name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a template to the internal template map. Note that it is not
|
||||
* immediately compiled.
|
||||
@@ -229,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);
|
||||
@@ -297,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!;
|
||||
}
|
||||
@@ -326,24 +345,28 @@ export class QWeb extends EventBus {
|
||||
_compile(name: string, elem: Element, parentContext?: Context): CompiledTemplate {
|
||||
const isDebug = elem.attributes.hasOwnProperty("t-debug");
|
||||
const ctx = new Context(name);
|
||||
if (elem.tagName !== "t") {
|
||||
ctx.shouldDefineResult = false;
|
||||
}
|
||||
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;
|
||||
ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`);
|
||||
|
||||
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);
|
||||
|
||||
@@ -405,6 +428,7 @@ export class QWeb extends EventBus {
|
||||
// template rendering.
|
||||
let nodeID = ctx.generateID();
|
||||
ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`);
|
||||
ctx.addLine(`result = vn${nodeID};`);
|
||||
ctx.rootContext.rootNode = nodeID;
|
||||
ctx.rootContext.parentTextNode = nodeID;
|
||||
}
|
||||
@@ -517,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) {
|
||||
|
||||
+7
-7
@@ -1,18 +1,18 @@
|
||||
import { Component } from "../component/component";
|
||||
import { xml } from "../tags";
|
||||
import { Destination, RouterEnv } from "./Router";
|
||||
|
||||
export const LINK_TEMPLATE_NAME = "__owl__-router-link";
|
||||
export const LINK_TEMPLATE = `
|
||||
type Props = Destination;
|
||||
|
||||
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"
|
||||
t-on-click="navigate">
|
||||
<t t-slot="default"/>
|
||||
</a>`;
|
||||
</a>
|
||||
`;
|
||||
|
||||
type Props = Destination;
|
||||
|
||||
export class Link<Env extends RouterEnv> extends Component<Env, Props, {}> {
|
||||
template = LINK_TEMPLATE_NAME;
|
||||
href: string = this.env.router.destToPath(this.props);
|
||||
|
||||
async willUpdateProps(nextProps) {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Component } from "../component/component";
|
||||
import { xml } from "../tags";
|
||||
|
||||
export class RouteComponent extends Component<any, {}> {
|
||||
static template = xml`
|
||||
<t>
|
||||
<t
|
||||
t-if="routeComponent"
|
||||
t-component="routeComponent"
|
||||
t-key="env.router.currentRouteName"
|
||||
t-props="env.router.currentParams" />
|
||||
</t>
|
||||
`;
|
||||
|
||||
get routeComponent(): any {
|
||||
return this.env.router.currentRoute && this.env.router.currentRoute.component;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import { Env } from "../component/component";
|
||||
import { QWeb } from "../qweb/index";
|
||||
import { makeDirective } from "./directive";
|
||||
import { LINK_TEMPLATE, LINK_TEMPLATE_NAME } from "./Link";
|
||||
import { shallowEqual } from "../utils";
|
||||
|
||||
type NavigationGuard = (info: {
|
||||
@@ -75,7 +73,7 @@ export class Router {
|
||||
partialRoute.name = "__route__" + nextId++;
|
||||
}
|
||||
if (partialRoute.component) {
|
||||
QWeb.register("__component__" + partialRoute.name, partialRoute.component);
|
||||
QWeb.registerComponent("__component__" + partialRoute.name, partialRoute.component);
|
||||
}
|
||||
if (partialRoute.redirect) {
|
||||
this.validateDestination(partialRoute.redirect);
|
||||
@@ -84,10 +82,6 @@ export class Router {
|
||||
this.routes[partialRoute.name] = partialRoute as Route;
|
||||
this.routeIds.push(partialRoute.name);
|
||||
}
|
||||
|
||||
// setup link and directive
|
||||
env.qweb.addTemplate(LINK_TEMPLATE_NAME, LINK_TEMPLATE);
|
||||
QWeb.addDirective(makeDirective(<RouterEnv>env));
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { RouterEnv } from "./Router";
|
||||
|
||||
export function makeDirective(env: RouterEnv) {
|
||||
return {
|
||||
name: "routecomponent",
|
||||
priority: 13,
|
||||
atNodeEncounter({ node }): boolean {
|
||||
let first = true;
|
||||
const router = env.router;
|
||||
for (let name of router.routeIds) {
|
||||
const route = router.routes[name];
|
||||
if (route.component) {
|
||||
// make new t t-component element
|
||||
const comp = node.ownerDocument.createElement("t");
|
||||
comp.setAttribute("t-component", "__component__" + route.name);
|
||||
comp.setAttribute(
|
||||
first ? "t-if" : "t-elif",
|
||||
`env.router.currentRouteName === '${route.name}'`
|
||||
);
|
||||
first = false;
|
||||
for (let param of route.params) {
|
||||
comp.setAttribute(param, `env.router.currentParams.${param}`);
|
||||
}
|
||||
node.appendChild(comp);
|
||||
}
|
||||
}
|
||||
node.removeAttribute("t-routecomponent");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { QWeb } from "./qweb/index";
|
||||
|
||||
/**
|
||||
* Owl Tags
|
||||
*
|
||||
* We have here a (very) small collection of tag functions:
|
||||
*
|
||||
* - xml
|
||||
*
|
||||
* The plan is to add a few other tags such as css, globalcss.
|
||||
*/
|
||||
|
||||
/**
|
||||
* XML tag helper for defining templates. With this, one can simply define
|
||||
* an inline template with just the template xml:
|
||||
* ```js
|
||||
* class A extends Component {
|
||||
* static template = xml`<div>some template</div>`;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function xml(strings, ...args) {
|
||||
const name = `__template__${QWeb.nextId++}`;
|
||||
const value = String.raw(strings, ...args);
|
||||
QWeb.registerTemplate(name, value);
|
||||
return name;
|
||||
}
|
||||
+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);
|
||||
}
|
||||
|
||||
@@ -10,15 +10,16 @@ exports[`animations t-transition combined with component 1`] = `
|
||||
var h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
result = vn1;
|
||||
//COMPONENT
|
||||
let def3;
|
||||
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let props4 = {};
|
||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
||||
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
||||
def3 = w4.__owl__.renderPromise;
|
||||
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;
|
||||
@@ -26,17 +27,17 @@ exports[`animations t-transition combined with component 1`] = `
|
||||
}
|
||||
if (!w4) {
|
||||
let componentKey4 = \`Child\`;
|
||||
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
|
||||
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4]|| context['Child'];
|
||||
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
||||
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);
|
||||
@@ -54,6 +55,7 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
|
||||
var h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
result = vn1;
|
||||
if (context['state'].display) {
|
||||
//COMPONENT
|
||||
let def3;
|
||||
@@ -61,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;
|
||||
@@ -71,17 +73,17 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
|
||||
}
|
||||
if (!w4) {
|
||||
let componentKey4 = \`Child\`;
|
||||
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
|
||||
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4]|| context['Child'];
|
||||
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
||||
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);
|
||||
@@ -97,6 +99,7 @@ exports[`animations t-transition with no delay/duration 1`] = `
|
||||
var h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('span', p1, c1);
|
||||
result = vn1;
|
||||
p1.hook = {
|
||||
insert: vn => {
|
||||
utils.transitionInsert(vn, 'jupiler');
|
||||
@@ -117,6 +120,7 @@ exports[`animations t-transition, on a simple node (insert) 1`] = `
|
||||
var h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('span', p1, c1);
|
||||
result = vn1;
|
||||
p1.hook = {
|
||||
insert: vn => {
|
||||
utils.transitionInsert(vn, 'chimay');
|
||||
|
||||
+19
-21
@@ -1,5 +1,6 @@
|
||||
import { Component, Env } from "../src/component/component";
|
||||
import { QWeb } from "../src/qweb/index";
|
||||
import { useState, useRef } from "../src/hooks";
|
||||
import {
|
||||
makeDeferred,
|
||||
makeTestFixture,
|
||||
@@ -36,7 +37,7 @@ afterEach(() => {
|
||||
fixture.remove();
|
||||
});
|
||||
|
||||
class Widget extends Component<any, any, any> {}
|
||||
class Widget extends Component<any, any> {}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Tests
|
||||
@@ -106,7 +107,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 +149,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 +165,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
|
||||
@@ -175,10 +177,10 @@ describe("animations", () => {
|
||||
|
||||
env.qweb.addTemplate("Parent", `<div><Child t-transition="chimay"/></div>`);
|
||||
env.qweb.addTemplate("Child", `<span>blue</span>`);
|
||||
class Parent extends Widget {
|
||||
components = { Child: Child };
|
||||
}
|
||||
class Child extends Widget {}
|
||||
class Parent extends Widget {
|
||||
static components = { Child: Child };
|
||||
}
|
||||
const widget = new Parent(env);
|
||||
|
||||
let def = makeDeferred();
|
||||
@@ -214,11 +216,11 @@ describe("animations", () => {
|
||||
`<div><t t-if="state.display" t-component="Child" t-transition="chimay"/></div>`
|
||||
);
|
||||
env.qweb.addTemplate("Child", `<span>blue</span>`);
|
||||
class Parent extends Widget {
|
||||
components = { Child: Child };
|
||||
state = { display: true };
|
||||
}
|
||||
class Child extends Widget {}
|
||||
class Parent extends Widget {
|
||||
static components = { Child: Child };
|
||||
state = useState({ display: true });
|
||||
}
|
||||
const widget = new Parent(env);
|
||||
|
||||
let def = makeDeferred();
|
||||
@@ -269,7 +271,7 @@ describe("animations", () => {
|
||||
`<div><span t-if="state.flag" t-mounted="f" t-transition="chimay">blue</span></div>`
|
||||
);
|
||||
class TestWidget extends Widget {
|
||||
state = { flag: false };
|
||||
state = useState({ flag: false });
|
||||
f() {}
|
||||
}
|
||||
const widget = new TestWidget(env);
|
||||
@@ -297,10 +299,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;
|
||||
}
|
||||
@@ -356,11 +356,9 @@ describe("animations", () => {
|
||||
);
|
||||
class Child extends Widget {}
|
||||
class Parent extends Widget {
|
||||
components = { Child };
|
||||
constructor(parent) {
|
||||
super(parent);
|
||||
this.state = { flag: false };
|
||||
}
|
||||
static components = { Child };
|
||||
state = useState({ flag: false });
|
||||
|
||||
toggle() {
|
||||
this.state.flag = !this.state.flag;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,15 +14,16 @@ exports[`props validation props are validated in dev mode (code snapshot) 1`] =
|
||||
var h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
result = vn1;
|
||||
//COMPONENT
|
||||
let def3;
|
||||
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let props4 = {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;
|
||||
@@ -30,16 +31,16 @@ exports[`props validation props are validated in dev mode (code snapshot) 1`] =
|
||||
}
|
||||
if (!w4) {
|
||||
let componentKey4 = \`Child\`;
|
||||
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
|
||||
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4]|| context['Child'];
|
||||
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
||||
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);
|
||||
|
||||
+856
-640
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
|
||||
@@ -235,7 +235,7 @@ describe("props validation", () => {
|
||||
static props = ["message"];
|
||||
}
|
||||
class App extends Widget {
|
||||
components = { Child };
|
||||
static components = { Child };
|
||||
}
|
||||
const app = new App(env);
|
||||
await app.mount(fixture);
|
||||
@@ -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();
|
||||
|
||||
+30
-4
@@ -5,6 +5,27 @@ import "../src/qweb/base_directives";
|
||||
import "../src/qweb/extensions";
|
||||
import "../src/component/directive";
|
||||
|
||||
// Some static cleanup
|
||||
let nextSlotId;
|
||||
let slots;
|
||||
let nextId;
|
||||
let TEMPLATES;
|
||||
|
||||
beforeEach(() => {
|
||||
nextSlotId = QWeb.nextSlotId;
|
||||
slots = Object.assign({}, QWeb.slots);
|
||||
nextId = QWeb.nextId;
|
||||
TEMPLATES = Object.assign({}, QWeb.TEMPLATES);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
QWeb.nextSlotId = nextSlotId;
|
||||
QWeb.slots = slots;
|
||||
QWeb.nextId = nextId;
|
||||
QWeb.TEMPLATES = TEMPLATES;
|
||||
});
|
||||
|
||||
// helpers
|
||||
export function nextMicroTick(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
@@ -78,11 +99,16 @@ 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)) {
|
||||
throw new Error("HTML string returned by renderToString helper does not match QWeb render");
|
||||
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,152 @@
|
||||
import { makeTestEnv, makeTestFixture, nextTick } from "./helpers";
|
||||
import { Component, Env } from "../src/component/component";
|
||||
import { useState, onMounted, onWillUnmount, useRef } from "../src/hooks";
|
||||
import { xml } from "../src/tags";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Setup and helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// We create before each test:
|
||||
// - fixture: a div, appended to the DOM, intended to be the target of dom
|
||||
// manipulations. Note that it is removed after each test.
|
||||
// - env: a WEnv, necessary to create new components
|
||||
|
||||
let fixture: HTMLElement;
|
||||
let env: Env;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = makeTestFixture();
|
||||
env = makeTestEnv();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.remove();
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Tests
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
describe("hooks", () => {
|
||||
test("can use a state hook", async () => {
|
||||
class Counter extends Component<any, any> {
|
||||
static template = xml`<div><t t-esc="counter.value"/></div>`;
|
||||
counter = useState({ value: 42 });
|
||||
}
|
||||
const counter = new Counter(env);
|
||||
await counter.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div>42</div>");
|
||||
counter.counter.value = 3;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div>3</div>");
|
||||
});
|
||||
|
||||
test("can use onMounted, onWillUnmount", async () => {
|
||||
const steps: string[] = [];
|
||||
function useMyHook() {
|
||||
onMounted(() => {
|
||||
steps.push("mounted");
|
||||
});
|
||||
onWillUnmount(() => {
|
||||
steps.push("willunmount");
|
||||
});
|
||||
}
|
||||
class MyComponent extends Component<any, any> {
|
||||
static template = xml`<div>hey</div>`;
|
||||
constructor(env) {
|
||||
super(env);
|
||||
useMyHook();
|
||||
}
|
||||
}
|
||||
const component = new MyComponent(env);
|
||||
await component.mount(fixture);
|
||||
expect(component).not.toHaveProperty("mounted");
|
||||
expect(component).not.toHaveProperty("willUnmount");
|
||||
expect(fixture.innerHTML).toBe("<div>hey</div>");
|
||||
expect(steps).toEqual(["mounted"]);
|
||||
component.unmount();
|
||||
expect(fixture.innerHTML).toBe("");
|
||||
expect(steps).toEqual(["mounted", "willunmount"]);
|
||||
});
|
||||
|
||||
test("mounted, willUnmount, onMounted, onWillUnmount order", async () => {
|
||||
const steps: string[] = [];
|
||||
function useMyHook() {
|
||||
onMounted(() => {
|
||||
steps.push("hook:mounted");
|
||||
});
|
||||
onWillUnmount(() => {
|
||||
steps.push("hook:willunmount");
|
||||
});
|
||||
}
|
||||
class MyComponent extends Component<any, any> {
|
||||
static template = xml`<div>hey</div>`;
|
||||
constructor(env) {
|
||||
super(env);
|
||||
useMyHook();
|
||||
}
|
||||
mounted() {
|
||||
steps.push("comp:mounted");
|
||||
}
|
||||
willUnmount() {
|
||||
steps.push("comp:willunmount");
|
||||
}
|
||||
}
|
||||
const component = new MyComponent(env);
|
||||
await component.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div>hey</div>");
|
||||
component.unmount();
|
||||
expect(fixture.innerHTML).toBe("");
|
||||
expect(steps).toEqual(["comp:mounted", "hook:mounted", "hook:willunmount", "comp:willunmount"]);
|
||||
});
|
||||
|
||||
test("two different call to mounted/willunmount should work", async () => {
|
||||
const steps: string[] = [];
|
||||
function useMyHook(i) {
|
||||
onMounted(() => {
|
||||
steps.push("hook:mounted" + i);
|
||||
});
|
||||
onWillUnmount(() => {
|
||||
steps.push("hook:willunmount" + i);
|
||||
});
|
||||
}
|
||||
class MyComponent extends Component<any, any> {
|
||||
static template = xml`<div>hey</div>`;
|
||||
constructor(env) {
|
||||
super(env);
|
||||
useMyHook(1);
|
||||
useMyHook(2);
|
||||
}
|
||||
}
|
||||
const component = new MyComponent(env);
|
||||
await component.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div>hey</div>");
|
||||
component.unmount();
|
||||
expect(fixture.innerHTML).toBe("");
|
||||
expect(steps).toEqual([
|
||||
"hook:mounted1",
|
||||
"hook:mounted2",
|
||||
"hook:willunmount2",
|
||||
"hook:willunmount1"
|
||||
]);
|
||||
});
|
||||
|
||||
test("useRef hook", async () => {
|
||||
class Counter extends Component<any, any> {
|
||||
static template = xml`<div><button t-ref="button"><t t-esc="value"/></button></div>`;
|
||||
button = useRef("button");
|
||||
value = 0;
|
||||
increment() {
|
||||
this.value++;
|
||||
(this.button.el as HTMLButtonElement).innerHTML = String(this.value);
|
||||
}
|
||||
}
|
||||
const counter = new Counter(env);
|
||||
await counter.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><button>0</button></div>");
|
||||
counter.increment();
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><button>1</button></div>");
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
+84
-9
@@ -11,6 +11,7 @@ import { normalize, renderToDOM, renderToString, trim, nextTick } from "../helpe
|
||||
let qweb: QWeb;
|
||||
|
||||
beforeEach(() => {
|
||||
QWeb.TEMPLATES = {};
|
||||
qweb = new QWeb();
|
||||
});
|
||||
|
||||
@@ -48,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", () => {
|
||||
@@ -574,6 +580,24 @@ describe("t-call (template calling", () => {
|
||||
expect(trim(renderToString(qweb, "caller"))).toBe(expected);
|
||||
});
|
||||
|
||||
test("call with several sub nodes on same line", () => {
|
||||
qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="SubTemplate">
|
||||
<t t-raw="0"/>
|
||||
</div>
|
||||
|
||||
<div t-name="main">
|
||||
<t t-call="SubTemplate">
|
||||
<span>hey</span> <span>yay</span>
|
||||
</t>
|
||||
</div>
|
||||
</templates>
|
||||
`);
|
||||
const expected = "<div><div><span>hey</span> <span>yay</span></div></div>";
|
||||
expect(renderToString(qweb, "main")).toBe(expected);
|
||||
});
|
||||
|
||||
test("recursive template, part 1", () => {
|
||||
qweb.addTemplates(`
|
||||
<templates>
|
||||
@@ -589,7 +613,6 @@ describe("t-call (template calling", () => {
|
||||
expect(renderToString(qweb, "recursive")).toBe(expected);
|
||||
const recursiveFn = Object.values(qweb.recursiveFns)[0];
|
||||
expect(recursiveFn.toString()).toMatchSnapshot();
|
||||
|
||||
});
|
||||
|
||||
test("recursive template, part 2", () => {
|
||||
@@ -611,9 +634,11 @@ describe("t-call (template calling", () => {
|
||||
|
||||
</templates>
|
||||
`);
|
||||
const root = { val: "a", children: [{val: "b"}, {val: "c"}]};
|
||||
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();
|
||||
});
|
||||
@@ -637,12 +662,20 @@ describe("t-call (template calling", () => {
|
||||
|
||||
</templates>
|
||||
`);
|
||||
const root = { val: "a", children: [{val: "b", children: [{val: "d"}]}, {val: "c"}]};
|
||||
const expected = "<div><div><p>a</p><div><p>b</p><div><p>d</p></div></div><div><p>c</p></div></div></div>";
|
||||
expect(renderToString(qweb, "Parent", {root })).toBe(expected);
|
||||
const root = { val: "a", children: [{ val: "b", children: [{ val: "d" }] }, { val: "c" }] };
|
||||
const expected =
|
||||
"<div><div><p>a</p><div><p>b</p><div><p>d</p></div></div><div><p>c</p></div></div></div>";
|
||||
expect(renderToString(qweb, "Parent", { root }, { fiber: {} })).toBe(expected);
|
||||
const recursiveFn = Object.values(qweb.recursiveFns)[0];
|
||||
expect(recursiveFn.toString()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("t-call, global templates", () => {
|
||||
QWeb.registerTemplate("abcd", '<div><t t-call="john"/></div>');
|
||||
qweb.addTemplate("john", `<span>desk</span>`);
|
||||
const expected = "<div><span>desk</span></div>";
|
||||
expect(trim(renderToString(qweb, "abcd"))).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("foreach", () => {
|
||||
@@ -1059,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");
|
||||
});
|
||||
|
||||
@@ -1081,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"]);
|
||||
});
|
||||
});
|
||||
@@ -1260,3 +1293,45 @@ describe("update on event bus", () => {
|
||||
expect(fn).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("global template registration", () => {
|
||||
test("can register template globally", () => {
|
||||
expect.assertions(5);
|
||||
let qweb = new QWeb();
|
||||
try {
|
||||
qweb.render("mytemplate");
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch("Template mytemplate does not exist");
|
||||
}
|
||||
expect(qweb.templates.mytemplate).toBeUndefined();
|
||||
|
||||
QWeb.registerTemplate("mytemplate", "<div>global</div>");
|
||||
expect(qweb.templates.mytemplate).toBeDefined();
|
||||
const vnode = qweb.render("mytemplate");
|
||||
expect(vnode.sel).toBe("div");
|
||||
expect((vnode as any).children[0].text).toBe("global");
|
||||
});
|
||||
});
|
||||
|
||||
describe("properly support svg", () => {
|
||||
test("add proper namespace to svg", () => {
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<svg width="100px" height="90px"><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/> </svg>`
|
||||
);
|
||||
expect(renderToString(qweb, "test")).toBe(
|
||||
`<svg width=\"100px\" height=\"90px\"><circle cx=\"50\" cy=\"50\" r=\"4\" stroke=\"green\" stroke-width=\"1\" fill=\"yellow\"></circle> </svg>`
|
||||
);
|
||||
});
|
||||
|
||||
test("add proper namespace to g tags", () => {
|
||||
// this is necessary if one wants to use components in a svg
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<g><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/> </g>`
|
||||
);
|
||||
expect(renderToString(qweb, "test")).toBe(
|
||||
`<g><circle cx=\"50\" cy=\"50\" r=\"4\" stroke=\"green\" stroke-width=\"1\" fill=\"yellow\"></circle> </g>`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component } from "../../src/component/component";
|
||||
import { Link, LINK_TEMPLATE_NAME } from "../../src/router/Link";
|
||||
import { Link } from "../../src/router/Link";
|
||||
import { RouterEnv } from "../../src/router/Router";
|
||||
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
|
||||
import { TestRouter } from "./TestRouter";
|
||||
@@ -30,8 +30,8 @@ describe("Link component", () => {
|
||||
</div>
|
||||
</templates>
|
||||
`);
|
||||
class App extends Component<any, any, any> {
|
||||
components = { Link: Link };
|
||||
class App extends Component<any, any> {
|
||||
static components = { Link: Link };
|
||||
}
|
||||
|
||||
const routes = [{ name: "about", path: "/about" }, { name: "users", path: "/users" }];
|
||||
@@ -50,7 +50,7 @@ describe("Link component", () => {
|
||||
'<div><a href="/about" class="router-link-active">About</a></div>'
|
||||
);
|
||||
|
||||
expect(env.qweb.templates[LINK_TEMPLATE_NAME].fn.toString()).toMatchSnapshot();
|
||||
expect(env.qweb.templates[Link.template].fn.toString()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("do not redirect if right clicking", async () => {
|
||||
@@ -61,8 +61,8 @@ describe("Link component", () => {
|
||||
</div>
|
||||
</templates>
|
||||
`);
|
||||
class App extends Component<any, any, any> {
|
||||
components = { Link: Link };
|
||||
class App extends Component<any, any> {
|
||||
static components = { Link: Link };
|
||||
}
|
||||
|
||||
const routes = [{ name: "about", path: "/about" }, { name: "users", path: "/users" }];
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Component } from "../../src/component/component";
|
||||
import { RouterEnv } from "../../src/router/Router";
|
||||
import { RouteComponent } from "../../src/router/RouteComponent";
|
||||
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
|
||||
import { TestRouter } from "./TestRouter";
|
||||
|
||||
describe("router directive t-routecomponent", () => {
|
||||
describe("RouteComponent", () => {
|
||||
let fixture: HTMLElement;
|
||||
let env: RouterEnv;
|
||||
let router: TestRouter | null = null;
|
||||
@@ -25,16 +26,16 @@ describe("router directive t-routecomponent", () => {
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="App">
|
||||
<t t-routecomponent="1" />
|
||||
<RouteComponent />
|
||||
</div>
|
||||
<span t-name="About">About</span>
|
||||
<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> {
|
||||
components = { About, Users };
|
||||
class About extends Component<any, any> {}
|
||||
class Users extends Component<any, any> {}
|
||||
class App extends Component<any, any> {
|
||||
static components = { RouteComponent };
|
||||
}
|
||||
|
||||
const routes = [
|
||||
@@ -51,21 +52,21 @@ describe("router directive t-routecomponent", () => {
|
||||
await router.navigate({ to: "users" });
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><span>Users</span></div>");
|
||||
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
|
||||
expect(env.qweb.templates[RouteComponent.template].fn.toString()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("can render parameterized route", async () => {
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="App">
|
||||
<t t-routecomponent="1" />
|
||||
<RouteComponent />
|
||||
</div>
|
||||
<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> {
|
||||
components = { Book };
|
||||
class Book extends Component<any, any> {}
|
||||
class App extends Component<any, any> {
|
||||
static components = { RouteComponent };
|
||||
}
|
||||
|
||||
const routes = [{ name: "book", path: "/book/{{title}}", component: Book }];
|
||||
@@ -74,25 +75,24 @@ describe("router directive t-routecomponent", () => {
|
||||
const app = new App(env);
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><span>Book 1984</span></div>");
|
||||
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("can render parameterized route with suffixes", async () => {
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="App">
|
||||
<t t-routecomponent="1" />
|
||||
<RouteComponent />
|
||||
</div>
|
||||
<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> {
|
||||
components = { Book };
|
||||
class App extends Component<any, any> {
|
||||
static components = { RouteComponent };
|
||||
}
|
||||
|
||||
const routes = [{ name: "book", path: "/book/{{title}}/{{val.number}}", component: Book }];
|
||||
@@ -101,6 +101,5 @@ describe("router directive t-routecomponent", () => {
|
||||
const app = new App(env);
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><span>Book 1984|124</span></div>");
|
||||
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -4,8 +4,6 @@ import { QWeb } from "../../src/qweb/index";
|
||||
export class TestRouter extends Router {
|
||||
destroy() {
|
||||
window.removeEventListener("popstate", (this as any)._listener);
|
||||
delete QWeb.DIRECTIVE_NAMES.routecomponent;
|
||||
QWeb.DIRECTIVES = QWeb.DIRECTIVES.filter(d => d.name !== "routecomponent");
|
||||
|
||||
// remove component defined inroutes
|
||||
for (let key in QWeb.components) {
|
||||
|
||||
@@ -10,12 +10,13 @@ exports[`Link component can render simple cases 1`] = `
|
||||
var _2 = context['href'];
|
||||
let c3 = [], p3 = {key:3,attrs:{href: _2},class:_1,on:{}};
|
||||
var vn3 = h('a', p3, c3);
|
||||
result = vn3;
|
||||
if (!context['navigate']) {
|
||||
throw new Error('Missing handler \\\\'' + 'navigate' + \`\\\\' when evaluating template '__owl__-router-link'\`)
|
||||
throw new Error('Missing handler \\\\'' + 'navigate' + \`\\\\' when evaluating template '__template__1'\`)
|
||||
}
|
||||
extra.handlers['click' + 3] = extra.handlers['click' + 3] || context['navigate'].bind(owner);
|
||||
p3.on['click'] = extra.handlers['click' + 3];
|
||||
const slot4 = this.slots[context.__owl__.slotId + '_' + 'default'];
|
||||
const slot4 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
|
||||
if (slot4) {
|
||||
slot4.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c3, vars: extra.vars, parent: owner}));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`RouteComponent can render simple cases 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let utils = this.constructor.utils;
|
||||
let QWeb = this.constructor;
|
||||
let parent = context;
|
||||
let owner = context;
|
||||
let result;
|
||||
var h = this.h;
|
||||
if (context['routeComponent']) {
|
||||
//COMPONENT
|
||||
let key4 = 'key' + context['env'].router.currentRouteName;
|
||||
let def2;
|
||||
let templateId5 = key4;
|
||||
let w3 = templateId5 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId5]] : false;
|
||||
let vn6 = {};
|
||||
result = vn6;
|
||||
let props3 = Object.assign({}, context['env'].router.currentParams);
|
||||
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
|
||||
if (utils.shallowEqual(props3, w3.__owl__.currentFiber.props)) {
|
||||
def2 = w3.__owl__.currentFiber.promise;
|
||||
} else {
|
||||
w3.destroy();
|
||||
w3 = false;
|
||||
}
|
||||
}
|
||||
if (!w3) {
|
||||
let componentKey3 = \`routeComponent\`;
|
||||
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['routeComponent'];
|
||||
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
|
||||
w3 = new W3(parent, props3);
|
||||
parent.__owl__.cmap[templateId5] = w3.__owl__.id;
|
||||
def2 = w3.__prepare(extra.fiber, undefined, undefined);
|
||||
def2 = def2.then(vnode=>{if (w3.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: templateId5, hook: {insert(vn) {let nvn=w3.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}});utils.defineProxy(vn6, pvnode);w3.__owl__.pvnode = pvnode;});
|
||||
} else {
|
||||
def2 = def2 || w3.__updateProps(props3, extra.fiber, undefined, undefined);
|
||||
def2 = def2.then(()=>{if (w3.__owl__.isDestroyed) {return};let pvnode=w3.__owl__.pvnode;utils.defineProxy(vn6, pvnode);});
|
||||
}
|
||||
extra.promises.push(def2);
|
||||
}
|
||||
return result;
|
||||
}"
|
||||
`;
|
||||
@@ -1,159 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`router directive t-routecomponent can render parameterized route 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let utils = this.constructor.utils;
|
||||
let QWeb = this.constructor;
|
||||
let parent = context;
|
||||
let owner = context;
|
||||
var h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
if (context['env'].router.currentRouteName==='book') {
|
||||
//COMPONENT
|
||||
let def3;
|
||||
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let props4 = {title:context['env'].router.currentParams.title};
|
||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
||||
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
||||
def3 = w4.__owl__.renderPromise;
|
||||
} else {
|
||||
w4.destroy();
|
||||
w4 = false;
|
||||
}
|
||||
}
|
||||
if (!w4) {
|
||||
let componentKey4 = \`__component__book\`;
|
||||
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
|
||||
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
||||
w4 = new W4(parent, props4);
|
||||
parent.__owl__.cmap[4] = w4.__owl__.id;
|
||||
def3 = w4.__prepare();
|
||||
def3 = def3.then(vnode=>{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 {
|
||||
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
||||
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
||||
}
|
||||
extra.promises.push(def3);
|
||||
}
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`router directive t-routecomponent can render parameterized route with suffixes 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let utils = this.constructor.utils;
|
||||
let QWeb = this.constructor;
|
||||
let parent = context;
|
||||
let owner = context;
|
||||
var h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
if (context['env'].router.currentRouteName==='book') {
|
||||
//COMPONENT
|
||||
let def3;
|
||||
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let props4 = {title:context['env'].router.currentParams.title,val:context['env'].router.currentParams.val};
|
||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
||||
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
||||
def3 = w4.__owl__.renderPromise;
|
||||
} else {
|
||||
w4.destroy();
|
||||
w4 = false;
|
||||
}
|
||||
}
|
||||
if (!w4) {
|
||||
let componentKey4 = \`__component__book\`;
|
||||
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
|
||||
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
||||
w4 = new W4(parent, props4);
|
||||
parent.__owl__.cmap[4] = w4.__owl__.id;
|
||||
def3 = w4.__prepare();
|
||||
def3 = def3.then(vnode=>{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 {
|
||||
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
||||
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
||||
}
|
||||
extra.promises.push(def3);
|
||||
}
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`router directive t-routecomponent can render simple cases 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let utils = this.constructor.utils;
|
||||
let QWeb = this.constructor;
|
||||
let parent = context;
|
||||
let owner = context;
|
||||
var h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
if (context['env'].router.currentRouteName==='about') {
|
||||
//COMPONENT
|
||||
let def3;
|
||||
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let props4 = {};
|
||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
||||
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
||||
def3 = w4.__owl__.renderPromise;
|
||||
} else {
|
||||
w4.destroy();
|
||||
w4 = false;
|
||||
}
|
||||
}
|
||||
if (!w4) {
|
||||
let componentKey4 = \`__component__about\`;
|
||||
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
|
||||
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
||||
w4 = new W4(parent, props4);
|
||||
parent.__owl__.cmap[4] = w4.__owl__.id;
|
||||
def3 = w4.__prepare();
|
||||
def3 = def3.then(vnode=>{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 {
|
||||
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
||||
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
||||
}
|
||||
extra.promises.push(def3);
|
||||
}
|
||||
else if (context['env'].router.currentRouteName==='users') {
|
||||
//COMPONENT
|
||||
let def6;
|
||||
let w7 = 7 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[7]] : false;
|
||||
let _5_index = c1.length;
|
||||
c1.push(null);
|
||||
let props7 = {};
|
||||
if (w7 && w7.__owl__.renderPromise && !w7.__owl__.vnode) {
|
||||
if (utils.shallowEqual(props7, w7.__owl__.renderProps)) {
|
||||
def6 = w7.__owl__.renderPromise;
|
||||
} else {
|
||||
w7.destroy();
|
||||
w7 = false;
|
||||
}
|
||||
}
|
||||
if (!w7) {
|
||||
let componentKey7 = \`__component__users\`;
|
||||
let W7 = context.components && context.components[componentKey7] || QWeb.components[componentKey7];
|
||||
if (!W7) {throw new Error('Cannot find the definition of component \\"' + componentKey7 + '\\"')}
|
||||
w7 = new W7(parent, props7);
|
||||
parent.__owl__.cmap[7] = w7.__owl__.id;
|
||||
def6 = w7.__prepare();
|
||||
def6 = def6.then(vnode=>{if (w7.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: 7, hook: {insert(vn) {let nvn=w7.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w7.destroy();}}});c1[_5_index]=pvnode;w7.__owl__.pvnode = pvnode;});
|
||||
} else {
|
||||
def6 = def6 || w7.__updateProps(props7, extra.forceUpdate, extra.patchQueue);
|
||||
def6 = def6.then(()=>{if (w7.__owl__.isDestroyed) {return};let pvnode=w7.__owl__.pvnode;c1[_5_index]=pvnode;});
|
||||
}
|
||||
extra.promises.push(def6);
|
||||
}
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
@@ -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,13 +28,13 @@ describe("connecting a component to store", () => {
|
||||
<span t-name="Todo"><t t-esc="props.msg"/></span>
|
||||
</templates>
|
||||
`);
|
||||
class App extends ConnectedComponent<any, any, any> {
|
||||
components = { Todo };
|
||||
class Todo extends Component<any, any> {}
|
||||
class App extends ConnectedComponent<any, any> {
|
||||
static components = { Todo };
|
||||
static mapStoreToProps(s) {
|
||||
return { todos: s.todos };
|
||||
}
|
||||
}
|
||||
class Todo extends Component<any, any, any> {}
|
||||
const state = { todos: [] };
|
||||
const actions = {
|
||||
addTodo({ state }, msg) {
|
||||
@@ -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,8 +122,8 @@ describe("connecting a component to store", () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
class App extends ConnectedComponent<any, any, any> {
|
||||
components = { Todo };
|
||||
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> {
|
||||
components = { Child };
|
||||
|
||||
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,15 +241,15 @@ 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> {
|
||||
components = { TodoItem };
|
||||
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,8 +305,8 @@ describe("connecting a component to store", () => {
|
||||
}
|
||||
}
|
||||
|
||||
class TodoList extends ConnectedComponent<any, any, any> {
|
||||
components = { TodoItem };
|
||||
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> {
|
||||
components = { Beer };
|
||||
state = { beerId: 1 };
|
||||
class App extends Component<any, any> {
|
||||
static components = { Beer };
|
||||
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> {
|
||||
components = { Beer };
|
||||
state = { beerId: 0 };
|
||||
class App extends Component<any, any> {
|
||||
static components = { Beer };
|
||||
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> {
|
||||
components = { Beer };
|
||||
state = { beerId: 0 };
|
||||
class App extends Component<any, any> {
|
||||
static components = { Beer };
|
||||
state = useState({ beerId: 0 });
|
||||
}
|
||||
|
||||
const actions = {
|
||||
@@ -559,20 +560,19 @@ describe("connecting a component to store", () => {
|
||||
</templates>
|
||||
`);
|
||||
|
||||
class Parent extends ConnectedComponent<any, any, any> {
|
||||
components = { Child };
|
||||
static mapStoreToProps(s) {
|
||||
steps.push("parent");
|
||||
return { current: s.current, isvisible: s.isvisible };
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
static components = { Child };
|
||||
static mapStoreToProps(s) {
|
||||
steps.push("parent");
|
||||
return { current: s.current, isvisible: s.isvisible };
|
||||
}
|
||||
}
|
||||
|
||||
const state = { current: "a", msg: { a: "a", b: "b" } };
|
||||
const actions = {
|
||||
@@ -609,8 +609,14 @@ describe("connecting a component to store", () => {
|
||||
</templates>
|
||||
`);
|
||||
|
||||
class Parent extends ConnectedComponent<any, any, any> {
|
||||
components = { Child };
|
||||
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> {
|
||||
static components = { Child };
|
||||
static mapStoreToProps(s) {
|
||||
steps.push("parent");
|
||||
return { flag: s.flag, someId: s.someId };
|
||||
@@ -621,13 +627,6 @@ describe("connecting a component to store", () => {
|
||||
}
|
||||
}
|
||||
|
||||
class Child extends ConnectedComponent<any, any, any> {
|
||||
static mapStoreToProps(s, props) {
|
||||
steps.push("child");
|
||||
return { msg: s.messages[props.someId] };
|
||||
}
|
||||
}
|
||||
|
||||
const state = { someId: 1, flag: true, messages: { 1: "abc" } };
|
||||
const actions = {
|
||||
setFlagToFalse({ state }) {
|
||||
@@ -685,19 +684,10 @@ describe("connecting a component to store", () => {
|
||||
</templates>
|
||||
`);
|
||||
|
||||
class TodoApp extends ConnectedComponent<any, any, any> {
|
||||
components = { TodoItem };
|
||||
static mapStoreToProps(state) {
|
||||
return {
|
||||
todos: state.todos
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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++;
|
||||
@@ -709,9 +699,17 @@ 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> {
|
||||
static components = { TodoItem };
|
||||
static mapStoreToProps(state) {
|
||||
return {
|
||||
todos: state.todos
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -764,19 +762,10 @@ describe("connecting a component to store", () => {
|
||||
</templates>
|
||||
`);
|
||||
|
||||
class TodoApp extends ConnectedComponent<any, any, any> {
|
||||
components = { TodoItem };
|
||||
static mapStoreToProps(state) {
|
||||
return {
|
||||
todos: state.todos
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -788,9 +777,18 @@ 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> {
|
||||
static components = { TodoItem };
|
||||
static mapStoreToProps(state) {
|
||||
return {
|
||||
todos: state.todos
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -820,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 };
|
||||
}
|
||||
@@ -874,12 +872,12 @@ 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> {
|
||||
components = { Greeter };
|
||||
class App extends Component<any, any> {
|
||||
static components = { Greeter };
|
||||
}
|
||||
|
||||
const store = new Store({ state: {} });
|
||||
@@ -898,12 +896,12 @@ 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> {
|
||||
components = { Greeter };
|
||||
class App extends Component<any, any> {
|
||||
static components = { Greeter };
|
||||
}
|
||||
|
||||
const store = new Store({ state: {} });
|
||||
@@ -913,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>");
|
||||
});
|
||||
@@ -938,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 {
|
||||
@@ -947,8 +946,8 @@ describe("connected components and default values", () => {
|
||||
};
|
||||
}
|
||||
|
||||
class Thread extends ConnectedComponent<any, any, any> {
|
||||
components = { Message };
|
||||
class Thread extends ConnectedComponent<any, any> {
|
||||
static components = { Message };
|
||||
static defaultProps = { showMessages: true };
|
||||
static mapStoreToProps = function(state, ownProps) {
|
||||
const thread = state.threads[ownProps.threadId];
|
||||
@@ -958,8 +957,8 @@ describe("connected components and default values", () => {
|
||||
};
|
||||
}
|
||||
|
||||
class App extends Component<any, any, any> {
|
||||
components = { Thread };
|
||||
class App extends Component<any, any> {
|
||||
static components = { Thread };
|
||||
static defaultProps = { threadId: 1 };
|
||||
}
|
||||
|
||||
@@ -1000,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>");
|
||||
|
||||
@@ -1019,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> {
|
||||
components = { Child };
|
||||
state = { child: true };
|
||||
class Parent extends Component<any, any> {
|
||||
static components = { Child };
|
||||
state = useState({ child: true });
|
||||
}
|
||||
|
||||
class TestStore extends Store {
|
||||
@@ -1063,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
|
||||
@@ -1144,20 +1144,20 @@ 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
|
||||
};
|
||||
}
|
||||
components = { Attachment };
|
||||
static components = { Attachment };
|
||||
state = { isAttachmentDeleted: false };
|
||||
doStuff() {
|
||||
this.dispatch("deleteAttachment", 100);
|
||||
|
||||
+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++;
|
||||
@@ -15,7 +16,7 @@ class Counter extends owl.Component {
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Message extends owl.Component {
|
||||
components = { Counter };
|
||||
static components = { Counter };
|
||||
|
||||
shouldUpdate(nextProps) {
|
||||
return nextProps.message !== this.props.message;
|
||||
@@ -31,8 +32,9 @@ class Message extends owl.Component {
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class App extends owl.Component {
|
||||
components = { Message };
|
||||
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
|
||||
static components = { Message };
|
||||
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>
|
||||
|
||||
+128
-126
@@ -1,5 +1,5 @@
|
||||
import { SAMPLES } from "./samples.js";
|
||||
|
||||
const {useState, useRef} = owl.hooks;
|
||||
//------------------------------------------------------------------------------
|
||||
// Constants, helpers, utils
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -150,145 +150,24 @@ Promise.all([loadTemplates(), owl.utils.whenReady()]).then(start);
|
||||
return zip.generateAsync({ type: "blob" });
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// MAIN APP
|
||||
//------------------------------------------------------------------------------
|
||||
class App extends owl.Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.version = owl.__info__.version;
|
||||
this.SAMPLES = SAMPLES;
|
||||
this.components = { TabbedEditor };
|
||||
|
||||
this.state = {
|
||||
js: SAMPLES[0].code,
|
||||
css: SAMPLES[0].css || "",
|
||||
xml: SAMPLES[0].xml || DEFAULT_XML,
|
||||
error: false,
|
||||
displayWelcome: true,
|
||||
splitLayout: true,
|
||||
leftPaneWidth: Math.ceil(window.innerWidth / 2),
|
||||
topPanelHeight: null
|
||||
};
|
||||
|
||||
this.toggleLayout = owl.utils.debounce(this.toggleLayout, 250, true);
|
||||
this.runCode = owl.utils.debounce(this.runCode, 250, true);
|
||||
this.downloadCode = owl.utils.debounce(this.downloadCode, 250, true);
|
||||
}
|
||||
|
||||
displayError(error) {
|
||||
this.state.error = error;
|
||||
if (error) {
|
||||
setTimeout(() => {
|
||||
this.refs.content.innerHTML = "";
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
runCode() {
|
||||
this.state.displayWelcome = false;
|
||||
let subiframe;
|
||||
let error = false;
|
||||
const errorHandler = e => this.displayError(e.message || e.reason.message);
|
||||
try {
|
||||
const { js, css, xml } = this.state;
|
||||
subiframe = makeCodeIframe(js, css, xml, errorHandler);
|
||||
} catch (e) {
|
||||
//probably problem with the templates
|
||||
error = e;
|
||||
// we still log the error, always useful to have it available
|
||||
console.error(e);
|
||||
}
|
||||
if (error) {
|
||||
this.displayError(error.message);
|
||||
return;
|
||||
} else {
|
||||
this.state.error = false;
|
||||
}
|
||||
this.refs.content.innerHTML = "";
|
||||
this.refs.content.appendChild(subiframe);
|
||||
}
|
||||
|
||||
setSample(ev) {
|
||||
const sample = SAMPLES.find(s => s.description === ev.target.value);
|
||||
this.state.js = sample.code;
|
||||
this.state.css = sample.css || "";
|
||||
this.state.xml = sample.xml || DEFAULT_XML;
|
||||
}
|
||||
|
||||
get leftPaneStyle() {
|
||||
return `width:${this.state.leftPaneWidth}px`;
|
||||
}
|
||||
|
||||
get rightPaneStyle() {
|
||||
return `width:${window.innerWidth - 6 - this.state.leftPaneWidth}px`;
|
||||
}
|
||||
|
||||
get topEditorStyle() {
|
||||
return `flex: 0 0 ${this.state.topPanelHeight}px`;
|
||||
}
|
||||
|
||||
onMouseDown() {
|
||||
const resizer = ev => {
|
||||
this.state.leftPaneWidth = ev.clientX;
|
||||
};
|
||||
|
||||
document.body.addEventListener("mousemove", resizer);
|
||||
for (let iframe of document.getElementsByTagName("iframe")) {
|
||||
iframe.classList.add("disabled");
|
||||
}
|
||||
|
||||
document.body.addEventListener("mouseup", () => {
|
||||
document.body.removeEventListener("mousemove", resizer);
|
||||
for (let iframe of document.getElementsByTagName("iframe")) {
|
||||
iframe.classList.remove("disabled");
|
||||
}
|
||||
});
|
||||
}
|
||||
updateCode(ev) {
|
||||
this.state[ev.detail.type] = ev.detail.value;
|
||||
}
|
||||
toggleLayout() {
|
||||
this.state.splitLayout = !this.state.splitLayout;
|
||||
}
|
||||
updatePanelHeight(ev) {
|
||||
if (!ev.detail.delta) {
|
||||
return;
|
||||
}
|
||||
let height = this.state.topPanelHeight;
|
||||
if (!height) {
|
||||
height = document.getElementsByClassName("tabbed-editor")[0].clientHeight;
|
||||
}
|
||||
this.state.topPanelHeight = height + ev.detail.delta;
|
||||
}
|
||||
|
||||
async downloadCode() {
|
||||
const { js, css, xml } = this.state;
|
||||
const content = await makeApp(js, css, xml);
|
||||
await owl.utils.loadJS("libs/FileSaver.min.js");
|
||||
saveAs(content, "app.zip");
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Tabbed editor
|
||||
//------------------------------------------------------------------------------
|
||||
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");
|
||||
@@ -362,6 +241,129 @@ class TabbedEditor extends owl.Component {
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// MAIN APP
|
||||
//------------------------------------------------------------------------------
|
||||
class App extends owl.Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.version = owl.__info__.version;
|
||||
this.SAMPLES = SAMPLES;
|
||||
|
||||
this.state = useState({
|
||||
js: SAMPLES[0].code,
|
||||
css: SAMPLES[0].css || "",
|
||||
xml: SAMPLES[0].xml || DEFAULT_XML,
|
||||
error: false,
|
||||
displayWelcome: true,
|
||||
splitLayout: true,
|
||||
leftPaneWidth: Math.ceil(window.innerWidth / 2),
|
||||
topPanelHeight: null
|
||||
});
|
||||
|
||||
this.toggleLayout = owl.utils.debounce(this.toggleLayout, 250, true);
|
||||
this.runCode = owl.utils.debounce(this.runCode, 250, true);
|
||||
this.downloadCode = owl.utils.debounce(this.downloadCode, 250, true);
|
||||
this.content = useRef("content");
|
||||
}
|
||||
|
||||
displayError(error) {
|
||||
this.state.error = error;
|
||||
if (error) {
|
||||
setTimeout(() => {
|
||||
this.content.el.innerHTML = "";
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
runCode() {
|
||||
this.state.displayWelcome = false;
|
||||
let subiframe;
|
||||
let error = false;
|
||||
const errorHandler = e => this.displayError(e.message || e.reason.message);
|
||||
try {
|
||||
const { js, css, xml } = this.state;
|
||||
subiframe = makeCodeIframe(js, css, xml, errorHandler);
|
||||
} catch (e) {
|
||||
//probably problem with the templates
|
||||
error = e;
|
||||
// we still log the error, always useful to have it available
|
||||
console.error(e);
|
||||
}
|
||||
if (error) {
|
||||
this.displayError(error.message);
|
||||
return;
|
||||
} else {
|
||||
this.state.error = false;
|
||||
}
|
||||
this.content.el.innerHTML = "";
|
||||
this.content.el.appendChild(subiframe);
|
||||
}
|
||||
|
||||
setSample(ev) {
|
||||
const sample = SAMPLES.find(s => s.description === ev.target.value);
|
||||
this.state.js = sample.code;
|
||||
this.state.css = sample.css || "";
|
||||
this.state.xml = sample.xml || DEFAULT_XML;
|
||||
}
|
||||
|
||||
get leftPaneStyle() {
|
||||
return `width:${this.state.leftPaneWidth}px`;
|
||||
}
|
||||
|
||||
get rightPaneStyle() {
|
||||
return `width:${window.innerWidth - 6 - this.state.leftPaneWidth}px`;
|
||||
}
|
||||
|
||||
get topEditorStyle() {
|
||||
return `flex: 0 0 ${this.state.topPanelHeight}px`;
|
||||
}
|
||||
|
||||
onMouseDown() {
|
||||
const resizer = ev => {
|
||||
this.state.leftPaneWidth = ev.clientX;
|
||||
};
|
||||
|
||||
document.body.addEventListener("mousemove", resizer);
|
||||
for (let iframe of document.getElementsByTagName("iframe")) {
|
||||
iframe.classList.add("disabled");
|
||||
}
|
||||
|
||||
document.body.addEventListener("mouseup", () => {
|
||||
document.body.removeEventListener("mousemove", resizer);
|
||||
for (let iframe of document.getElementsByTagName("iframe")) {
|
||||
iframe.classList.remove("disabled");
|
||||
}
|
||||
});
|
||||
}
|
||||
updateCode(ev) {
|
||||
this.state[ev.detail.type] = ev.detail.value;
|
||||
}
|
||||
toggleLayout() {
|
||||
this.state.splitLayout = !this.state.splitLayout;
|
||||
}
|
||||
updatePanelHeight(ev) {
|
||||
if (!ev.detail.delta) {
|
||||
return;
|
||||
}
|
||||
let height = this.state.topPanelHeight;
|
||||
if (!height) {
|
||||
height = document.getElementsByClassName("tabbed-editor")[0].clientHeight;
|
||||
}
|
||||
this.state.topPanelHeight = height + ev.detail.delta;
|
||||
}
|
||||
|
||||
async downloadCode() {
|
||||
const { js, css, xml } = this.state;
|
||||
const content = await makeApp(js, css, xml);
|
||||
await owl.utils.loadJS("libs/FileSaver.min.js");
|
||||
saveAs(content, "app.zip");
|
||||
}
|
||||
}
|
||||
App.components = { TabbedEditor };
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Application initialization
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
+162
-78
@@ -1,7 +1,8 @@
|
||||
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'
|
||||
@@ -9,9 +10,9 @@ class Greeter extends owl.Component {
|
||||
}
|
||||
|
||||
// Main root component
|
||||
class App extends owl.Component {
|
||||
components = { Greeter };
|
||||
state = { name: 'World'};
|
||||
class App extends Component {
|
||||
static components = { Greeter };
|
||||
state = useState({ name: 'World'});
|
||||
}
|
||||
|
||||
// Application setup
|
||||
@@ -45,18 +46,19 @@ const COMPONENTS_CSS = `.greeter {
|
||||
|
||||
const ANIMATION = `// The goal of this component is to see how the t-transition directive can be
|
||||
// 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: [] };
|
||||
components = { Counter };
|
||||
class App extends Component {
|
||||
state = useState({ flag: false, componentFlag: false, numbers: [] });
|
||||
static components = { Counter };
|
||||
|
||||
toggle(key) {
|
||||
this.state[key] = !this.state[key];
|
||||
@@ -183,11 +185,12 @@ const LIFECYCLE_DEMO = `// This example shows all the possible lifecycle hooks
|
||||
// methods in the console. Try modifying its state by clicking on it, or by
|
||||
// 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 {
|
||||
components = { DemoComponent };
|
||||
state = { n: 0, flag: true };
|
||||
class App extends Component {
|
||||
static components = { DemoComponent };
|
||||
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) {
|
||||
@@ -388,8 +450,8 @@ class TodoItem extends owl.Component {
|
||||
// TodoApp
|
||||
//------------------------------------------------------------------------------
|
||||
class TodoApp extends owl.store.ConnectedComponent {
|
||||
components = { TodoItem };
|
||||
state = { filter: "all" };
|
||||
static components = { TodoItem };
|
||||
state = useState({ filter: "all" });
|
||||
|
||||
static mapStoreToProps(state) {
|
||||
return {
|
||||
@@ -896,25 +958,24 @@ const RESPONSIVE = `// In this example, we show how we can modify keys in the gl
|
||||
//------------------------------------------------------------------------------
|
||||
class Navbar extends owl.Component {}
|
||||
|
||||
class ControlPanel extends owl.Component {
|
||||
components = { MobileSearchView };
|
||||
}
|
||||
class MobileSearchView extends owl.Component {}
|
||||
|
||||
class FormView extends owl.Component {
|
||||
components = { AdvancedComponent };
|
||||
class ControlPanel extends owl.Component {
|
||||
static components = { MobileSearchView };
|
||||
}
|
||||
|
||||
class AdvancedComponent extends owl.Component {}
|
||||
|
||||
class FormView extends owl.Component {
|
||||
static components = { AdvancedComponent };
|
||||
}
|
||||
|
||||
class Chatter extends owl.Component {
|
||||
messages = Array.from(Array(100).keys());
|
||||
}
|
||||
|
||||
class MobileSearchView extends owl.Component {}
|
||||
|
||||
|
||||
class App extends owl.Component {
|
||||
components = { Navbar, ControlPanel, FormView, Chatter };
|
||||
static components = { Navbar, ControlPanel, FormView, Chatter };
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -1053,17 +1114,18 @@ const SLOTS = `// We show here how slots can be used to create generic component
|
||||
//
|
||||
// Note that the t-on-click event, defined in the App template, is executed in
|
||||
// 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++;
|
||||
@@ -1071,9 +1133,9 @@ class Counter extends owl.Component {
|
||||
}
|
||||
|
||||
// Main root component
|
||||
class App extends owl.Component {
|
||||
components = {Card, Counter};
|
||||
state = {a: 1, b: 3};
|
||||
class App extends Component {
|
||||
static components = {Card, Counter};
|
||||
state = useState({a: 1, b: 3});
|
||||
|
||||
inc(key, delta) {
|
||||
this.state[key] += delta;
|
||||
@@ -1169,10 +1231,21 @@ const ASYNC_COMPONENTS = `// This example will not work if your browser does not
|
||||
// However, we don't want renderings of the other sub component to be delayed
|
||||
// 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 App extends owl.Component {
|
||||
components = {SlowComponent, NotificationList};
|
||||
state = { value: 0, notifs: [] };
|
||||
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
|
||||
return new Promise(resolve => setTimeout(resolve, 1500));
|
||||
}
|
||||
}
|
||||
|
||||
class NotificationList extends Component {}
|
||||
|
||||
class App extends Component {
|
||||
static components = {SlowComponent, NotificationList};
|
||||
state = useState({ value: 0, notifs: [] });
|
||||
|
||||
increment() {
|
||||
this.state.value++;
|
||||
@@ -1185,16 +1258,6 @@ class App extends owl.Component {
|
||||
}
|
||||
}
|
||||
|
||||
class SlowComponent extends owl.Component {
|
||||
willUpdateProps() {
|
||||
// simulate a component that needs to perform async stuff (e.g. an RPC)
|
||||
// with the updated props before re-rendering itself
|
||||
return new Promise(resolve => setTimeout(resolve, 1500));
|
||||
}
|
||||
}
|
||||
|
||||
class NotificationList extends owl.Component {}
|
||||
|
||||
const qweb = new owl.QWeb(TEMPLATES);
|
||||
const app = new App({ qweb });
|
||||
app.mount(document.body);
|
||||
@@ -1251,15 +1314,16 @@ const FORM = `// This example illustrate how the t-model directive can be used t
|
||||
// data between html inputs (and select/textareas) and the state of a component.
|
||||
// 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
|
||||
@@ -1272,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>
|
||||
@@ -1293,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>
|
||||
@@ -1317,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');
|
||||
@@ -1362,44 +1431,52 @@ 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 {
|
||||
components = { Window };
|
||||
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.components[id] = info.component;
|
||||
this.render();
|
||||
}
|
||||
|
||||
closeWindow(ev) {
|
||||
const id = ev.detail.id;
|
||||
delete this.components[id];
|
||||
delete this.constructor.components[id];
|
||||
const index = this.windows.findIndex(w => w.id === id);
|
||||
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);
|
||||
@@ -1408,11 +1485,12 @@ class WindowManager extends owl.Component {
|
||||
}
|
||||
}
|
||||
|
||||
class App extends owl.Component {
|
||||
components = { WindowManager };
|
||||
class App extends Component {
|
||||
static components = { WindowManager };
|
||||
wmRef = useRef("wm");
|
||||
|
||||
addWindow(name) {
|
||||
this.refs.wm.addWindow(name);
|
||||
this.wmRef.comp.addWindow(name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1453,7 +1531,7 @@ const WMS_XML = `<templates>
|
||||
t-on-update-z-index="updateZIndex"
|
||||
t-on-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>
|
||||
|
||||
@@ -1571,6 +1649,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,
|
||||
|
||||
Reference in New Issue
Block a user